1. 背景
目前有个项目 线上环境 使用spring session管理的登录
项目中有两个接口
一个用来登录的 登录成功后会设置cookie 后续请求就会使用该cookie (cookie的键值就是session Id 和 登录后的信息 例如菜单,权限等)
一个用来检查是否登录的 根据session id 来判断是否登录 没有登录信息的 直接返回未登录
调用登录成功后 发现Set-Cookie 出现响应头中 但是 调用检查接口发现还是还是未登录
如下图 模拟登录接口 响应中正确返回了 session
但是检查登录却未将 模拟登陆返回的接口携带上 导致登录校验失败
2. 问题排查
刚开始怀疑两次请求的session id 不一致导致无法使用 模拟登录返回的cookie 但是在测试环境时 可以校验登录接口正常使用 模拟登陆返回的session
模拟登录的接口
验证登录的接口
那问题好像不是session不一致的问题。
因为目前使用spring-session来管理session 如果登录成功后 会将session id 以及登录信息 设置到cookie 同时存入到浏览器 应用的 cookie中 同时会将session 存入到 redis中 其他的接口就会携带
那目前设置没问题 那登录成功后是否 将session 存入到到浏览器应用的cookie 直接去检查 发现没有设置上
这里发现 模拟登录成功后 并没有将session 存入到 浏览器应用的cookie中
那么问题就变成了 为什么 没有将session 存入到 浏览器应用的cookie中
接下来就是一步步的修改
首先可以明确的是 页面部署在A域名 接口部署在B域名 这种请求肯定是跨域的 所以我们要在接口的NGINX中添加 跨域配置
#在指定的接口路径中才添加
location /xxxx/ {
add_header 'Access-Control-Allow-Origin' 'xxx';
add_header 'Access-Control-Allow-Headers' '*';
add_header 'Access-Control-Allow-Methods' '*';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
#代理的配置 这个是虚拟网关的配置 每个项目不一定相同
proxy_pass http://gateway;
}
- 修改过程中 首先将登录接口中设置cookie 时增加sameSite 参数 并设置值为None 表示 允许在跨站点请求中发送
public static void setCookie(HttpServletResponse response, String cookieName, String value, int maxAgeExpiry) {
//设置cookie 默认7天
ResponseCookie cookie = ResponseCookie.from(cookieName, value)
.httpOnly(true) // 禁止js读取
.secure(true) // 在http下也传输
.path("/") // path
.maxAge(maxAgeExpiry)
// 新增加的部分 允许在跨站点请求中发送
.sameSite("None")
.build()
;
// 设置Cookie Header
response.setHeader(HttpHeaders.SET_COOKIE, cookie.toString());
log.info("设置cookie完成,{}: {}", cookieName, cookie);
}
- 其次在前端 vue的页面调用接口时增加 使用凭证参数 由于使用的时 vue的 axios 请求框架 直接在项目中增加 配置即可 axios.defaults.withCredentials = true;
其他的方式 添加方式如下
使用vue.resource发送请求时配置如下:
main.js中 Vue.http.options.xhr = { withCredentials: true }
使用vue.axios发送请求时配置如下:
axios.defaults.withCredentials = true;
jquery请求带上 xhrFields: {withCredentials: true}, crossDomain: true;
$.ajax({
type: "post",
url: "",
xhrFields: {withCredentials: true},
crossDomain: true,
data: {username:$("#username").val()},
dataType: "json",
success: function(data){ }
});
- 最后在接口的NGINX配置中同样添加使用凭证参数
add_header 'Access-Control-Allow-Credentials' 'true';
本来以为添加完 应该可以正常请求了 但是 比之前错误更明显了 接口都无法调用了
结果一看前端页面的控制台 看到如下错误 更有点百思不得其解
Access to XMLHttpRequest at ‘https://xxx/checkAuthorityUpdate’ from origin ‘https://xxx’ has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
居然提示请求头不被允许 但是我的接口 nginx配置
add_header 'Access-Control-Allow-Headers '*';
这个配置意思 应该是允许所有的请求头吧 但是查询了下,看下ai的解释
这个错误提示是因为在进行跨域请求时,浏览器在发送实际请求之前会先发送一个预检请求(OPTIONS请求),这个请求包含了实际请求的所有信息,包括请求头。服务器在接收到这个预检请求后,需要返回一个响应,告诉浏览器哪些请求头是允许的。
在你的配置中,add_header ‘Access-Control-Allow-Headers’ ‘*’;设置的是允许所有请求头,但是浏览器在发送实际请求时,可能只发送了部分请求头,所以服务器需要明确指定允许哪些请求头。
你可以通过add_header ‘Access-Control-Allow-Headers’ ‘Content-Type, Authorization’;来指定允许的请求头为 “Content-Type” 和 “Authorization”。这样,浏览器在发送实际请求时,只有这两个请求头会被发送,服务器在接收到这个请求后,也会只检查这两个请求头,就不会出现这个错误了。
所以我们再次修改请求的配置
add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Accept-Language';
然后就大家都欢喜,校验登录的接口 也可以正常使用模拟登录的session ,模拟登录的session也存入到了应用的cookie里
突然在测试某个功能 更新接口 居然提示跨域请求 一看控制台发现 put 方法不被允许 报错详细如下
access to XMLHttpRequest at ‘https://xxx/updateAuditResult’ from origin 'https://xxx has been blocked by CORS policy: Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.
接口中的nginx配置
add_header 'Access-Control-Allow-Methods' '*';
难道又是不允许* 我们改成具体的尝试下看看
修改为具体的method如下
add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,DELETE';
果然可以了,至此 set-cookie 问题 解决完成
最后来汇总下解决办法
- 接口的nginx 跨域配置 例如 headers和 method 中的* 配置修改为具体的
#xx为接口前缀 请注意配置 如果没有前缀可以放在 / 下
location /xxx/ {
add_header 'Access-Control-Allow-Origin' 'xxx';
add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Accept-Language';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,DELETE';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://gateway;
}
- 接口中设置cookies的方法 增加 samsite属性 且 属性值为None
public static void setCookie(HttpServletResponse response, String cookieName, String value, int maxAgeExpiry) {
//设置cookie 默认7天
ResponseCookie cookie = ResponseCookie.from(cookieName, value)
.httpOnly(true) // 禁止js读取
.secure(true) // 在http下也传输
.path("/") // path
.maxAge(maxAgeExpiry)
// 新增加的部分 允许在跨站点请求中发送
.sameSite("None")
.build()
;
// 设置Cookie Header
response.setHeader(HttpHeaders.SET_COOKIE, cookie.toString());
log.info("设置cookie完成,{}: {}", cookieName, cookie);
}
- 在前端 vue的页面调用接口时增加 使用凭证参数 由于使用的时 vue的 axios 请求框架 直接在项目中增加 配置即可 axios.defaults.withCredentials = true;
其他的方式 添加方式如下
使用vue.resource发送请求时配置如下:
main.js中 Vue.http.options.xhr = { withCredentials: true }
使用vue.axios发送请求时配置如下:
axios.defaults.withCredentials = true;
jquery请求带上 xhrFields: {withCredentials: true}, crossDomain: true;
$.ajax({
type: "post",
url: "",
xhrFields: {withCredentials: true},
crossDomain: true,
data: {username:$("#username").val()},
dataType: "json",
success: function(data){ }
});
另外 登录成功session 不能生效问题有很多种 包括但不仅限于 如下
- sessionID 不同问题 前后两个请求的session不同 请注意判断是否 是真的不一致 (例如说 我在登录接口中 将session设置成功 并成功保存到了应用的cookie中 然后在校验登录接口传入的sessionId 却是另一个 这种就属于 sessionID 不一致的 )
解决: 这种问题不是很好找 需要排查是代码问题 还是使用的框架 问题 还是 接口NGINX配置问题 - set-cookie 失效 参考文中解决办法
- 登录请求和校验请求 cookie设置的domin 和 校验请求的domin不同 这种也会导致检验请求 无法使用 登录请求设置的 cookie 不过这种通常出现在 第三方授权中
- nginx配置的 Access-Control-Allow-Origin 和实际的 跨域地址不同 这个在页面控制台会报错 这个修改办法 将NGINX配置修改为和实际请求的地址一样即可
Access to XMLHttpRequest at ‘https://xxx/checkAuthorityUpdate’ from origin ‘https://xxx:8068’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: The ‘Access-Control-Allow-Origin’ header has a value ‘https://xxx’ that is not equal to the supplied origin
- 等等
god day ! ! !