本文根据笔者开发过程(java)中遇到的问题整理而来,旨在减少大家查询无用资料的时间,对于一些原理解释可能不是很完美
首先,先让我们来看看Cookie的一些官方文档(rfc6265):
- 翻译版本:github.com/renaesop/bl…
- 规范文件原版:datatracker.ietf.org/doc/html/dr…
- CSRF预防:cheatsheetseries.owasp.org/cheatsheets…
相信你也看得一脸懵,接下来我说一下里面的关键词
-
5.2.3. The Domain Attribute(作用域)
-
5.2.5. The Secure Attribute(Secure属性)
-
4.1.2.7. The SameSite Attribute(SameSite属性)
操作复现:
环境:Java 11, SpringBoot2.7.0(Tomcat), Edge浏览器, Js/axios(fetch函数)
服务器端Controller层(API)代码:
@PostMapping("/api/v1/login")
public BaseResponse<?> test(HttpSession session,@RequestBody LoginRequest request){//BaseResponse 为自定义的成功响应
//执行登录逻辑
//...
//假设现在登录成功了
log.info("登录请求:{}",request);
Object successUserDO=request;
session.setAttribute("loginUser",successUserDO);
return BaseResponse.success();
}
@GetMapping("/api/v1/detail")
public BaseResponse<Object> getLogin(HttpSession session){
//前端获取登录信息和验证登录是否失效
return BaseResponse.success(session.getAttribute("loginUser"));
}
后端Config配置允许跨域:
@Configuration
public class MVCConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//配置允许跨域的路径
registry.addMapping("/**")
//配置允许访问的跨域资源的请求域名
.allowCredentials(true)
.allowedOriginPatterns("*")
//配置允许访问该跨域资源服务器的请求方法
.allowedMethods("*")
//配置允许请求 头部head的访问
.allowedHeaders("*");
}
}
前端调用代码:
const BaseURL="http://127.0.0.1:8080"
let data={
loginRequest:{
userName:"",
password:""
}
}
function userLogin(){//用户登录
fetch(BaseURL+"/api/v1/login",{
method:"POST",
headers:{
"Content-Type":"application/json",
},
credentials: "include",//前端跨域
mode:"cors",
body:JSON.stringify(data.loginRequest)
}).then(rep=>rep.json()).then((rep)=>{
console.log(rep)
})
}
function getUserDetail(){//获取用户详情
fetch(BaseURL+"/api/v1/detail",{
method:"GET",
headers:{
"Content-Type":"application/json",
}
credentials: "include",//前端跨域
mode:"cors",
}).then(rep=>rep.json()).then((rep)=>{
console.log(rep)
})
}
写完测试,一看:登录成功,但是请求/api/v1/detail的时候,拿到的却是空对象
登录:
获取用户信息:
这时候大概率就是前后端打架的时候了:
"我登录成功了为什么也还是拿不到"
"肯定是前端的问题"
......
我的思路和目前用的两套解决方案
总结原因:
这锅,原因有点复杂,但总的来说,是前后端分离+浏览器更新的锅
分析过程:
前置知识->后端Session:
简要来讲,java后端中的Session本质上是一个K-V键值对,也就是一个Map,key是string类型的,value是也是一 个Map<String,Object>,也就等价于Java中的Session看做对象的话,其实它可以写成 Map<String,Map<String,Object>>,那么,这个session的key(第一个String)是放在浏览器哪里的呢? 结合标题我们可以知道(bushi),它是放在浏览器的Cookie中的 这么做的原因如下:
- Cookie对于前端开发来说其实是无感的,每次请求由浏览器自带就行了
- 将Session-Key放Cookie中也便于用户管理
流程分析以及排错:
当一个http请求后端的时候
服务器的解析流程:
- 查看是否Cookie中携带JESSION字段,如果有,则去Map中找,如果有且不为null,则将Session指向那个value
- 如果没有JESSION字段,就自动生成一个JESSION,然后在Response的时候添加一条
key:Set-Cookie value: JESSSION=xxxx - 如果有JESSION字段,但是Map中查询到的内容是null,则执行第二步
图片如下:
然后就给Controller使用实例化好了的Session对象
问题查找:
我们点击Login请求的Cookie
再点击Detail请求的Cookie
发现了吗?每次请求都响应了一个Session,并且没有请求cookie, 也就是说每一次请求对于后端来说都是新的请求!
解释:
因为浏览器有个默认行为:检查Set-Cookie这个响应头是否为可接受的,其中和本问题相关的最重要的的就是一个Set-Cookie的属性:SameSite,关于SameSite,可以看看MDN上关于这部分的解释:developer.mozilla.org/zh-CN/docs/…
本文不过多说明
只需要知道,在浏览器发展中,已经将这一默认属性替换为SameSite=Lax了,而要使cookie能跨域发送的需要的是SameSite=None
解决方案:
- 替换掉Session在请求中存放的位置,由Cookie转为header(token解决方案) 优点:不用去考虑关于Cookie转session的事了 缺点:要后端自己去写查找转化逻辑代码(还有考虑过期失效的问题,这部分可以用JWT或者是Redis解决),前端也需要每次手动携带(也可以说要手动封装请求)token
2.前后端分离开发,但上线时将前端打包放进后端: 优点:代码方面可以完全不用做额外的操作 缺点:开发的时候需要前端配置代理服务器,上线后可能服务器压力很大(静态资源占用带宽较高)
- 后端配置过滤器,将Session手动设置 Tomcat
response.addHeader("Set-Cookie","JSESSIONID="+request.getSession().getId()+";SameSite=None;Secure");
Redis作为Session:
String id = session.getId();
String encode = Base64.encode(id);//Base64编码
response.setHeader("Set-Cookie", "SESSION=" + encode + ";Path=/;SameSite=None;Secure");
优点:可前后端分离式开发,相对于第一种来讲前端 缺点:后端需要有https能访问的域名