springboot+shiro+redis实现session共享和cache共享

news2024/12/28 2:13:41

在分布式应用中,若是使用了负载均衡,用户第一次访问,连接的A服务器,进行了登录操作进入了系统,当用户再次操作时,请求被转发到了B服务器,用户并没有在B进行登录,此时用户又来到了登录页面,这是难以理解和接受的,这就引出了session共享。
对于shiro安全框架如何实现session共享?shiro共享分为两方面,一个是session共享,一个是cache共享。下面聊聊在springboot工程中整合shiro框架,并通过redis实现session共享和cache共享。

1、shiro

Apache Shiro 是 Java 的一个安全框架,提供认证、授权、加密、会话管理、与 Web 集成、缓存等。
在这里插入图片描述

  • SecurityManager即安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。
    SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。
  • SessionManager即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
  • CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能。
  • SessionDAO即会话dao,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库。

其中各个插件的依赖关系如下:
shiro-spring包:shiro-core子包和shiro-web子包
shiro-redis包:shiro-core子包
AuthorizationAttributeSourceAdvisor-》shiro-spring插件
ShiroFilterFactoryBean-》shiro-spring插件
DefaultWebSecurityManager-》shiro-web插件
DefaultWebSessionManager-》shiro-web插件
SecurityManager-》shiro-core插件
SessionManager-》shiro-core插件
CacheManager-》shiro-core插件
AbstractSessionDAO-》shiro-core插件
SessionDAO-》shiro-core插件
Cache-》shiro-core插件
RedisCache-》shiro-redis插件
RedisCacheManager-》shiro-redis插件
RedisSessionDAO-》shiro-redis插件
RedisClusterManager-》shiro-redis插件
RedisManager-》shiro-redis插件

通过上面可知,通过redis重写Cache、CacheManager、SessionDAO的实现类实现session共享和cache共享。其中shiro-redis插件也实现了session共享和cache共享,可参考自己实现一遍。

2、springboot整合redis

新建springboot工程,版本为2.7.x版本,引用redis依赖。

使用的是RedisTemplate来集中存储session和cache,一个列化工具继承RedisSerializer

public class SerializeUtils implements RedisSerializer {

    private static Logger logger = LoggerFactory.getLogger(SerializeUtils.class);

    public static boolean isEmpty(byte[] data) {
        return (data == null || data.length == 0);
    }

    /**
     * 序列化
     * @param object
     * @return
     * @throws SerializationException
     */
    @Override
    public byte[] serialize(Object object) throws SerializationException {
        byte[] result = null;

        if (object == null) {
            return new byte[0];
        }
        try (
                ByteArrayOutputStream byteStream = new ByteArrayOutputStream(128);
                ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteStream)
        ){

            if (!(object instanceof Serializable)) {
                throw new IllegalArgumentException(SerializeUtils.class.getSimpleName() + " requires a Serializable payload " +
                        "but received an object of type [" + object.getClass().getName() + "]");
            }

            objectOutputStream.writeObject(object);
            objectOutputStream.flush();
            result =  byteStream.toByteArray();
        } catch (Exception ex) {
            logger.error("Failed to serialize",ex);
        }
        return result;
    }

    /**
     * 反序列化
     * @param bytes
     * @return
     * @throws SerializationException
     */
    @Override
    public Object deserialize(byte[] bytes) throws SerializationException {

        Object result = null;

        if (isEmpty(bytes)) {
            return null;
        }
        try (
                ByteArrayInputStream byteStream = new ByteArrayInputStream(bytes);
                ObjectInputStream objectInputStream = new ObjectInputStream(byteStream)
        ){
            result = objectInputStream.readObject();
        } catch (Exception e) {
            logger.error("Failed to deserialize",e);
        }
        return result;
    }
}

RedisConfig配置类

@Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        // 1.创建RedisTemplate对象
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        // 2.加载Redis配置
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        // 3.配置key序列化
        RedisSerializer<?> stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // 4.配置Value序列化
