springboot shiro框架整合多模式redis实现session共享以及注意事项(报错记录)

2022-08-21 14:26:27

背景:已有工程springboot+shiro框架,现需要接入redis并集群部署,故需要session共享,商定方案为通过shiro框架集成redis+session的模式实现session共享。

概述:实现shiro集成redis后,自定义实现RedisCache(对权限和认证信息的缓存处理)、RedisSessionDao、RedisCacheManager(缓存管理器),将登录过程的用户信息、数据库的权限菜单等session信息等放到放到redis缓存中。

本文不含整合redis部分

引入依赖:

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

shiro配置类:

package com.xdja.pki.security;

import com.xdja.pki.cache.config.RedisConfigConstants;
import com.xdja.pki.core.constants.RedisKey;
import com.xdja.pki.security.bean.Menu;
import com.xdja.pki.security.filter.CustomAuthorizationFilter;
import com.xdja.pki.security.filter.KickoutSessionControlFilter;
import com.xdja.pki.security.filter.SessionTimeoutFilter;
import com.xdja.pki.security.realm.CustomShiroRealm;
import com.xdja.pki.security.service.SecurityService;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisSentinelPool;
import redis.clients.util.Pool;

import javax.servlet.Filter;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * shiro配置类
 *
 * @author wyf
 */
@Configuration
public class ShiroConfig {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    protected SecurityService securityService;
    /**
     * redis部署模式,1-standalone,2-sentinel
     */
    @Value("${spring.redis.type}")
    private int type;
    /**
     * 系统标识
     */
    @Value("${system.flag}")
    private String systemFlag;

    @Autowired
    private Pool<Jedis> jedisPool;

    /**
     * 配置自定义认证器
     *
     * @return
     */
    @Bean
    public CustomShiroRealm customShiroRealm() {
        return new CustomShiroRealm(securityService);
    }

    /**
     * shiro缓存管理器;
     * 需要添加到securityManager中
     *
     * @return
     */
    @Bean
    public RedisCacheManager redisCacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        redisCacheManager.setKeyPrefix(MessageFormat.format(RedisKey.SHIRO_CACHE, systemFlag));
        //redis中针对不同用户缓存 一个登陆用户(AdminCertCardNoToken)的标识
        redisCacheManager.setPrincipalIdFieldName("signSn");
        return redisCacheManager;
    }

    /**
     * 集成 redis 支持两种模式
     *
     * @return
     */
    @Bean
    public IRedisManager redisManager() {
        /**
         *  redis部署模式,1-standalone,2-sentinel,3-cluster
         */
        if (RedisConfigConstants.REDIS_TYPE_STANDALONE == type) {
            RedisManager redisManager = new RedisManager();
            redisManager.setJedisPool((JedisPool) jedisPool);
            return redisManager;
        } else if (RedisConfigConstants.REDIS_TYPE_SENTINEL == type) {
            RedisSentinelManager redisSentinelManager = new RedisSentinelManager();
            redisSentinelManager.setJedisPool((JedisSentinelPool) jedisPool);
            return redisSentinelManager;
        } else {
            return null;
        }
    }

    /**
     * 配置Session管理器  Default main session timeout value, equal to {@code 30} minutes.
     *
     * @return
     */
    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        sessionManager.setSessionIdCookie(rememberMeCookie());
        sessionManager.setGlobalSessionTimeout(30L * 60 * 1000);
        return sessionManager;
    }

    /**
     * 配置SessionDao
     *
     * @return
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        redisSessionDAO.setKeyPrefix(MessageFormat.format(RedisKey.SHIRO_SESSION, systemFlag));
        //session在redis中的保存时间,最好大于session会话超时时间(30min)
        redisSessionDAO.setExpire(30 * 60 * 1000);
        //session只能从redis中读取 不能读内存
        redisSessionDAO.setSessionInMemoryEnabled(false);
        return redisSessionDAO;
    }

    /**
     * 配置权限管理器
     *
     * @return
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customShiroRealm());
        securityManager.setSessionManager(sessionManager());
        securityManager.setCacheManager(redisCacheManager());
        return securityManager;
    }


    @Bean
    public SimpleCookie rememberMeCookie(){
        //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie();
        simpleCookie.setName("name");
        simpleCookie.setHttpOnly(true);
        simpleCookie.setSecure(true);
        return simpleCookie;
    }

    /**
     * 配置Shiro Filter工厂
     *
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        logger.info("Shiro权限加载 ========== 开始");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());

        Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("authc", new SessionTimeoutFilter(securityService));
        filters.put("kickout", new KickoutSessionControlFilter(securityService));
        filters.put("perms", new CustomAuthorizationFilter(securityService));
        shiroFilterFactoryBean.setFilters(filters);

        Map<String, String> filterChainDefinitions = new LinkedHashMap<String, String>();

        Collection<String> whiteLinks = securityService.getWhiteLink();

        if (null != whiteLinks && !whiteLinks.isEmpty()) {
            for (String link : whiteLinks) {
                filterChainDefinitions.put(link, "anon");
            }
        }

        this.processPermission(filterChainDefinitions, securityService.getFunctions());

        // 对所有用户进行自定义拦截认证
        filterChainDefinitions.put("/**", "kickout, authc, perms");

//		logger.info("过滤器拦截定义集合 ========== "+ JsonUtils.object2Json(filterChainDefinitions));

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitions);

        logger.info("Shiro权限加载 ========== 完成");

        return shiroFilterFactoryBean;
    }

    /**
     * 开启shiro aop注解
     *
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 处理菜单权限过滤器链
     *
     * @param filterChainDefinitions 过滤器链集合
     * @param menus                  待处理菜单
     */
    private void processPermission(Map<String, String> filterChainDefinitions, Collection<Menu> menus) {
        if (null != menus && !menus.isEmpty()) {
            for (Menu menu : menus) {
                if (StringUtils.isNotBlank(menu.getName()) && StringUtils.isNotBlank(menu.getPermission())) {
                    filterChainDefinitions.put(menu.getPermissionKey(), "perms[" + menu.getPermission() + "]");
                }

                processPermission(filterChainDefinitions, menu.getChildren());
            }
        }
    }

}

