@[TOC]
前言
在分布式系统中,会话管理是一个重要的问题。Shiro框架提供了一种解决方案,通过其会话管理组件来处理分布式会话。本文演示通过RedisSessionManager解决分布式会话问题。
Shiro 会话管理组件
Shiro框架的会话管理组件提供了会话的创建、维护、删除和失效等操作。在分布式环境中,多个应用服务器可能需要共享会话状态。为了实现这一点,Shiro框架提供了一些会话管理器实现,其中包括:
- DefaultSessionManager:默认会话管理器,提供了基本的会话管理功能。在分布式环境中,如果需要在多个应用服务器之间共享会话状态,则需要使用其他会话管理器。
- EnterpriseCacheSessionDAO:基于缓存的会话管理器,使用缓存来存储会话状态。在分布式环境中,可以使用分布式缓存来实现会话状态的共享。
- RedisSessionManager:基于Redis的会话管理器,使用Redis来存储会话状态。Redis是一个分布式缓存系统,可以在多个应用服务器之间共享会话状态。
使用这些会话管理器实现,可以将会话状态存储在分布式缓存中,以便在多个应用服务器之间共享。
配置 RedisSessionManager
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
编写配置
spring:
redis:
host: 192.168.0.10
port: 6379
password: xxxxx
声明SessionDAO的实现类,并重写核心方法
@Component
public class RedisSessionDAO extends AbstractSessionDAO {
@Resource
private RedisTemplate redisTemplate;
// 存储到Redis时,sessionId作为key,Session作为Value
// sessionId就是一个字符串
// Session可以和sessionId绑定到一起,绑定之后,可以基于Session拿到sessionId
// 需要给Key设置一个统一的前缀,这样才可以方便通过keys命令查看到所有关联的信息
private final String SHIOR_SESSION = "session:";
@Override
protected Serializable doCreate(Session session) {
System.out.println("Redis---doCreate");
//1. 基于Session生成一个sessionId(唯一标识)
Serializable sessionId = generateSessionId(session);
//2. 将Session和sessionId绑定到一起(可以基于Session拿到sessionId)
assignSessionId(session, sessionId);
//3. 将 前缀:sessionId 作为key,session作为value存储
redisTemplate.opsForValue().set(SHIOR_SESSION + sessionId,session,30, TimeUnit.MINUTES);
//4. 返回sessionId
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
//1. 基于sessionId获取Session (与Redis交互)
if (sessionId == null) {
return null;
}
Session session = (Session) redisTemplate.opsForValue().get(SHIOR_SESSION + sessionId);
if (session != null) {
redisTemplate.expire(SHIOR_SESSION + sessionId,30,TimeUnit.MINUTES);
}
return session;
}
@Override
public void update(Session session) throws UnknownSessionException {
System.out.println("Redis---update");
//1. 修改Redis中session
if(session == null){
return ;
}
redisTemplate.opsForValue().set(SHIOR_SESSION + session.getId(),session,30, TimeUnit.MINUTES);
}
@Override
public void delete(Session session) {
// 删除Redis中的Session
if(session == null){
return ;
}
redisTemplate.delete(SHIOR_SESSION + session.getId());
}
@Override
public Collection<Session> getActiveSessions() {
Set keys = redisTemplate.keys(SHIOR_SESSION + "*");
Set<Session> sessionSet = new HashSet<>();
// 尝试修改为管道操作,pipeline(Redis的知识)
for (Object key : keys) {
Session session = (Session) redisTemplate.opsForValue().get(key);
sessionSet.add(session);
}
return sessionSet;
}
}
将RedisSessionDAO交给SessionManager
@Bean
public SessionManager sessionManager(RedisSessionDAO sessionDAO) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(sessionDAO);
return sessionManager;
}
将SessionManager注入到SecurityManager
@Bean
public DefaultWebSecurityManager securityManager(ShiroRealm realm,SessionManager sessionManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
使用 RedisSession 问题
将传统的基于Web容器或者ConcurrentHashMap切换为Redis之后,发现每次请求需要访问多次Redis服务,这个访问的频次会出现很长时间的IO等待,对每次请求的性能减低了,并且对Redis的压力也提高了。
基于装饰者模式重新声明SessionManager中提供的retrieveSession方法,让每次请求先去request域中查询session信息,request域中没有,再去Redis中查询
public class DefaultRedisWebSessionManager extends DefaultWebSessionManager {
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
// 通过sessionKey获取sessionId
Serializable sessionId = getSessionId(sessionKey);
// 将sessionKey转为WebSessionKey
if(sessionKey instanceof WebSessionKey){
WebSessionKey webSessionKey = (WebSessionKey) sessionKey;
// 获取到request域
ServletRequest request = webSessionKey.getServletRequest();
// 通过request尝试获取session信息
Session session = (Session) request.getAttribute(sessionId + "");
if(session != null){
System.out.println("从request域中获取session信息");
return session;
}else{
session = retrieveSessionFromDataSource(sessionId);
if (session == null) {
//session ID was provided, meaning one is expected to be found, but we couldn't find one:
String msg = "Could not find session with ID [" + sessionId + "]";
throw new UnknownSessionException(msg);
}
System.out.println("Redis---doReadSession");
request.setAttribute(sessionId + "",session);
return session;
}
}
return null;
}
}
配置DefaultRedisWebSessionManager到SecurityManager中
@Bean
public SessionManager sessionManager(RedisSessionDAO sessionDAO) {
DefaultRedisWebSessionManager sessionManager = new DefaultRedisWebSessionManager();
sessionManager.setSessionDAO(sessionDAO);
return sessionManager;
}
Shiro的授权缓存
如果后台接口存在授权操作,那么每次请求都需要去数据库查询对应的角色信息和权限信息,对数据库来说,这样的查询压力太大了。
在Shiro中,发现每次在执行自定义Realm的授权方法查询数据库之前,会有一个执行Cache的操作。先从Cache中基于一个固定的key去查询角色以及权限的信息。
只需要提供好响应的CacheManager实例,还要实现一个与Redis交互的Cache对象,将Cache对象设置到CacheManager实例中。
将上述设置好的CacheManager设置到SecurityManager对象中
实现RedisCache
@Component
public class RedisCache<K, V> implements Cache<K, V> {
@Autowired
private RedisTemplate redisTemplate;
private final String CACHE_PREFIX = "cache:";
/**
* 获取授权缓存信息
* @param k
* @return
* @throws CacheException
*/
@Override
public V get(K k) throws CacheException {
V v = (V) redisTemplate.opsForValue().get(CACHE_PREFIX + k);
if(v != null){
redisTemplate.expire(CACHE_PREFIX + k,15, TimeUnit.MINUTES);
}
return v;
}
/**
* 存放缓存信息
* @param k
* @param v
* @return
* @throws CacheException
*/
@Override
public V put(K k, V v) throws CacheException {
redisTemplate.opsForValue().set(CACHE_PREFIX + k,v,15,TimeUnit.MINUTES);
return v;
}
/**
* 清空当前缓存
* @param k
* @return
* @throws CacheException
*/
@Override
public V remove(K k) throws CacheException {
V v = (V) redisTemplate.opsForValue().get(CACHE_PREFIX + k);
if(v != null){
redisTemplate.delete(CACHE_PREFIX + k);
}
return v;
}
/**
* 清空全部的授权缓存
* @throws CacheException
*/
@Override
public void clear() throws CacheException {
Set keys = redisTemplate.keys(CACHE_PREFIX + "*");
redisTemplate.delete(keys);
}
/**
* 查看有多个权限缓存信息
* @return
*/
@Override
public int size() {
Set keys = redisTemplate.keys(CACHE_PREFIX + "*");
return keys.size();
}
/**
* 获取全部缓存信息的key
* @return
*/
@Override
public Set<K> keys() {
Set keys = redisTemplate.keys(CACHE_PREFIX + "*");
return keys;
}
/**
* 获取全部缓存信息的value
* @return
*/
@Override
public Collection<V> values() {
Set values = new HashSet();
Set keys = redisTemplate.keys(CACHE_PREFIX + "*");
for (Object key : keys) {
Object value = redisTemplate.opsForValue().get(key);
values.add(value);
}
return values;
}
}
实现CacheManager
@Component
public class RedisCacheManager implements CacheManager {
@Autowired
private RedisCache redisCache;
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
return redisCache;
}
}
将RedisCacheManager配置到SecurityManager
@Bean
public DefaultWebSecurityManager securityManager(ShiroRealm realm, SessionManager sessionManager, RedisCacheManager redisCacheManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setSessionManager(sessionManager);
// 设置CacheManager,提供与Redis交互的Cache对象
securityManager.setCacheManager(redisCacheManager);
return securityManager;
}
启用授权缓存后,Shiro框架会将授权数据缓存在内存中,以便快速进行授权验证。在需要进行授权验证时,Shiro框架会首先从缓存中查找授权数据,如果缓存中不存在,则会从数据源中获取授权数据,并将其缓存到内存中。
需要注意的是,授权缓存可能会导致数据的过时。因此,在启用授权缓存时,需要根据具体的业务需求,设置合适的缓存失效时间和更新机制,以确保授权数据的实时性和准确性。