//        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
//        ObjectMapper objMapper = new ObjectMapper();
//        objMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//        objMapper.activateDefaultTyping(objMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
//        jackson2JsonRedisSerializer.setObjectMapper(objMapper);
        SerializeUtils serializeUtils = new SerializeUtils();
        redisTemplate.setValueSerializer(serializeUtils);
        redisTemplate.setHashValueSerializer(serializeUtils);
        // 5.初始化RedisTemplate
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

RedisService工具类

public class RedisService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    //=============================common============================
    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 时间(秒)
     */
    public void expire(String key,long time){
        redisTemplate.expire(key, time, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public Boolean hasKey(String key){
        return redisTemplate.hasKey(key);
    }

    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String ... key){
        if(key!=null&&key.length>0){
            if(key.length==1){
                redisTemplate.delete(key[0]);
            }else{
                redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
            }
        }
    }

    /**
     * 批量删除key
     * @param keys
     */
    public void del(Collection keys){
        redisTemplate.delete(keys);
    }

    //============================String=============================
    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key){
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     * @param key 键
     * @param value 值
     */
    public void set(String key,Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 普通缓存放入并设置时间
     * @param key 键
     * @param value 值
     * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     */
    public void set(String key,Object value,long time){
        if(time>0){
            redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
        }else{
            set(key, value);
        }
    }

    /**
     * 使用scan命令 查询某些前缀的key
     * @param key
     * @return
     */
    public Set<String> scan(String key){
        Set<String> keys = this.redisTemplate.keys(key);
        return keys;
    }

    /**
     * 使用scan命令 查询某些前缀的key 有多少个
     * 用来获取当前session数量,也就是在线用户
     * @param key
     * @return
     */
    public Long scanSize(String key){
        long dbSize = this.redisTemplate.execute(new RedisCallback<Long>() {

            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                long count = 0L;
                Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().match(key).count(1000).build());
                while (cursor.hasNext()) {
                    cursor.next();
                    count++;
                }
                return count;
            }
        });
        return dbSize;
    }
}

3、session共享

shiro提供了自己的会话管理器sessionManager,其中有个属性叫sessionDao,它来处理所有的会话信息。对于sessionDao,shiro也提供了自己的实现。若是通过redis来实现session共享,则需要重写sessionDao。

新建实现类RedisSessionDao,并继承AbstractSessionDAO

@Component
public class ShiroRedisSessionDAO extends AbstractSessionDAO {

    private static Logger logger = LoggerFactory.getLogger(ShiroRedisSessionDAO.class);

    private static final String DEFAULT_SESSION_KEY_PREFIX = "shiro:session:";
    private String keyPrefix = DEFAULT_SESSION_KEY_PREFIX;
    private static final long DEFAULT_SESSION_IN_MEMORY_TIMEOUT = 1000L;
    /**
     * doReadSession be called about 10 times when login.
     * Save Session in ThreadLocal to resolve this problem. sessionInMemoryTimeout is expiration of Session in ThreadLocal.
     * The default value is 1000 milliseconds (1s).
     * Most of time, you don't need to change it.
     */
    private long sessionInMemoryTimeout = DEFAULT_SESSION_IN_MEMORY_TIMEOUT;
    /**
     * expire time in seconds
     */
    private static final int DEFAULT_EXPIRE = -2;
    private static final int NO_EXPIRE = -1;
    /**
     * Please make sure expire is longer than sesion.getTimeout()
     */
    private int expire = DEFAULT_EXPIRE;
    private static final int MILLISECONDS_IN_A_SECOND = 1000;
    @Autowired
    private RedisService redisService;
    @Autowired
    private ShiroRedisSessionIdGenerator shiroRedisSessionIdGenerator;
    private static ThreadLocal sessionsInThread = new ThreadLocal();

