Shiro安全框架的使用

Shiro安全框架的使用(记录)

框架介绍

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码学和会话管理。
使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

Shrio的主要功能:

  • Authentication:用户认证(登录)
  • Authorization:权限控制 (授权)
  • Session Management:会话管理
  • Cryptography:数据加密
  • Web Support:支持web的API
  • Caching:缓存
  • Concurrency:支持多线程应用程序
  • Testing:测试的支持
  • Run As:假设一个用户为另一个用户的身份
  • Remember Me:在Session中保存用户身份

基本原理

Shiro的基本架构:

Shiro有三个核心的概念:Subject、SecurityManager和Realms。

Subject: Subject实质上是一个当前执行用户的特定的安全“视图”,开发者所写的应用代码就通过Subject与Shiro框架进行交互。所有Subject实例都必须绑定到一个SecurityManager上,当使用一个Subject实例时,Subject实例会和SecurityManager进行交互,完成相应操作。

SecurityManager: SecurityManager是Shiro的核心部分,作为一种“保护伞”对象来协调内部安全组件共同构成一个对象图。开发人员并不直接操作SecurityManager,而是通过Subject来操作SecurityManager来完成各种安全相关操作。

Realms: Realms担当Shiro和应用程序的安全数据之间的“桥梁”或“连接器”。从本质来讲,Realm是一个特定安全的DAO,Realm中封装了数据操作的模块和用户自定义的认证匹配过程。SecurityManager可能配置多个Realms,但至少要有一个。

Shiro过滤器

只解释了几个常用的,日后补充

在shiroFilter的配置中配置了要过滤的目录,其中一个号表示匹配当前目录后的参数,两个号表示匹配该目录及其子目录及参数。等号后面是Shiro各类过滤器的简称

Filter Name Class explanation
anon org.apache.shiro.web.filter.authc.AnonymousFilter 匿名过滤器
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter 表单认证过滤器
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
authcBearer org.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter
logout org.apache.shiro.web.filter.authc.LogoutFilter 退出登录过滤器
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter 记住我以后默认访问

缓存文件参数解释

