一、问题说明
项目主要技术:
SpringBoot 2.5+
MybatisPlus 3.4.2
Apache Shiro 1.8.0
其他:Thymeleaf,Quartz,Druid,Redis,Fastjson,lombok,velocity,swagger等
源码修改自【若依】开源系统
问题描述:
在启动项目后再第次打开登录页面登录总是报验证错误
问题原因:
Captcha生成验证码时会把验证码放在session中,第一次打开登录lonin页面输入验证登录,生成的验证码在sessionz中找不到了。
二、问题分析
问题出现在org.crazycake.shiro.RedisSessionDAO这里:
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(new ShiroRedisManager());
return redisSessionDAO;
}
打开登录页面请求后端时候生成session,shiro-redis会把session放在本地的ThreadLocal一份和Redis中一份。当生成验证码往session放之后会被ThreadLocal中的session同步掉,没有了验证码(断点跟代码时RedisSessionDAO 同一个请求超过一秒会访问Redis中的session,所以同步掉这一步我跟源码没有跟出来)
RedisSessionDAO的doReadSession在获取session时会优先从ThreadLocal获取
protected Session doReadSession(Serializable sessionId) {
//sessionInMemoryEnabled从本地获取session的开关
if (this.sessionInMemoryEnabled) {
this.removeExpiredSessionInMemory();
}
if (sessionId == null) {
logger.warn("session id is null");
return null;
} else {
Session session;
if (this.sessionInMemoryEnabled) {
session = this.getSessionFromThreadLocal(sessionId);
if (session != null) {
return session;
}
}
session = null;
try {
String sessionRedisKey = this.getRedisSessionKey(sessionId);
logger.debug("read session: " + sessionRedisKey + " from Redis");
session =
(Session)this.valueSerializer.deserialize(this.redisManager.get(this.keySerializer.serialize(sessionRedisKey)));
if (this.sessionInMemoryEnabled) {
this.setSessionToThreadLocal(sessionId, session);
}
} catch (SerializationException var4) {
logger.error("read session error. sessionId: " + sessionId);
}
return session;
}
}
三、解决问题
到这可以很明显看到怎么解决问题了把,把sessionInMemoryEnabled改为false不从ThreadLocal取就可以解决了,修改为
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setSessionInMemoryEnabled(false);
redisSessionDAO.setRedisManager(new ShiroRedisManager());
return redisSessionDAO;
}
四、衍生问题
频繁请求redis获取session会造成一定压力,而且也不太合适
解决方法:重写DefaultWebSessionManager类的retrieveSession方法,我们可以直接把 session 对象放进 request 里去!那么在单次请求周期内我们都可以从 request 中取 session 了,而且请求结束后 request 被销毁,作用域和生命周期的问题都不需要我们考虑了。 所以我们需要 Override 这个 retrieveSession() 方法,为此我们需要使用自定义的 SessionManager,如下:
public class ShiroSessionManager extends DefaultWebSessionManager {
private static final Logger logger = LoggerFactory.getLogger(ShiroSessionManager.class);
public ShiroSessionManager(){
super();
}
//重写这个方法为了减少多次从redis中读取session(自定义redisSessionDao中的doReadSession方法)
@Override
protected Session retrieveSession(SessionKey sessionKey){
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
if(sessionKey instanceof WebSessionKey){
request = ((WebSessionKey)sessionKey).getServletRequest();
}
if(request != null && sessionId != null){
Session session = (Session) request.getAttribute(sessionId.toString());
if(session != null){
return session;
}
}
Session session = super.retrieveSession(sessionKey);
if(request != null && sessionId != null){
request.setAttribute(sessionId.toString(),session);
}
return session;
}
}
修改 ShiroConfig配置
/**
* 会话管理器
*/
@Bean
public ShiroSessionManager sessionManager() {
ShiroSessionManager manager = new ShiroSessionManager();
// 加入缓存管理器
manager.setCacheManager(cacheManager());
// 删除过期的session
manager.setDeleteInvalidSessions(true);
// 设置全局session超时时间,单位:毫秒
manager.setGlobalSessionTimeout(expireTime * 60 * 1000);
// 去掉 JSESSIONID
manager.setSessionIdUrlRewritingEnabled(false);
//使用Redis的SessionDao
manager.setSessionDAO(redisSessionDAO());
return manager;
}
解决思路参考了:springboot + shiro 整合 redis 解决频繁访问 redis 和更新 session_快乐的小三菊的博客-优快云博客_redis频繁更新