    @Override
    public void update(Session session) throws UnknownSessionException {
        //如果会话过期/停止 没必要再更新了
        try {
            if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
                return;
            }

            this.saveSession(session);
        } catch (Exception e) {
            logger.warn("update Session is failed", e);
        }
    }

    private void saveSession(Session session) throws UnknownSessionException {
        if (session == null || session.getId() == null) {
            logger.error("session or session id is null");
            throw new UnknownSessionException("session or session id is null");
        }
        String key = getRedisSessionKey(session.getId());
        if (expire == DEFAULT_EXPIRE) {
            this.redisService.set(key, session, (int) (session.getTimeout() / MILLISECONDS_IN_A_SECOND));
            return;
        }
        if (expire != NO_EXPIRE && expire * MILLISECONDS_IN_A_SECOND < session.getTimeout()) {
            logger.warn("Redis session expire time: "
                    + (expire * MILLISECONDS_IN_A_SECOND)
                    + " is less than Session timeout: "
                    + session.getTimeout()
                    + " . It may cause some problems.");
        }
        this.redisService.set(key, session, expire);
    }

    @Override
    public void delete(Session session) {
        if (session == null || session.getId() == null) {
            logger.error("session or session id is null");
            return;
        }
        try {
            redisService.del(getRedisSessionKey(session.getId()));
        } catch (Exception e) {
            logger.error("delete session error. session id= {}",session.getId());
        }
    }

    @Override
    public Collection<Session> getActiveSessions() {
        Set<Session> sessions = new HashSet<Session>();
        try {
            Set<String> keys = redisService.scan(this.keyPrefix + "*");
            if (keys != null && keys.size() > 0) {
                for (String key:keys) {
                    Session s = (Session) redisService.get(key);
                    sessions.add(s);
                }
            }
        } catch (Exception e) {
            logger.error("get active sessions error.");
        }
        return sessions;
    }

    @Override
    protected Serializable doCreate(Session session) {
        if (session == null) {
            logger.error("session is null");
            throw new UnknownSessionException("session is null");
        }
        Serializable sessionId = this.generateSessionId(session);
        this.assignSessionId(session, sessionId);
        this.saveSession(session);
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        if (sessionId == null) {
            logger.warn("session id is null");
            return null;
        }
        Session s = getSessionFromThreadLocal(sessionId);

        if (s != null) {
            return s;
        }

        logger.debug("read session from redis");
        try {
            s = (Session) redisService.get(getRedisSessionKey(sessionId));
            setSessionToThreadLocal(sessionId, s);
        } catch (Exception e) {
            logger.error("read session error. settionId= {}",sessionId);
        }
        return s;
    }

    private void setSessionToThreadLocal(Serializable sessionId, Session s) {
        Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get();
        if (sessionMap == null) {
            sessionMap = new HashMap<Serializable, SessionInMemory>();
            sessionsInThread.set(sessionMap);
        }
        SessionInMemory sessionInMemory = new SessionInMemory();
        sessionInMemory.setCreateTime(new Date());
        sessionInMemory.setSession(s);
        sessionMap.put(sessionId, sessionInMemory);
    }

    private Session getSessionFromThreadLocal(Serializable sessionId) {
        Session s = null;

        if (sessionsInThread.get() == null) {
            return null;
        }

        Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get();
        SessionInMemory sessionInMemory = sessionMap.get(sessionId);
        if (sessionInMemory == null) {
            return null;
        }
        Date now = new Date();
        long duration = now.getTime() - sessionInMemory.getCreateTime().getTime();
        if (duration < sessionInMemoryTimeout) {
            s = sessionInMemory.getSession();
            logger.debug("read session from memory");
        } else {
            sessionMap.remove(sessionId);
        }

        return s;
    }

    @Override
    public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) {
        super.setSessionIdGenerator(shiroRedisSessionIdGenerator);
    }

    private String getRedisSessionKey(Serializable sessionId) {
        return this.keyPrefix + sessionId;
    }

    public RedisService getRedisService() {
        return redisService;
    }

    public void setRedisService(RedisService redisService) {
        this.redisService = redisService;
    }

    public String getKeyPrefix() {
        return keyPrefix;
    }

    public void setKeyPrefix(String keyPrefix) {
        this.keyPrefix = keyPrefix;
    }

    public long getSessionInMemoryTimeout() {
        return sessionInMemoryTimeout;
    }

    public void setSessionInMemoryTimeout(long sessionInMemoryTimeout) {
        this.sessionInMemoryTimeout = sessionInMemoryTimeout;
    }

    public int getExpire() {
        return expire;
    }

    public void setExpire(int expire) {
        this.expire = expire;
    }
}
public class ShiroRedisSessionManager extends DefaultWebSessionManager {
    /**
     * 注入自己的sessionDAO
     */
    @Autowired
    private ShiroRedisSessionDAO shiroRedisSessionDAO;

