shiro实现共享session;springboot集成redis共享session;集群环境下shiro共享session

一、实现session共享

1. 聊聊session共享

如果是单机应用,session共享意义不大。使用默认的session缓存,还是存放到第三方应用缓存中,都可以。当然极端情况下,如果服务器内存非常小等极特殊情况下可能需要第三方缓存的。

session共享是针对集群(或分布式、或分布式集群)的情况下采用;如果不做session共享,仍然采用默认的方式(session存放到默认的servlet容器),当我们的应用是以集群的方式发布的时候,同个用户的请求会被分发到不同的集群节点(分发依赖具体的负载均衡规则),那么每个处理同个用户请求的节点都会重新生成该用户的session,这些session之间是毫无关联的。那么同个用户的请求会被当成多个不同用户的请求,这肯定是不行的。

2. shiro实现session共享(使用redis方式实现)

实现共享session是比较简单的,换一种说明你就能明白。大家都会增、删、改、查,session的操作就是增删改查的过程,只不过默认是缓存到servlet容器中,咱们要将数据转移到redis,来实现它的增删改查。这样在集群环境中,大家都访问这个redis,也就实现了共享session.

下面首先要了解下 shiro创建和缓存session的过程。

  1. DefaultWebSessionManager.java
    这个类是shiro的session管理类,我们在管理session生命周期时候,也该从这入手。
  2. AbstractSessionDAO.java
    这个类是session创建、保存、删除、更新操作的代码,默认会保存的servlet容器中。我们只需要继承这个类,并覆写对应代码即可实现session从redis中增删改查。下面来看看AbstractSessionDAO类官方的源码,其实也是增删改查:

protected abstract Serializable doCreate(Session session);

public void delete(Session session) {
        uncache(session);
        doDelete(session);
    }

public void update(Session session) throws UnknownSessionException {
        doUpdate(session);
        if (session instanceof ValidatingSession) {
            if (((ValidatingSession) session).isValid()) {
                cache(session, session.getId());
            } else {
                uncache(session);
            }
        } else {
            cache(session, session.getId());
        }
    }

protected abstract Session doReadSession(Serializable sessionId);

从源码中可以看出,共享sessionId说白了就是改变增删改查保存的位置。
默认session是保存的servlet缓存中,进行增删改查,现在咱们覆写方法,把增删改查的数据源改为redis。

  1. 新建实现类RedisSessionDao,并继承AbstractSessionDAO
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.Collection;
import java.util.concurrent.TimeUnit;

/**
 * @Author wangy
 * @create 2022/3/19 13:47
 * @Description  实现sessionDao,从而将session信息保存到redis中,达到集群环境共享session目的
 */
@Component
public class RedisSessionDao extends AbstractSessionDAO {
    /**
     * Session超时时间,单位为毫秒 当前设置半个小时
     */
    private long expireTime = 1800000;

    /**
     * 注入Redis操作类
     */
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 获取活跃的session,可以用来统计在线人数,如果要实现这个功能,可以在将session加入redis时指定一个session前缀,统计的时候则使用keys("session-prefix*")的方式来模糊查找redis中所有的session集合
     * @return
     */
    @Override
    public Collection<Session> getActiveSessions() {
        return redisTemplate.keys("*");
    }

    /**
     * 新增和保存session到redis中
     * @param session
     * @return
     */
    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = this.generateSessionId(session);
        this.assignSessionId(session, sessionId);

        redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.MILLISECONDS);
        return sessionId;
    }

    /**
     * 读取redis中的sessioin
     * @param sessionId
     * @return
     */
    @Override
    protected Session doReadSession(Serializable sessionId) {
        if (sessionId == null) {
            return null;
        }
        Session session = (Session) redisTemplate.opsForValue().get(sessionId);
        return session;
    }

    /**
     * 用户请求接口,然后修改session的有效期
     * @param session
     */
    @Override
    public void update(Session session) throws UnknownSessionException {
        if (session == null || session.getId() == null) {
            return;
        }
        //设置超时时间,这个是毫秒
        session.setTimeout(expireTime);
        redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.MILLISECONDS);
    }
    /**
     * session到期后删除session,比如说退出登录 logout
     * @param session
     */
    @Override
    public void delete(Session session) {
        if (null == session) {
            return;
        }
        redisTemplate.opsForValue().getOperations().delete(session.getId());
    }
}

这个类的代码中实现了对session的增删改查操作,大家应该到这比较容易理解了。

  1. 最后将实现类应用到DefaultWebSessionManager中。
    在自己的ShiroConfig配置类型,如下代码:
	@Bean
    public DefaultWebSecurityManager securityManager(RedisCachingSessionDao redisCachingSessionDao) {
        //新建security并设置realm、SessionManager
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(new AdminAuthorizingRealm());
        //新建SessionManager并设置SessionDao(session的获取途径)
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisCachingSessionDao);
        //应用sessionManager
        securityManager.setSessionManager(sessionManager);
        return securityManager;
    }

