SpringBoot + Shiro + Redis 缓存配置,缓存认证和授权信息

Shiro 安全管理器 SecurityManager 可以配置缓存,就我使用, Shiro 可以缓存两类信息,一类是用户认证登录后,登录成功后的用户的信息,它们使用SessionManager管理。

Shiro 默认使用 Session 会话管理技术,第一次访问被拦截要求认证登录时,会生成 JSESSIONID 在响应头中,可以自行 F12 查看

所以,最简洁的代码是这样的

    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        return sessionManager;
    }
   @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setSessionManager(sessionManager());
        securityManager.setRealm(shiroRealm());
        return securityManager;
    }

但使用运行后会发现一个小 Bug,第一次访问浏览器会提示

Whitelabel Error Page
HTTP Status 400 – Bad Request

这是因为 Shiro 跳转登录认证,302 重定向 URL 中带 JESSIONID 导致。

解决办法也很简单,禁止就行。

    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;
    }

这是 Shiro 的 Session 缓存功能,接下来,认证成功后,会将用户信息缓存在服务器里,使用 JESSIONID 获取,这就是我们说的会话技术。

如果是单体项目,不需要负载均衡,上面这种方案其实也还好,也不是不能用,但还不够好。更好的方案是用 Redis 缓存用户登录后的个人信息,一样可以解决重定向 URL 带 JESSIONID的问题,而且这种方案还能实现分布式架构中 Session 共享,所以现在我们一般使用 Shiro + Redis 缓存数据。

用的比较多的框架是 Shiro-Redis 开源项目

	<dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.3.1</version>
            <exclusions>
                <exclusion>
                    <artifactId>shiro-core</artifactId>
                    <groupId>org.apache.shiro</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>jedis</artifactId>
                    <groupId>redis.clients</groupId>
                </exclusion>
            </exclusions>
        </dependency>

还记得我前面提到的缓存管理器是谁吗?SessionManager,这个逻辑要理清楚,因为这个插件还可以缓存其他东西,也就是开头说的第二类。

    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setSessionManager(sessionManager());
        securityManager.setRealm(shiroRealm());
        return securityManager;
    }

从如上代码可知,这里需要sessionManager()。行,那我们就来写一个。

   public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }

这里相比上面代码多了一行SessionDAO,它的参数 redisSessionDAO() 就是插件的内容,就是在这里引入 Redis缓存 Session 的。

    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * 使用的是shiro-redis开源插件
     */
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        redisSessionDAO.setExpire(redisProperties.getExpire());
        return redisSessionDAO;
    }

RedisSessionDAO 设置的两个参数,第二个是 Session 的过期时间,这个时间插件有要求,至少要比默认的半小时(1800000ms)要多,当然还可以设置成特殊的 -1 和 -2 ,具体意义看教程

另外一个参数是 RedisManager,这里面设置的参数属性就是 Redis 的配置信息。

   public RedisManager redisManager(){
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(redisProperties.getHost()+":"+redisProperties.getPort());
        redisManager.setDatabase(redisProperties.getDatabase());
        redisManager.setTimeout(redisProperties.getTimeout().getNano() * 1000);
        redisManager.setPassword(redisProperties.getPassword());
        return redisManager;
    }

Shiro-Redis 插件的配置差不多完成,使用后,连接 Redis就能发现有存储信息。

这里还要说个事,可能有些人使用这个插件,或者说这个框架时会碰到下面的错。

tried to access method redis.clients.jedis.JedisPool.returnResource

网上解决方案都说降版本,也就是降低依赖Shiro-Redis的版本,其实升版本一样可以,而且会向下兼容,建议使用如上我贴出的版本。

再来看第二类信息缓存,也就是用户权限信息缓存,使用CacheManager管理。

如果没给 Shiro 配置授权缓存,每次访问接口都会授权,也就是执行doGetAuthorizationInfo这个函数方法。但如果加了缓存,会先从缓存中查询是否有权限信息,如果有,就不再执行如上方法。如果没有,就执行然后缓存到 Redis 。

如果你手上有案例,可以测试对比,先不配置缓存,然后修改数据库的权限信息,doGetAuthorizationInfo 的授权规则马上就会生效,如果打断点,应该每次访问接口都会进入这个方法。不加缓存代码如下:

    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm());
        return securityManager;
    }

怎样配置授权的缓存呢?

Shiro 自带一个内存缓存MemoryConstrainedCacheManager,其原理就是利用 HashMap 缓存数据。

    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm());
        securityManager.setCacheManager(new MemoryConstrainedCacheManager());
        return securityManager;
    }

然后再试验如上代码,修改权限信息,就不会立刻生效,因为有缓存,就不会执行doGetAuthorizationInfo

上面是系统自带的缓存类,Shiro 也支持自己实现拓展,支持用 Redis 缓存。

怎么实现呢?上面的Shiro-Redis插件一样可以做到。

   @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm());
        securityManager.setCacheManager(cacheManager());
        return securityManager;
    }

这里的cacheManager也就是我们需要补全的

    @Bean
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        redisCacheManager.setPrincipalIdFieldName("userId");
        return redisCacheManager;
    }

简单解释一下上面俩参数,setRedisManager不用多说,就上面配置的Redis配置信息,如果上面用了,这里直接复用,而setPrincipalIdFieldName是用来设置主键字段名称的。这里有点奇怪,Redis缓存用户信息时,竟然没法识别我用@Id注解的主键,我记得之前版本是可以做到的。如果不加上这行设置,会报如下错误:

org.crazycake.shiro.exception.PrincipalInstanceException:

基本就这样,记得将实体类序列化,这里不用多说。

如果两类缓存都设置,Redis缓存的数据,完整应该是这样的。

    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setSessionManager(sessionManager());
        securityManager.setRealm(shiroRealm());
        securityManager.setCacheManager(cacheManager());
        return securityManager;
    }

这个插件使用下来,唯一遗憾的地方是缓存的对象是乱码,没法看清楚缓存的数据是啥,因为默认使用序列化是 ObjectSerializer,虽然官方教程提到可以自己实现org.crazycake.shiro.serializer.RedisSerializer,做其他形式的序列化和反序列化,比如用FastJsonRedisSerializer 或者GenericJackson2JsonRedisSerializer。但我尝试时,还是会碰到一些问题,这里就不分享了。

ok,就这样,希望如上教程能帮到你,虽然我个人挺喜欢Shiro,但以后用到机会不会很多,当前大家都喜欢用新的技术和玩法,比如Spring Security Oauth2,这篇文章也算是一个总结,万一以后用到还能想起。

本文由老郭种树原创,转载请注明:https://guozh.net/springboot-shiro-redis/

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注