前言
写博客主要是为了记录一下工作中所遇到的一些问题,下一次再出现相同的问题,也好迅速的解决,本篇文章用来记录登录模块所遇到的一些坑。
一、场景分析
某天,前端小姐姐突然问我,为啥她的验证码一直过期(线上环境),经过一顿排查,发现是浏览器的问题,只有用谷歌浏览器(80版本之后)才会出现验证码失效问题。经过一顿搜索找到答案,谷歌浏览器在80版本之后,对SameSite属性做出了一些更改,它的默认值从None变为了Lax,桥豆麻袋!!!SameSite是什么玩意,我咋没听过?别急。现在就给你讲解讲解
Chrome 51 开始,浏览器的 Cookie 新增加了一个SameSite属性,用来防止 CSRF 攻击和用户追踪。
它有三个属性。分别是Strict,Lax,None
- Strict:完全禁止第三方获取cookie,跨站点时,任何情况下都不会发送cookie;
- Lax:大多数情况下禁止获取cookie,除非导航到目标网址的GET请求(链接、预加载、GET表单);
- None:没有限制。
在谷歌的80版本发布之后,对SameSite属性做了以下更改:
- 没有设置SameSite属性的默认SameSite=Lax;
- SameSite=None的cookie则必须设置为Secure,即安全链接(https)。
那这又跟验证码失效有什么关系呢,我的验证码存在redis中,key是用户的sessionId,发现每次请求验证码时,请求头中携带的sessionId都是不一样的,那么登录时校验验证码的key,肯定就不一样了,自然就会校验失败。知道了原因那么这个问题就差不多解决了百分之80
二、解决方案
1.浏览器解决方案
直接设置谷歌浏览器的属性,改变SameSite的默认配置即可,
在谷歌浏览器中输入chrome://flags/,然后按照下图进行配置,重新launch即可。
2.代码解决方案
在网上找了很多这里贴出几个帖子,反正我自己试了没有成功,或许是我太菜
1.关于解决Chrome新版本中cookie跨域携带和samesite的问题处理
2.方法二的话就用JWT,来实现无状态服务,下一篇博客再详细说明。
3.在请求验证码时,就将sessionId通过响应头的方式返回,前端在登录时,添加一个请求头,然后后端进行处理
代码如下
//创建验证码
public void create(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession();
String key = session.getId();
ValidateCodeProperties code = properties.getCode();
response.setHeader("sid",key);
response.setHeader("Access-Control-Expose-Headers", "sid");
setHeader(response, code.getType());
Captcha captcha = createCaptcha(code);
redisService.set(FebsConstant.CODE_PREFIX + key, StringUtils.lowerCase(captcha.text()), code.getTime());
captcha.out(response.getOutputStream());
}
自定义一个SessionManager,让它继承DefaultSessionManager,具体看代码
@Configuration
public class MySessionManager extends DefaultWebSessionManager {
private static final String TOKEN = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public MySessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(TOKEN);
//如果请求头中有 token 则其值为sessionId
if (!StringUtils.isEmpty(id)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
//否则按默认规则从cookie取sessionId
return super.getSessionId(request, response);
}
}
}
然后将自定义的sessionManager交给Shiro
@Bean
public SecurityManager securityManager(ShiroRealm shiroRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 配置 SecurityManager,并注入 shiroRealm
securityManager.setRealm(shiroRealm);
// 配置 shiro session管理器
securityManager.setSessionManager(sessionManager());
// 配置 缓存管理类 cacheManager
securityManager.setCacheManager(cacheManager());
// 配置 rememberMeCookie
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
@Bean
public DefaultWebSessionManager sessionManager() {
MySessionManager mySessionManager = new MySessionManager();
Collection<SessionListener> listeners = new ArrayList<>();
listeners.add(new ShiroSessionListener());
// 设置 session超时时间
mySessionManager.setGlobalSessionTimeout(febsProperties.getShiro().getSessionTimeout() * 1000L);
mySessionManager.setSessionListeners(listeners);
mySessionManager.setSessionDAO(redisSessionDAO());
mySessionManager.setSessionIdUrlRewritingEnabled(false);
return mySessionManager;
}
总结
大致思路就这几个吧,如果改浏览器会显得很业余,但总感觉session存也有点不妥,找个时间弄一个无状态的Token解决方案。
Token的解决方案已更新 JWT 的一站式解决方案