    /**
     * @description: 重写构造器
     * @author: ldc
     * @date: 2021/5/22 15:20
     */
    public ShiroRedisSessionManager() {
        super();
    }

    /**
     * @description: 重写方法实现从请求头获取Token便于接口统一,每次请求进来,Shiro会去从请求头找REQUEST_HEADER这个key对应的Value(Token)
     * @author: ldc
     * @date: 2021/5/22 15:20
     */
    @Override
    public Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String token = WebUtils.toHttp(request).getHeader("Authorization");
        // 如果请求头中存在token 则从请求头中获取token
        if (!ObjectUtils.isEmpty(token)) {
            // 去掉URL中的JSESSIONID
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "Stateless request");
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return token;
        } else {
            // 否则按默认规则从cookie取token
            return super.getSessionId(request, response);
        }
    }

    @Override
    public void setSessionDAO(SessionDAO sessionDAO) {
        super.setSessionDAO(shiroRedisSessionDAO);
    }
}

4、cache共享

shijro提供了自己的缓存管理器cacheManager,此时只需实现shiro的CacheManager和Cache接口即可。前者暴漏了获取Cache实例的方法,后者提供了Cache实例的增删改查操作。若是通过redis来实现cache共享,则需要重写cache和cacheManager。

public class ShiroRedisCache<K, V> implements Cache<K, V> {

    private static Logger logger = LoggerFactory.getLogger(ShiroRedisCache.class);

    private RedisService redisService;
    private String keyPrefix = "shiro:cache:";
    private int expire;
    private String principalIdFieldName = "id";

    public ShiroRedisCache(RedisService redisService, String prefix, int expire, String principalIdFieldName) {
        if (redisService == null) {
            throw new IllegalArgumentException("redisService cannot be null.");
        }
        this.redisService = redisService;
        if (prefix != null && !"".equals(prefix)) {
            this.keyPrefix = prefix;
        }
        if (expire != -1) {
            this.expire = expire;
        }
        if (principalIdFieldName != null && !"".equals(principalIdFieldName)) {
            this.principalIdFieldName = principalIdFieldName;
        }
    }

    @Override
    public V get(K key) throws CacheException {
        logger.debug("get key [{}]",key);
        if (key == null) {
            return null;
        }
        try {
            String redisCacheKey = getRedisCacheKey(key);
            Object rawValue = redisService.get(redisCacheKey);
            if (rawValue == null) {
                return null;
            }
            V value = (V) rawValue;
            return value;
        } catch (Exception e) {
            throw new CacheException(e);
        }
    }

    @Override
    public V put(K key, V value) throws CacheException {
        logger.debug("put key [{}]",key);
        if (key == null) {
            logger.warn("Saving a null key is meaningless, return value directly without call Redis.");
            return value;
        }
        try {
            String redisCacheKey = getRedisCacheKey(key);
            redisService.set(redisCacheKey, value, expire);
            return value;
        } catch (Exception e) {
            throw new CacheException(e);
        }
    }

    @Override
    public V remove(K key) throws CacheException {
        logger.debug("remove key [{}]",key);
        if (key == null) {
            return null;
        }
        try {
            String redisCacheKey = getRedisCacheKey(key);
            Object rawValue = redisService.get(redisCacheKey);
            V previous = (V) rawValue;
            redisService.del(redisCacheKey);
            return previous;
        } catch (Exception e) {
            throw new CacheException(e);
        }
    }

    private String getRedisCacheKey(K key) {
        if (key == null) {
            return null;
        }
        return this.keyPrefix + getStringRedisKey(key);
    }

    private String getStringRedisKey(K key) {
        String redisKey;
        if (key instanceof PrincipalCollection) {
            redisKey = getRedisKeyFromPrincipalIdField((PrincipalCollection) key);
        } else {
            redisKey = key.toString();
        }
        return redisKey;
    }

    private String getRedisKeyFromPrincipalIdField(PrincipalCollection key) {
        Object principalObject = key.getPrimaryPrincipal();
        if (principalObject instanceof String) {
            return principalObject.toString();
        } else {
            Method pincipalIdGetter = this.getPrincipalIdGetter(principalObject);
            return this.getIdObj(principalObject, pincipalIdGetter);
        }
    }