将自己的实现类redisCachingSessionDao,通过代码set到sessionManager中。
这样自己创建session后,可以从redis中查到自己的session数据了,共享session完成了,简单吧!

3. 共享缓存实现

共享session已经实现了,集群环境中能够访问相同的session数据源,但是shiro仍会有一些数据会缓存在servlet容器中,这样集群环境会出现一些其他的shiro配置数据各自用的还是各自的,出现各种各样问题。所以后面还需要解决 共享缓存的问题。
用到下面一些类:

  1. CacheManager.java
    这个类是shiro的缓存接口,默认会使用servlet容器的来充当缓存。
    我们看下源码,这个类定义了获取缓存的方法。
package org.apache.shiro.cache;

public interface CacheManager {
    <K, V> Cache<K, V> getCache(String var1) throws CacheException;
}
  1. Cache.java
    这个类是获取缓存方式的,通过getCache得到Cache的实现类,默认获取到的是shiro自己容器中,看下源码分析:
public interface Cache<K, V> {
    V get(K var1) throws CacheException;

    V put(K var1, V var2) throws CacheException;

    V remove(K var1) throws CacheException;

    void clear() throws CacheException;

    int size();

    Set<K> keys();

    Collection<V> values();
}

同样道理,咱们继承这个类,并覆写这个类的增删改查,从缓存到servlet,转移到redis中。

下面咱们定义自己的实现类
3. ShiroRedisCache

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
 * @Author wangy
 * @create 2022/3/19 11:55
 * @Description 缓存的实现类 shiro所有的缓存数据,都会存到redis中
 */
@Component
public class ShiroRedisCache<K,V> implements Cache<K,V>{

    /**
     * redis操作类
     */
    @Autowired
    private RedisTemplate<K,V> redisTemplate;

    /**
     * 定义缓存生效时间 为半个小时
     */
    private long expireTime = 1800;

    /**
     * 查询 操作
     */
    @Override
    public V get(K k) throws CacheException {
        return redisTemplate.opsForValue().get(k);
    }

    /**
     * 新增 操作
     */
    @Override
    public V put(K k, V v) throws CacheException {
        redisTemplate.opsForValue().set(k,v,expireTime, TimeUnit.SECONDS);
        return null;
    }

    /**
     * 删除 操作
     */
    @Override
    public V remove(K k) throws CacheException {
        V v = redisTemplate.opsForValue().get(k);
        redisTemplate.opsForValue().getOperations().delete(k);
        return v;
    }

    @Override
    public void clear() throws CacheException {
    }

    @Override
    public int size() {
        return 0;
    }

    @Override
    public Set<K> keys() {
        return null;
    }

    @Override
    public Collection<V> values() {
        return null;
    }
}

然后再次继承CacheManger,实现getCache方法
4. ShiroRedisCacheManager

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;

/**
 1. @Author wangy
 2. @create 2022/3/19 11:52
 3. @Description 实现缓存管理manager,所有缓存从redis中取数据
 */
@Component
public class ShiroRedisCacheManager implements CacheManager {
    /**
     * 注入自己的redisCache
     */
    @Resource
    private Cache shiroRedisCache;

    /**
     * 覆写方法
     */
    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return shiroRedisCache;
    }
}

最后将两个类set到Shrio配置中。
4. set到shiro配置类中

@Bean
    public DefaultWebSecurityManager securityManager(RedisCachingSessionDao redisCahingSessionDao, ShiroRedisCacheManager shiroRedisCacheManager) {
        //新建security并设置realm、CacheManager、SessionManager
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(new AdminAuthorizingRealm());
        //如果使用redis共享session,这个必须设置,因为集群之中 session要共享,同样一些缓存的数据也要共享,比如shiro缓存的数据
        securityManager.setCacheManager(shiroRedisCacheManager);
        //新建SessionManager并设置SessionDao(session的获取途径)
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisCahingSessionDao);
        securityManager.setSessionManager(sessionManager);
        return securityManager;
    }

上面代码通过securityManager设置cacheManager属性来使用redis缓存方式。

4. 总结

  1. 集群环境中shiro实现完共享session缓存,同样也要实现共享缓存,才能保证系统完美运行。
  2. 共享缓存不一定要用redis,大家通过覆写方法可以用mongodb、mysql、其他缓存工具等等都是可以实现的,当然要考虑效率和性能。
  3. 集群环境中负载均衡还可以通过ip_hash的机制将同个ip的请求定向到同一台后端,这样保证用户的请求始终是同一台服务处理,与单机应用基本一致了;但这有很多方面的缺陷(比如在同一个局域网环境下,都会分配到同一个ip,就失去负载均衡作用了)
    nginx负载均衡可以参考本人博文:https://blog.csdn.net/wangyue23com/article/details/108197650
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