spring整合shiro系列(三)spring boot 整合 shiro

823 阅读6分钟

​本文已参与「新人创作礼」活动,一起开启掘金创作之路

之前写了两篇有关shiro和spring的文章,结合目前java开发spring boot框架是大势,所以有必要将shiro和spring boot进行一次整合。我在使用spring  boot的时候比较明显的一个感触就是原来spring的xml配置后面都使用java bean的形式替代了。总体看java代码的程度更加纯粹了。

首先看一下依赖

<!--shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
<!--redis-->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.2.3</version>
        </dependency>

第一个依赖是spring和shiro的依赖,相对于目前来说,版本不算新的。第二个是一个插件主要是使用redis去缓存shiro的一些授权认证信息。

然后就是shiro的配置过程。我的话喜欢把安全框架的内容放在一个包里面,这样感觉会更加清晰一点

第一步新建是个ShiroConfig。同时加上Configuration注解将它标记成一个配置类。然后我们定义几个变量

这里的变量主要是关联redis的配置,比如ip,秘密,端口,过期时间。可以将值写在配置文件yml里,也可以直接赋值。

这个是shiro官网整合spring时的配置(xml版)。大致我们可以看到这是定义了一个自定义的shiroFilter(过滤器)

里面配置了一些参数,主要时登录路由,登录成功路由,不用授权的路由,自定义的过滤器链,和自定义的过滤器,

还有就是securityManager安全管理器

下面的是spring boot版Java bean的格式。我们可以看到大多的参数只是换了一个形式而已。首先实例化了一个过滤器工厂,添加了两个自定义的过滤器分别是处理跨域和登录拦截的。然后声明了一个map,里面的key代表的是我们系统请求时用到的路由,而value代表的就是这个路由所需要的权限。这里的anon代表游客权限,具体可以看之前的博客文章。这里主要是放行了一些静态资源比如swagger接口文档和一些登录接口。

 /**
     * @description 过滤器
     * @author zhou
     * @created  2019/3/13 15:47
     * @param
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shrioFilter(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //自定义过滤器
        Map<String,Filter> filterMap = new LinkedHashMap<String,Filter>();
        filterMap.put("cros",crosFilter());
        filterMap.put("login",logInterceptor());
        shiroFilterFactoryBean.setFilters(filterMap);
        //拦截器
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/v2/**", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon");
        filterChainDefinitionMap.put("/back/admin/login/**","anon");
        filterChainDefinitionMap.put("/back/admin/sendCode/**","anon");
        filterChainDefinitionMap.put("/back/admin/forgetPassword/**","anon");
        filterChainDefinitionMap.put("/shop/**","anon");
        //filterChainDefinitionMap.put("/back/**","authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

然后比较重要的就是这个安全管理器了具体的授权逻辑在这里展开。这里主要是配置了三个东西第一个realm就是授权和验证的实现,第二个是session管理(会话管理,判断你是不是在一个会话里),第三个是缓存管理

   /**
     * @description 安全管理器
     * @author zhou
     * @created  2019/3/13 15:42
     * @param
     * @return
     */
    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        securityManager.setSessionManager(sessionManager());
        securityManager.setCacheManager(redisCacheManager());
        return securityManager;
    }

先看这个realm,还是比较简单的主要是两个方面,一是用户登录授权和验证的具体实现,二就是加密策略。下面来看下代码实现

realm这个和之前xml版的没有什么区别,我们创建一个MyShiroRealm.class然后继承AuthorizingRealm。这里主要是重写父类中授权和验证这两个方法。不同系统里面的内部业务逻辑可能会有所不同,但整体的思路是一样的。

第一个方法是授权,主要做的就是通过用户名去调用内部的业务逻辑获取你的角色和权限集合,然后将你的角色和权限存入到SimpleAuthorizationInfo 这个对象中,那么在后续调用方法的时候就可以联动注解进行权限的判断。一般可以将String类型的角色和权限存入。

第二个是登录的验证。这个方法主要是通过用户名去你的数据库中获取这个用户,然后对于这个用户可以先做一些禁用,过期的校验,然后将用户名密码封装后返回,交由shiro做后面的密码匹配。

 /**
     * @description 为当前登录的用户授予角色和权限
     * @author zhou
     * @created  2019/4/30 15:03    
     * @param 
     * @return 
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取用户名
        String name = (String) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        //查询用户的角色
        Set<String> roles = roleService.getRoleNameByUserName(name);
        authorizationInfo.setRoles(roles);
        return authorizationInfo;
    }

    /**
     * @description 验证当前的用户
     * @author zhou
     * @created  2019/4/30 16:28
     * @param 
     * @return 
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //获取用户信息
        String name = (String) authenticationToken.getPrincipal();
        //从数据库中查找
        TbUser tbUser = userMapper.getUserByName(name);
        if(null == tbUser){
            //账号不存在
            throw new UnknownAccountException();
        }else if(tbUser.getDeleted()){
            //账号被禁用
            throw new LockedAccountException();
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(tbUser.getName(),
                tbUser.getPassword(), ByteSource.Util.bytes(tbUser.getSalt()),getName());

        return authenticationInfo;
    }

密码工具类的话也是需要配置在自定义的Realm中
/**
     * @description 自定义Realm
     * @author zhou
     * @created  2019/3/13 15:43
     * @param
     * @return
     */
    @Bean
    public MyShiroRealm myShiroRealm(){
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myShiroRealm;
    }