    private String getIdObj(Object principalObject, Method pincipalIdGetter) {
        try {
            Object idObj = pincipalIdGetter.invoke(principalObject);
            if (idObj == null) {
                throw new PrincipalIdNullException(principalObject.getClass(), this.principalIdFieldName);
            } else {
                String redisKey = idObj.toString();
                return redisKey;
            }
        } catch (IllegalAccessException var5) {
            throw new org.crazycake.shiro.exception.PrincipalInstanceException(principalObject.getClass(), this.principalIdFieldName, var5);
        } catch (InvocationTargetException var6) {
            throw new org.crazycake.shiro.exception.PrincipalInstanceException(principalObject.getClass(), this.principalIdFieldName, var6);
        }
    }

    private Method getPrincipalIdGetter(Object principalObject) {
        Method pincipalIdGetter = null;
        String principalIdMethodName = this.getPrincipalIdMethodName();

        try {
            pincipalIdGetter = principalObject.getClass().getMethod(principalIdMethodName);
            return pincipalIdGetter;
        } catch (NoSuchMethodException var5) {
            throw new PrincipalInstanceException(principalObject.getClass(), this.principalIdFieldName);
        }
    }

    private String getPrincipalIdMethodName() {
        if (this.principalIdFieldName != null && !"".equals(this.principalIdFieldName)) {
            return "get" + this.principalIdFieldName.substring(0, 1).toUpperCase() + this.principalIdFieldName.substring(1);
        } else {
            throw new CacheManagerPrincipalIdNotAssignedException();
        }
    }

    @Override
    public void clear() throws CacheException {
        logger.debug("clear cache");
        Set<String> keys = null;
        try {
            keys = redisService.scan(this.keyPrefix + "*");
        } catch (Exception e) {
            logger.error("get keys error", e);
        }
        if (keys == null || keys.size() == 0) {
            return;
        }
        for (String key: keys) {
            redisService.del(key);
        }
    }

    @Override
    public int size() {
        Long longSize = 0L;
        try {
            longSize = new Long(redisService.scanSize(this.keyPrefix + "*"));
        } catch (Exception e) {
            logger.error("get keys error", e);
        }
        return longSize.intValue();
    }

    @Override
    public Set<K> keys() {
        Set<String> keys = null;
        try {
            keys = redisService.scan(this.keyPrefix + "*");
        } catch (Exception e) {
            logger.error("get keys error", e);
            return Collections.emptySet();
        }

        if (CollectionUtils.isEmpty(keys)) {
            return Collections.emptySet();
        }

        Set<K> convertedKeys = new HashSet<K>();
        for (String key:keys) {
            try {
                convertedKeys.add((K) key);
            } catch (Exception e) {
                logger.error("deserialize keys error", e);
            }
        }
        return convertedKeys;
    }

    @Override
    public Collection<V> values() {
        Set<String> keys = null;
        try {
            keys = redisService.scan(this.keyPrefix + "*");
        } catch (Exception e) {
            logger.error("get values error", e);
            return Collections.emptySet();
        }

        if (CollectionUtils.isEmpty(keys)) {
            return Collections.emptySet();
        }

        List<V> values = new ArrayList<V>(keys.size());
        for (String key : keys) {
            V value = null;
            try {
                value = (V) redisService.get(key);
            } catch (Exception e) {
                logger.error("deserialize values= error", e);
            }
            if (value != null) {
                values.add(value);
            }
        }
        return Collections.unmodifiableList(values);
    }

    public String getKeyPrefix() {
        return keyPrefix;
    }

    public void setKeyPrefix(String keyPrefix) {
        this.keyPrefix = keyPrefix;
    }

    public String getPrincipalIdFieldName() {
        return principalIdFieldName;
    }

    public void setPrincipalIdFieldName(String principalIdFieldName) {
        this.principalIdFieldName = principalIdFieldName;
    }
}
public class ShiroRedisCacheManager implements CacheManager {

    private final Logger logger = LoggerFactory.getLogger(ShiroRedisCacheManager.class);

