在中,我们已经在 Spring Boot 项目中集成了 Shiro,并实现了权限控制。最后用 Redis 缓存用户权限信息的方式优化了 Shiro 鉴权的性能。
但这个项目有一个问题,即只能作为单体应用部署。原因是项目用于跟踪用户状态信息的 Session 实际上存储在内存中。如果对项目进行集群部署,多台服务器之间的内存中 Session 显然是无法共享的。
1.SessionDao
Shiro 使用的 Session 存储方式是由 SessionManager 决定的:
public DefaultWebSessionManager webSessionManager() {
DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
// 不使用 Session 的验证和更新机制
webSessionManager.setSessionValidationSchedulerEnabled(false);
// 设置 session 使用的 cookie
webSessionManager.setSessionIdCookieEnabled(true);
webSessionManager.setSessionIdCookie(simpleCookie());
// 设置 session 的有效时长(超过后 session 失效)单位毫秒
webSessionManager.setGlobalSessionTimeout(60 * 60 * 1000);
return webSessionManager;
}
这里DefaultWebSessionManager
继承自DefaultSessionManager
,而DefaultSessionManager
中有一个属性SessionDAO
,决定具体的 Session 存储方式,而默认的sessionDao
实现是MemorySessionDAO
:
public class DefaultSessionManager extends AbstractValidatingSessionManager implements CacheManagerAware {
protected SessionDAO sessionDAO;
// ...
public DefaultSessionManager() {
this.deleteInvalidSessions = true;
this.sessionFactory = new SimpleSessionFactory();
this.sessionDAO = new MemorySessionDAO();
}
}
显然,MemorySessionDAO
是一个在内存中实现 Session 存储的方案。
如果要改用其他的存储方式,我们可以选择实现一个自定义的SessionDao
以替换默认实现。这里我们使用 Redis 作为存储来实现 SessionDao
。
1.1.RedisTemplate
在实现SessionDao
之前,我们需要先实现一个配套的 RedisTemplate,之前我们实现的 RedisTemplate 对 Value 序列化的方式是 JSON,这样做的好处是 Redis 中存储的数据有良好的可读性,缺点是对于某些对象无法成功序列化,比如这里 Shiro 的 Session 对象。这里定义一个新的 RedisTemplate,并使用 JDK 对象序列化的方式对 Value 序列化和反序列化:
public class RedisConfig extends CachingConfigurerSupport {
// ...
public RedisTemplate<String, Session> sessionRedisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Session> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setValueSerializer(new JdkSerializationRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
1.2.RedisSessionDao
现在实现 SessionDao
:
public class RedisSessionDao extends AbstractSessionDAO {
name = "sessionRedisTemplate")
( private RedisTemplate<String, Session> redisTemplate;
private final String REDIS_SESSION_KEY_PREFIX = "session:";
/**
* session 过期时长,单位秒
*/
private Long timeout = 1800L;
/**
* 创建 Session
*
* @param session Session 对象
* @return sessionID
*/
protected Serializable doCreate(Session session) {
// 生成新的唯一的 SessionID
Serializable sessionId = generateSessionId(session);
// 为 Session 对象赋予新生成的 SessionID
assignSessionId(session, sessionId);
// 保存 Session
storeSession(sessionId, session);
return sessionId;
}
/**
* 获取 Redis 中存储的 Session 的 key
*
* @param id SessionID
* @return Redis 中存储的 key
*/
private String getRedisStoreSessionKey(Serializable id) {
return REDIS_SESSION_KEY_PREFIX + id.toString();
}
/**
* 将 Session 对象存储到 Redis
*
* @param id SessionID
* @param session Session 对象
*/
private void storeSession(Serializable id, Session session) {
ValueOperations<String, Session> ops = redisTemplate.opsForValue();
String key = getRedisStoreSessionKey(id);
ops.set(key, session, timeout, TimeUnit.SECONDS);
}
/**
* 从 Redis 读取 Session 对象
*
* @param sessionId SessionID
* @return Session 对象
*/
protected Session doReadSession(Serializable sessionId) {
ValueOperations<String, Session> ops = redisTemplate.opsForValue();
return ops.get(getRedisStoreSessionKey(sessionId));
}
/**
* 将更新后的 Session 对象写入 Redis
*
* @param session Session 对象
* @throws UnknownSessionException
*/
public void update(Session session) throws UnknownSessionException {
storeSession(session.getId(), session);
}
/**
* 从 Redis 中删除 Session 对象
*
* @param session
*/
public void delete(Session session) {
if (session == null) {
throw new NullPointerException("session argument cannot be null.");
}
Serializable id = session.getId();
if (id != null) {
ValueOperations<String, Session> ops = redisTemplate.opsForValue();
ops.getAndDelete(getRedisStoreSessionKey(id));
}
}
/**
* 获取存活的 Session 对象
*
* @return Session 对象序列
*/
public Collection<Session> getActiveSessions() {
Set<String> keys = redisTemplate.keys(REDIS_SESSION_KEY_PREFIX + "?");
ValueOperations<String, Session> ops = redisTemplate.opsForValue();
List<Session> sessions = new ArrayList<>();
keys.forEach(key -> {
Session session = ops.get(key);
if (session != null) {
sessions.add(session);
}
});
return sessions;
}
}
在配置类中使用这个 SessionDao 设置 SessionManager:
public class ShiroConfig {
// ...
public RedisSessionDao redisSessionDao(){
RedisSessionDao redisSessionDao = new RedisSessionDao();
redisSessionDao.setTimeout(sessionProperties.getTimeout());
return redisSessionDao;
}
public DefaultWebSessionManager webSessionManager() {
DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
webSessionManager.setSessionDAO(redisSessionDao());
// ...
return webSessionManager;
}
}
这里使用了一个配置类以读取 Spring 默认控制 Session 过期时间的配置spring.session.timeout
:
prefix = "spring.session")
(
public class SessionProperties {
// session 过期时长,单位秒
private Long timeout;
}
配置文件 application.yml 中过期时间设置为 30 分钟:
spring
session
timeout 1800 # session 超时时间,单位秒
2.测试
可以用 Nginx 搭建一个分布式应用进行测试。
在不同的两个端口启动两个应用实例:
在 Nginx 中配置负载均衡访问:
http {
upstream boot-demo {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
server {
listen 80;
server_name localhost;
location / {
root html;
proxy_pass http://boot-demo;
index index.html index.htm;
}
}
}
现在对 http://localhost:80 的请求会被转发到 http://localhost:8080 和 http://localhost:8081,这两个服务都使用同一个 Redis 作为 Session,所以都可以对用户的会话进行跟踪。
3.redis-shiro
实际上有一个开源项目 提供了基于 Redis 存储的 Shiro 完整整合方案,如果是 Spring Boot 项目,还可以很方便的用一个 Starter 快速完成整合。该方案不仅实现了一个 RedisSessionDao 以提供基于 Redis 的 Session 支持,还实现了基于 Redis 的 CacheManager,可以将 Realm 的认证和授权信息用 Redis 进行缓存。
如果你不需要对 Redis 存储 Shiro Session 以及缓存信息的细粒度控制,直接使用该开源项目是一个不错的方案。
The End,谢谢阅读。
本文的完整示例可以从获取。
文章评论