具体的密码类如下,这里使用一个hash的密码工具类,定义了加密的算法和加密次数,对于密码使用了加盐处理。当我们之前的验证方法返回后,shiro就会去处理密码比对,这里就是根据你realm中配置的密码匹配器去做对应的处理

 /**
     * @description 密码比较器
     * @author zhou
     * @created  2018/12/27 9:34
     * @param
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashIterations(2);
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        return hashedCredentialsMatcher;
    }

 /**
     * @description 登录密码加盐
     * @author zhou
     * @created  2019/3/14 20:23
     * @param
     * @return
     */
    public TbUser encryptPassword(TbUser tbUser){
        if(null == tbUser.getSalt() || "".equals(tbUser.getSalt())){
             tbUser.setSalt(UUID.randomUUID().toString().replace("-",""));
        }
        String password = new SimpleHash(hashedCredentialsMatcher.getHashAlgorithmName(),tbUser.getPassword(),
                ByteSource.Util.bytes(tbUser.getSalt()),hashedCredentialsMatcher.getHashIterations()).toHex();
        tbUser.setPassword(password);
        return tbUser;
    }

    /**
     * @description 加密
     * @author zhou
     * @created  2019/5/13 15:39
     * @param password 密码
     * @param salt 盐
     * @return
     */
    public String getNewPassword(String password,String salt){
        String newPassword =  new SimpleHash(hashedCredentialsMatcher.getHashAlgorithmName(),password,
                ByteSource.Util.bytes(salt),hashedCredentialsMatcher.getHashIterations()).toHex();
        return newPassword;
    }

 上述的用户授权和登录验证。是通过下面这个方法经过shiro的层层调用进入的,具体大家断点一下就知道了,首先你需要在登录接口中获取当前会话的主体就是这个第一行代码,然后你需要将你的用户名和密码封装成shiro认可的UsernamePasswordToken对象,这个对象简单理解就是一个凭证。然后用主体把凭证作为参数调用login,后面就会去调用你自定义realm中的重写方法。

//获取主体
        Subject currentUser = SecurityUtils.getSubject();
        //判断用户是否登陆
        UsernamePasswordToken token = new UsernamePasswordToken(loginParam.getName(),loginParam.getPassword());

  currentUser.login(token);

下面说一下安全管理器另外配置的两个东西sessionManager和cacheManager(会话管理和缓存管理)。这个会话管理可以有不同的保存载体比如内存或redis.

这里我们看一下文档中对于会话管理器的定义和阐述,大致意思就是管理器用于对会话信息的增删改查,信息用于首次登录后程序的调用,用户可以选择数据库(mysql)或者Nosql(redis等)来做存储。

我这里使用redis来做session管理的数据访问层,因为shiro默认的会话是基于服务器内存的不适合生产环境,所以用redis做持久化,这里配置了一个RedisManager由会话管理和缓存管理共用。通用的配置也比较简单就是redis的序列化方式,端口之类的。

  /**
     * @description shiro-redis开源插件
     * @author zhou
     * @created  2019/3/22 17:01
     * @param
     * @return
     */
    @Bean
    public RedisManager redisManager(){
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host+":"+port);
        redisManager.setPassword(password);
        redisManager.setTimeout(timeout);
        return redisManager;
    }
/**
     * @description sessionDao层的实现
     * @author zhou
     * @created  2019/3/22 17:03
     * @param
     * @return
     */
    @Bean
    public RedisSessionDAO redisSessionDAO(){
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setKeySerializer(new StringSerializer());
        redisSessionDAO.setValueSerializer(new ObjectSerializer());
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

然后我们查看这个RedisSessionDAO的源码,可以看到他具体缓存了什么信息。这里应该是以sessionId为key,缓存了整个session的信息,

然后说一下缓存管理器,我最初也是比较模糊既然会话管理已经用了redis,为什么又整了一个缓存管理?其实按照文档的意思,会话管理仅仅是处理用户的登录和交互的session,而缓存管理器是缓存所有shiro需要缓存的数据的。同时当你配置了cacheManager的时候,sessionDAO在做持久化的时候会调用你的cacheManager。这是我对这个文档的一些理解

后面的话就是再增加一个激活注解的配置,这样就可以在方法上增加鉴权的注解

/**
     * @description 注解启用
     * @author zhou
     * @created  2018/12/27 9:35
     * @param
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

到这里的话,spring  boot 结合shiro大致的框架脉络已经清晰了,当然还有一些注解使用,登出,和源码的细节,由于篇幅有限就不扩展了。希望对大家使用spring boot 来整合shiro有所帮助