    private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<>();
    @Autowired
    private RedisService redisService;
    /**
     * expire time in seconds
     */
    private static final int DEFAULT_EXPIRE = 1800;
    private int expire = DEFAULT_EXPIRE;
    /**
     * The Redis key prefix for caches
     */
    public static final String DEFAULT_CACHE_KEY_PREFIX = "shiro:cache:";
    private String keyPrefix = DEFAULT_CACHE_KEY_PREFIX;
    public static final String DEFAULT_PRINCIPAL_ID_FIELD_NAME = "id";
    private String principalIdFieldName = DEFAULT_PRINCIPAL_ID_FIELD_NAME;

    @Override
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        logger.debug("get cache, name={}",name);
        Cache cache = caches.get(name);

        if (cache == null) {
            cache = new ShiroRedisCache(redisService,keyPrefix + name + ":", expire, principalIdFieldName);
            caches.put(name, cache);
        }
        return cache;
    }

    public RedisService getRedisService() {
        return redisService;
    }

    public void setRedisService(RedisService redisService) {
        this.redisService = redisService;
    }

    public String getKeyPrefix() {
        return keyPrefix;
    }

    public void setKeyPrefix(String keyPrefix) {
        this.keyPrefix = keyPrefix;
    }

    public int getExpire() {
        return expire;
    }

    public void setExpire(int expire) {
        this.expire = expire;
    }

    public String getPrincipalIdFieldName() {
        return principalIdFieldName;
    }

    public void setPrincipalIdFieldName(String principalIdFieldName) {
        this.principalIdFieldName = principalIdFieldName;
    }
}

自定义异常类

public class PrincipalIdNullException extends RuntimeException {
    private static final long serialVersionUID = -3311055610511133072L;

    private static final String MESSAGE = "Principal Id shouldn't be null!";

    public PrincipalIdNullException(Class clazz, String idMethodName) {
        super(clazz + " id field: " +  idMethodName + ", value is null\n" + MESSAGE);
    }
}
public class PrincipalInstanceException extends RuntimeException {
    private static final long serialVersionUID = 5011741906229816259L;

    private static final String MESSAGE = "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.\n"
            + "Default value is authCacheKey or id, that means your principal object has a method called \"getAuthCacheKey()\" or \"getId()\"";

    public PrincipalInstanceException(Class clazz, String idMethodName) {
        super(clazz + " must has getter for field: " + idMethodName + "\n" + MESSAGE);
    }

    public PrincipalInstanceException(Class clazz, String idMethodName, Exception e) {
        super(clazz + " must has getter for field: " + idMethodName + "\n" + MESSAGE, e);
    }

}
public class CacheManagerPrincipalIdNotAssignedException extends RuntimeException {
    private static final String MESSAGE = "CacheManager didn't assign Principal Id field name!";

    public CacheManagerPrincipalIdNotAssignedException() {
        super("CacheManager didn't assign Principal Id field name!");
    }
}

5、配置共享

实现了session和cache的重写,但是此时还没有被spring shiro使用,所以需要通过配置来实现让容器使用自己实现的session和cache。

	@Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 自定义session会话管理
        securityManager.setSessionManager(sessionManager());
        // 自定义cache缓存管理
        securityManager.setCacheManager(cacheManager());
        // 自定义Realm验证
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }
    
	@Bean
    public CacheManager cacheManager() {
        ShiroRedisCacheManager shiroRedisCacheManager = new ShiroRedisCacheManager();
        return shiroRedisCacheManager;
    }

    @Bean
    public SessionManager sessionManager() {
        ShiroRedisSessionManager shiroRedisSessionManager = new ShiroRedisSessionManager();
        return shiroRedisSessionManager;
    }

6、小结

实现shiro安全框架中的session共享和cache共享主要是重写其存储实现,通过自定义实现基于redis的共享,当redis是集群时,也可以在springboot工程中使用lettuce客户端实现动态刷新集群拓扑,实现应用中认证、授权功能的高可用。使用shiro-redis插件实现的无法实现动态刷新集群拓扑,当redis集群中主节点宕掉后,应用中的认证、授权功能调用出现报错。具体的业务场景需要具体分析,多多深入解读shiro源码,以便更深入的应用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/688678.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL 主从复制[异步 同步 半同步复制] 读写分离 优化 (非常重要)