FileName explanation
eternal 缓存中对象是否为永久的,如果是,超时设置将被忽略,对象从不过期
maxElementsInMemory 缓存中允许创建的最大对象数
overflowToDisk 内存不足时,是否启用磁盘缓存
timeToIdleSeconds 缓存数据的钝化时间,也就是在一个元素消亡之前,两次访问时间的最大时间间隔值,这只能在元素不是永久驻留时有效,如果该值是 0 就意味着元素可以停顿无穷长的时间
timeToLiveSeconds 缓存数据的生存时间,也就是一个元素从构建到消亡的最大时间间隔值,这只能在元素不是永久驻留时有效,如果该值是0就意味着元素可以停顿无穷长的时间
memoryStoreEvictionPolicy 缓存满了之后的淘汰算法
diskPersistent: 定在虚拟机重启时是否进行磁盘存储,默认为false
diskExpiryThreadIntervalSeconds: 属性可以设置该线程执行的间隔时间(默认是120秒,不能太小
FIFO 先进先出
LFU 最少被使用,缓存的元素有一个hit属性,hit值最小的将会被清出缓存
LRU 最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存

使用方法

1、maven中添加Shiro以及缓存的依赖

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <shiro.version>1.2.3</shiro.version>
  </properties>
<!--shiro框架的依赖-->
<dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>${shiro.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-web</artifactId>
      <version>${shiro.version}</version>
    </dependency>
    <!--shiro和spring的集成包-->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring</artifactId>
      <version>${shiro.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-quartz</artifactId>
      <version>${shiro.version}</version>
    </dependency>
    <!--缓存依赖-->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-ehcache</artifactId>
      <version>${shiro.version}</version>
    </dependency>
    <dependency>
      <groupId>net.sf.ehcache</groupId>
      <artifactId>ehcache-core</artifactId>
      <version>2.6.0</version>
    </dependency>

2、在web.xml添加核心过滤器

<!--配置过滤器,让Spring管理Shiro框架-->
  <filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  <!--将Shiro框架过滤器对应的生命周期交还给tomcat-->
    <init-param>
      <param-name>targetFilterLifecycle</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

3、新建spring-shiro.xml配置文件配置Shiro框架(同时让Spring监听器监听此文件)
为显示高亮效果,所有/**均写成了/ **(含空格),写的时候记得改过来

<!--配置spring监听器,默认只加载WEB-INF目录下的spring.xml配置文件-->
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <!--设置配置文件路径-->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring.xml,classpath:spring-shiro.xml</param-value>
  </context-param>

spring-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--配置Shiro框架-->

    <!-- Shiro的Web过滤器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">

        <!--注入安全管理器-->
        <property name="securityManager" ref="securityManager"></property>

        <!--认证成功页面,认证成功以后访问的页面-->
        <property name="successUrl" value="/index.jsp"></property>

        <!--认证失败页面,没有认证强制访问需要认证的页面自动跳转的页面-->
        <property name="loginUrl" value="/admin/login"></property>

        <!--自定义的filter-->
        <property name="filters">
            <map>
                <!-- 将自定义 的FormAuthenticationFilter注入shiroFilter中 -->
                <entry key="authc" value-ref="formAuthenticationFilter"/>
            </map>
        </property>

        <!--配置过滤器链-->
        <!-- 过虑器链定义,从上向下顺序执行,一般将/ ** 放在最下边 -->
        <property name="filterChainDefinitions">
            <value>
                <!--静态资源放行(匿名过滤器)anon	org.apache.shiro.web.filter.authc.AnonymousFilter-->
                /lib/ **=anon
                /static/ **=anon

                <!--页面-->
                /verifyCode.jsp = anon
                /login.jsp = anon
                /index.jsp = anon
                /welcome.jsp = anon

                <!--记住我以后访问的默认页面-->
                /index.jsp = user

                <!--退出登录过滤器 logout	org.apache.shiro.web.filter.authc.LogoutFilter-->
                /logout = logout

                <!--表单认证过滤器 authc	org.apache.shiro.web.filter.authc.FormAuthenticationFilter-->
                <!-- -/ **=authc 表示所有的url都必须认证通过才可以访问- -->
                / ** = authc
            </value>
        </property>

    </bean>

    <!--安全管理器-->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!--注入自定义realm-->
        <property name="realm" ref="customRealm"></property>

        <!--注入缓存管理器(防止用户授权操作一直反复执行)-->
        <property name="cacheManager">
            <bean class="org.apache.shiro.cache.ehcache.EhCacheManager">
                <!--缓存配置文件-->
                <property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/>
            </bean>
        </property>

        <!--注入会话管理器-->
        <property name="sessionManager" ref="sessionManager"></property>
        
        <!--记住我cookie-->
        <property name="rememberMeManager" ref="rememberMeManager"></property>
    </bean>

    <!--配置会话管理器(记住我功能)-->
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <!--删除残留的session(退出以后删除session)-->
        <property name="deleteInvalidSessions" value="true"/>
        <!--设置session时长 单位毫秒 1000*3600表示一小时-->
        <property name="globalSessionTimeout" value="#{1000 * 3600}"/>
    </bean>

    <!--记住我-->
    <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
        <property name="cookie">
            <bean class="org.apache.shiro.web.servlet.SimpleCookie">
                <constructor-arg value="rememberMe"/>
                <!--设置cookie时长 单位是秒-->
                <property name="maxAge" value="#{3600 * 24 * 3}"/>
            </bean>
        </property>
    </bean>



    <!--自定义realm-->
    <bean id="customRealm" class="com.shadow.shiro.CustomRealm">
        <!--注入凭证匹配器(密码加密)-->
        <property name="credentialsMatcher" ref="credentialsMatcher"></property>
    </bean>

    <!--数据库中存储到的md5的散列值,在realm中需要设置数据库中的散列值它使用散列算法及散列次数,让shiro进行散列对比时和原始数据库中的散列值使用的算法一致-->
    <!--配置凭证匹配器(密码加密)-->
    <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
        <!--指定加密算法-->
        <property name="hashAlgorithmName" value="MD5"></property>
        <!--散列次数-->
        <property name="hashIterations" value="3"></property>
    </bean>


    <!--自定义的from认证过滤器-->
    <bean id="formAuthenticationFilter" class="com.shadow.shiro.MyFormAuthenticationFilter">
        <!-- 表单中账号的input名称 -->
        <property name="usernameParam" value="username"/>
        <!-- 表单中密码的input名称 -->
        <property name="passwordParam" value="password"/>
        <property name="rememberMeParam" value="rememberMe"/>
    </bean>

    <!--完成授权操作*****************************-->

    <!--shiro为集成springMVC拦截异常-->
    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
                <!--没有权限异常跳转的页面-->
                <prop key="org.apache.shiro.authz.UnauthorizedException">unauthorized</prop>
            </props>
        </property>
    </bean>
    <!--开启aop,对代理类-->
    <aop:config proxy-target-class="true"/>

    <!--开启shiro的注解支持-->
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <!--注入安全管理器-->
        <property name="securityManager" ref="securityManager"></property>
    </bean>
</beans>

4、Ehcache的配置文件 ehcache-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" name="shiroCache">

    <!--缓存数据持久化的地址-->
    <diskStore path="C:\shiro\ehcache" />
    <defaultCache
            maxElementsInMemory="1000"
            maxElementsOnDisk="10000000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"
    />
</ehcache>

5、编写自定义reaml

public class CustomRealm extends AuthorizingRealm {

    /**
    *判断当前的请求中是否包含username和password表单参数
 	*   1、如果没有
 	*          说明当前不是登录页面,就判断当前Shiro框架是否认证过
 	*              如果认证过 放行
 	*              如果没有认证,跳转认证失败页面 <property name="loginUrl" value="/admin/login"></property>
 	*   2、如果有
 	*          进行realm自定义认证
 	*          认证成功 进认证成功页面  <property name="successUrl" value="index.jsp"></property>
 	*          认证失败 获取认证失败的错误信息,再shiro框架的FormAuthenticationFilter过滤器中共享admin/login 获取失败errorMsg,显示到login.jsp上

     * 自定义认证方法
     * 自定义认证思路:
     * 1、注入IUserService,service层
     *      @Autowired
     *      public IUserService userService;
     * 2、从token获取身份(账号信息)
     *      String username = (String)token.getPrincipal();
     * 3、调用IUserService层根据账号查询出用户信息的方法
     *     User user = userService.findUserByName(username);
     *      3.1 users不为null,说明账号存在
     *      3.2 users为null,当前方法直接返回null
     * 4、创建认证信息对象,将身份(数据查询的那个user对象,和user对象的密码(凭证)作为参数)传递出去
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Autowired
    private IUserService userService;

    @Autowired
    private IPermissionService permissionService;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("doGetAuthenticationInfo");
        //从token获取身份(账号信息)
        String username = (String)token.getPrincipal();

        //调用IUserService层根据账号查询出用户信息的方法
        User user = userService.findUserByName(username);
        if(user == null){
            return null;
        }
        //获取密码
        String password = user.getPassword();
        //获取盐
        String salt = user.getSalt();
        //创建认证信息对象 参数:身份 凭证
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, ByteSource.Util.bytes(salt), this.getName());
        return info;
    }

    /**
    *认证通过以后,因为方法基本都是从后台控制器跳过来的,可以使用权限注解,将需要有权限访问的资源打上@RequiresPermissions
 	*  注解,在SpringMVC的控制器上面打上注解以后,shiro会为控制器创建代理对象(运用aop),进行权限相关的控制
	 *          1、<aop:config proxy-target-class="true"/>shiro框架会创建控制器的代理对象
 	*          2、开启shiro的注解支持,代理对象可以拿到@RequiresPermissions注解
 	*          3、在底层进行权限判断,走doGetAuthenticationInfo方法,
 	*          4、程序运行时会根据有权限判断的地方获取权限表达式和设置shiro框架权限表达式数据进行比对
 	*              若有权限,则可直接访问
 	*              若无权限,则进入没有权限访问的页面,默认没有捕获异常,可以自己处理
     * 授权
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("doGetAuthorizationInfo");
        //获取当前主身份(认证的user对象)
        User user = (User) principals.getPrimaryPrincipal();

        //获取身份的角色的权限id
        String ids = user.getPermission_ids();

        //切割权限id字符串获取每一个权限的id值
        String[] split = ids.split(",");

        //创建List<Integer>.把(字符串数组split)转化成(整型数组idsList)
        List<Integer> idsList = new ArrayList<>();
        for(String id : split){
            idsList.add(Integer.valueOf(id));
        }

        //根据每一个权限的id值获取对应的权限表达式
        List<String> permissionExpressions = permissionService.selectPermissionByIds(idsList);
        System.out.println(permissionExpressions);

        //创建授权对象
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        //将查询出的权限表达式添加到权限信息对中
        info.addStringPermissions(permissionExpressions);
        return info;
    }
}

6、自定义表单认证过滤器

public class MyFormAuthenticationFilter extends FormAuthenticationFilter {

    /**
     * 记住我功能生效以后,Shiro会将用户身份数据存放到本地cookie,但是Shiro在运行过程中,
     * 使用session获取身份进行判断处理的,所以记住我以后会存在一个问题
     *
     * 当用户关闭浏览器以后,再次打开,除了在过滤器中设置记住我过滤器能够访问的页面以外,/index.jsp = user
     * 其他的页面还是无法认证
     *
     * 因为,Shiro从cookie中获取的信息并没有自动设置到session中,所以我们需要自己把cookie记住我信息存放到session中
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //从请求中获得Shiro的主体
        Subject subject = getSubject(request, response);
        //从主体中获取Shiro框架的session
        Session session = subject.getSession();

        //如果主体没有认证并且主体已经设置记住我了
        if(!subject.isAuthenticated()&&subject.isRemembered()){
            //获取主体的身份,从记住我的cookie中获取
            User principal = (User) subject.getPrincipal();
            //将身份认证信息共享到session中
            session.setAttribute("user",principal);
        }
        return subject.isAuthenticated() || subject.isRemembered();
    }

    /**
     * 重写FormAuthenticationFilter的onLoginSuccess方法
     * 指定的url传递进去,这样就实现了跳转到指定的页面;
     */
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
                                     ServletResponse response) throws Exception {

        WebUtils.getAndClearSavedRequest(request);//清理了session中保存的请求信息
        WebUtils.redirectToSavedRequest(request, response, getSuccessUrl());
        return false;
    }

    /**
     * 验证码验证
     * 从request请求对象获取验证码表单数据
     * 如果有:说明用户在做登录操作 要进行验证码判断
     *
     * 判断思路:
     * 1、获取用户提交的验证码
     * 2、从session获取共享的写入图片 随机数
     * 3、比对
     *          相同:放行,继续调用父类方法
     *          不同:跳转到登录页,并且共享错误信息
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //只有HttpServletRequest可以获取session
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        //从session获取验证码,正确的验证码
        HttpSession session = httpServletRequest.getSession();
        String validate = (String) session.getAttribute("sRand");

        //获取输入的验证码
        String myValidate = httpServletRequest.getParameter("verifyCode");

        //验证失败,设置错误信息
        if (StringUtils.isNotBlank(validate) && StringUtils.isNotBlank(myValidate)
                && !validate.toLowerCase().equals(myValidate.toLowerCase())) {
            httpServletRequest.setAttribute("errorMsg", "验证码错误");
            //拒绝访问
            return true;
        }
            //相同:放行,继续调用父类方法
            return super.onAccessDenied(request, response);
    }
}

7、控制器中方法(示例)

/**
 * 管理员页面控制器
 */
@Controller
@RequestMapping("/admin")
public class AdminController {

    @Autowired
    private IUserService userService;

    @Autowired
    private IRoleService roleService;

    /**
     * 认证失败页面,没有认证强制访问需要认证的页面自动跳转的页面
     * @return
     */
    @RequestMapping("/login")
    public String login(HttpServletRequest request,Model model){

        //获取认证失败的错误信息,再shiro框架的FormAuthenticationFilter过滤器中共享
        //共享的属性名称 shiroLoginFailure
        //共享shiro异常的字节码类型
        String shiroLoginFailure = (String)request.getAttribute("shiroLoginFailure");
        if(shiroLoginFailure != null){
            if(UnknownAccountException.class.getName().equals(shiroLoginFailure)){
                model.addAttribute("errorMsg","用户名错误");
            }else if(IncorrectCredentialsException.class.getName().equals(shiroLoginFailure)){
                model.addAttribute("errorMsg","密码错误");
            }
        }
        return "forward:/login.jsp";
    }

    @RequestMapping("/adminPage")
    @RequiresPermissions("admin:adminPage")
    public String adminPage(){
        return "adminPage";
    }
}

8、后台数据库设计(参考)
用户表
用户表
角色表
在这里插入图片描述
权限表
权限表

发布了2 篇原创文章 · 获赞 1 · 访问量 7

猜你喜欢

转载自blog.csdn.net/zyinger/article/details/105470386