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. CachingSessionDAO.java
    这个类是session创建、保存、删除、更新操作的代码,默认会保存的servlet容器中。我们只需要继承这个类,并覆写对应代码即可实现session从redis中增删改查。下面来看看CachingSessionDAO类官方的源码,其实也是增删改查:
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. 新建实现类RedisCachingSessionDao,并集成CachingSessionDAO
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
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 RedisCachingSessionDao extends CachingSessionDAO {
/**
* 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
protected void doUpdate(Session session) {
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
protected void doDelete(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,就失去负载均衡作用了)
    ​​​​