MySQL 主从复制 1、什么是读写分离&#xff1f; 读写分离&#xff0c;基本的原理是让主数据库处理事务性增、改、删操作&#xff08;INSERT、UPDATE、DELETE&#xff09;&#xff0c;而从数据库处理SELECT查询操作。数据库复制被用来把事务性操作导致的变更同步到集群中的从数据…

MVP: Multi-view Prompting Improves Aspect Sentiment Tuple Prediction

MVP: Multi-view Prompting Improves Aspect Sentiment Tuple Prediction 论文地址: https://arxiv.org/pdf/2305.12627.pdf 论文代码: https://github.com/ZubinGou/multi-view-prompting 1. 介绍 Multi-view Prompting (MVP) 1.1 研究目标 本文提出了多试图提示(MVP)模型…

python 深度学习 解决遇到的报错问题

目录 一、解决报错ModuleNotFoundError: No module named ‘tensorflow.examples 二、解决报错ModuleNotFoundError: No module named ‘tensorflow.contrib‘ 三、安装onnx报错assert CMAKE, ‘Could not find “cmake“ executable!‘ 四、ImportError: cannot import na…

67、基于51单片机ADXL345计步器系统设计(程序+原理图+PCB源文件+参考论文+开题报告+设计资料+元器件清单等)

摘 要 计步器是一种颇受欢迎的日常锻炼进度监控器&#xff0c;可以激励人们挑战自己&#xff0c;增强体质&#xff0c;帮助瘦身。早期设计利用加重的机械开关检测步伐&#xff0c;并带有一个简单的计数器。晃动这些装置时&#xff0c;可以听到有一个金属球来回滑动&#xff0c…

MongoDB集群搭建(四)

基础环境准备 安装Docker 创建Docker网络 因为需要使用Docker搭建MongoDB集群&#xff0c;所以先创建Docker网络 docker network create mongo-cluster docker network ls 创建挂载目录 创建对应的挂载目录来存储配置文件以及日志文件 # 创建配置文件目录 mkdir -p /opt/mongo…

MYSQL数据类型介绍

一、MySQL的数据类型 主要包括以下五大类&#xff1a; 整数类型&#xff1a;BIT、BOOL、TINY INT、SMALL INT、MEDIUM INT、 INT、 BIG INT 浮点数类型&#xff1a;FLOAT、DOUBLE、DECIMAL 字符串类型&#xff1a;CHAR、VARCHAR、TINY TEXT、TEXT、MEDIUM TEXT、LONGTEXT、TINY…

测试进阶面试必问12个算法题,洞悉出题思路,拿的就是高薪!

可以明确的一点是&#xff0c;面试算法题目在难度上&#xff08;尤其是代码难度上&#xff09;会略低一些&#xff0c;倾向于考察一些基础数据结构与算法&#xff0c;对于高级算法和奇技淫巧一般不作考察。 代码题主要考察编程语言的应用是否熟练&#xff0c;基础是否扎实&…

GPIO通用输入输出口

GPIO 1、简介1.1、基本结构1.2、工作模式使用库函数的使用方法 2、GPIO输出LED流水灯蜂鸣器 3、GPIO输入按键控制LED光敏传感器控制蜂鸣器 1、简介 1、GPIO(general Purpose Input Output)通用输入输出口&#xff1b; 2、可配置为8种输入输出模式&#xff1b; 3、引脚电平&…

如何在Linux部署Jdk1.8备忘录(高效版)

提示&#xff1a;高效简洁版 文章目录 前言一、整理环境二、部署jdk1.8三、建立Java环境四、生效验证总结 前言 作为备忘录阐述&#xff0c;力求简洁明了&#xff0c;直接开始贴步骤。 一、整理环境 1.首先查看服务器上是否有Java&#xff0c;如果自带恭喜你不用装了。 java…

Linux5.gcc(g++),动静态链接,make和Makefile