注意和说明:(都是坑)

  • 放到缓存的东西必须实现序列化,比如session中的菜单信息,否则会报序列化错误;

  • redis连接密码为空时不要赋空值给redisManage;

if (StringUtils.isNotBlank(password)) {
    redisManager.setPassword(password);
}
// redisSentinelManager、redisClusterManager同

否则报错:

Caused by: redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
Caused by: redis.clients.jedis.exceptions.JedisDataException: ERR Client sent AUTH, but no password is set

  • 多项目共用redis的情况下,redisSessionDao和redisCacheManager的keyPrefix值建议不使用默认值;通过源码查看默认值如下:
/**
* The Redis key prefix for caches
*/
public static final String DEFAULT_CACHE_KEY_PREFIX = "shiro:cache:";
private String keyPrefix = DEFAULT_CACHE_KEY_PREFIX;
/**
* The Redis key prefix for session
*/
private static final String DEFAULT_SESSION_KEY_PREFIX = "shiro:session:";
private String keyPrefix = DEFAULT_SESSION_KEY_PREFIX;
  • redis为哨兵模式或集群模式时,redis的host为用逗号分割,例:ip:prot,ip:prot

可参照源码使用时为如下:

  • redisCacheManager.setPrincipalIdFieldName("signSn");该值为登录用户唯一标识,按实际情况设置,可参考项目中 extends UsernamePasswordToken这个类的某登录标识唯一用户值,默认值为id。否则报错:

    {
        "timestamp": 1605775820370,
        "status": 500,
        "error": "Internal Server Error",
        "exception": "org.crazycake.shiro.exception.PrincipalInstanceException",
        "message": "org.crazycake.shiro.exception.PrincipalInstanceException: class com.xdja.pki.security.bean.AdminCertCardNoToken must has getter for field: id
    We need a field to identify this Cache Object in Redis. So you need to defined an id field which you can get unique id to identify this principal. For example, if you use UserInfo as Principal class, the id field maybe userId, userName, email, etc. For example, getUserId(), getUserName(), getEmail(), etc.
    Default value is "id", that means your principal object has a method called "getId()"",
        "path": "/ra-web/v1/user/person/query"
    }

  •  另外shiro-redis依赖的版本太低可能会报错。

登录成功后通过redis客户端查看:

 今天也是学习的一天!

后话:虽然说是比较简单的集成,用某同事的话说就两行代码的事,真的是踩了很多的坑,呜呜呜!

  • 作者:java小白-
  • 原文链接:https://blog.csdn.net/weixin_42209368/article/details/110006026
    更新时间:2022-08-21 14:26:27