1.gcc od 文件 :查看二进制文件。 2.动静态库(此处简单认识&#xff0c;详细内容后面介绍) ldd 文件 :查看文件所依赖的库。 file 文件 :查看文件的具体信息。 在Linux当中&#xff0c;程序在链接的时候&#xff0c;默认是动态链接(后缀是.so) 如果要使用静态链接&#xff…

Redis的3大特殊数据结构(3)-Geospatial

Geospatial地理空间&#xff0c;Redis 在 3.2 版本中加入了地理空间&#xff08;geospatial&#xff09;以及索引半径查询的功能&#xff0c;主要用在需要地理位置的应用上。将指定的地理空间位置&#xff08;经度、纬度、名称&#xff09;添加到指定的 key 中&#xff0c;这些…

JS逆向入门教程p1 浏览器设置 常用工具

1.准备工作 http 编程语言(网络apidavaScript) 逆向(js逆向 安卓逆向 ios逆向 PC逆向)调试 图像识别 下载城南Post助手、fiddler、wireshark(鲨鱼)抓包工具; 通过进程抓包 fiddler插件: f12配置推荐,实验功能全部关闭 时间线上的分配检测:js的运行时间线 关闭时间…

【软考网络管理员】2023年软考网管初级常见知识考点(21)-安装及配置DHCP服务器(图文结合)

涉及知识点 安装DHCP服务器&#xff0c;配置DHCP服务器&#xff0c;如何去安装DHCP服务器&#xff0c;如何去配置DHCP服务器&#xff0c;软考网络管理员常考知识点&#xff0c;软考网络管理员网络安全&#xff0c;网络管理员考点汇总。 原创于&#xff1a;CSDN博主-《拄杖盲学…

项目进度计划表的制作方法解析:简单易懂的步骤指南

项目进度计划表怎么做&#xff1f;创建项目进度表是项目管理的一个重要组成部分&#xff0c;它有助于确保项目的成功完成。它是一个详细的计划&#xff0c;概述了实现项目目标所需的工作范围、时间线、里程碑和资源。本文将讨论制定项目进度表所涉及的步骤。 1、定义项目范围: …

掌握Gradio的Audio模块:实时交互与多功能展示

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

C语言学习(二十七)---指针练习题(三)

在上一节的内容中&#xff0c;我们继续学习了字符数组的相关指针练习题&#xff0c;今天我们将继续将练习有关二维数组的指针练习题和有关结构体的指针练习题&#xff0c;好了&#xff0c;话不多说&#xff0c;开整&#xff01;&#xff01;&#xff01;&#xff01; 二维数组…

从零开始学习:如何使用Selenium和Python进行自动化测试?

安装selenium 打开命令控制符输入&#xff1a;pip install -U selenium 火狐浏览器安装firebug&#xff1a;www.firebug.com&#xff0c;调试所有网站语言&#xff0c;调试功能 Selenium IDE 是嵌入到Firefox 浏览器中的一个插件&#xff0c;实现简单的浏览器操 作的录制与回…

开源社 KCC@硅谷正式成立,搭建国际开源交流平台

大家好&#xff01;我很高兴向大家宣布一个重要的消息&#xff1a; 开源社在硅谷的KCC&#xff08;Kaiyuanshe City Community&#xff09;正式成立了&#xff01;作为开源社的一项重要举措&#xff0c;KCC硅谷将成为国际开源交流的桥梁&#xff0c;架起中国和全球开源社区之间…

第十八章、Spring中的事务属性:隔离、传播属性等

1.什么是事务属性 属性&#xff1a;描述物体特征的一系列值 事务属性&#xff1b;描述事务特征的一系列值 1. 隔离属性 2. 传播属性 3. 只读属性 4. 超时属性 5. 异常属性2.如何添加事务属性 Transactional(isloation,propagation,readOnly,timeout,rollbackFor,norollbackFo…

three.js中物体的灯光与阴影设置

一、.设置物体阴影的核心步骤 1. 以平面上有一个球体为例&#xff0c;设置球体的阴影投射到平面上&#xff0c;核心步骤如下&#xff1a; 要让球体的阴影照射到平面上&#xff0c;需要使用阴影映射技术。具体步骤如下&#xff1a; 在渲染器中启用阴影&#xff1a; renderer…