前后端分离后产生的跨域问题sessionid丢失,cookies无法写入等
前言现在大部分项目都采用的前后端分离,比哪后台用spring boot ,前端用vue等。一、会话机制session和cookies常用来会话保持。1. 何为一次会话,会话从什么时候开始,从什么时候结束?一次会话是指: 好比打电话,当A打给B,电话接通了 会话开始,持断会话结束。 浏览器访问服务器,就如同打电话,浏览器A给服务器发送请求,访问web程序,该次会话就开始,其中不管浏览器发送了多少请求
前言
现在大部分项目都采用的前后端分离,比哪后台用spring boot ,前端用vue等。
一、会话机制
session和cookies常用来会话保持。
1. 何为一次会话,会话从什么时候开始,从什么时候结束?
一次会话是指: 好比打电话,当A打给B,电话接通了 会话开始,持断会话结束。 浏览器访问服务器,就如同打电话,浏览器A给服务器发送请求,访问web程序,该次会话就开始,其中不管浏览器发送了多少请求 ,都为一次会话,直到浏览器关闭,本次会话结束。
2.cookies如何保持会话,它的工作流程?
工作流程:
- servlet创建cookie,保存少量数据,发送浏览器。
- 浏览器获得服务器发送的cookie数据,将自动的保存到浏览器端。
- 下次访问时,浏览器将自动携带cookie数据发送给服务器。
3、session原理分析:
工作流程:
1、首先浏览器请求服务器访问web站点时,程序需要为客户端的请求创建一个session的时候,服务器首先会检查这个客户端请求是否已经包含了一个session标识、称为SESSIONID
2、如果已经包含了一个sessionid则说明以前已经为此客户端创建过session,服务器就按照sessionid把这个session检索出来使用
3、如果客户端请求不包含session id,则服务器为此客户端创建一个session并且生成一个与此session相关联的session id,sessionid 的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串
4、这个sessionid将在本次响应中返回到客户端保存,保存这个sessionid的方式就可以是cookie,这样在交互的过程中,浏览器可以自动的按照规则把这个标识发回给服务器,服务器根据这个sessionid就可以找得到对应的session,又回到了这段文字的开始
实例记录sessionid变化(前后端不分离网站,同一个域名不存在跨域问题)
1、第一次访问 http://127.0.0.1:8085/login 登录页面
2、后台获取sessionid,信息
sessionId:8038E64DE4036536341C7EB784AC1AA7,getLastAccessedTime:2020-06-09,getMaxInactiveInterval:1800
3、刷新一下 http://127.0.0.1:8085/login 这个接口
4、后台打印sessionid信息;
sessionId:8038E64DE4036536341C7EB784AC1AA7,getLastAccessedTime:2020-06-09,getMaxInactiveInterval:1800
5、登录后sessionid也是同一个
6、后台打印也是同一个
sessionId:8038E64DE4036536341C7EB784AC1AA7,getLastAccessedTime:2020-06-09,getMaxInactiveInterval:1800
4、session的生命周期
常常听到这样一种误解“只要关闭浏览器,session就消失了”。其实可以想象一下会员卡的例子,除非顾客主动对店家提出销卡,否则店家绝对不会轻易删除顾客的资料。对session来说也是一样的,除非程序通知服务器删除一个session,否则服务器会一直保留。
所以浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分session机制都使用会话cookie来保存session id,而关闭浏览器后这个session id就消失了,再次连接服务器时也就无法找到原来的session
恰恰是由于关闭浏览器不会导致session被删除,迫使服务器为seesion设置了一个失效时间,一般是30分钟,当距离客户端上一次使用session的时间超过这个失效时间时,服务器就可以认为客户端已经停止了活动,才会把session删除以节省存储空间
5、控制session有效时间
- session.invalidate()将session对象销毁
- setMaxInactiveInterval(int interval) 设置有效时间,单位秒
- 在web.xml中配置session的有效时间
<session-config>
<session-timeout>30</session-timeout> 单位:分钟
<session-config>
6、 session的生命周期就是
创建:第一次调用getSession()
销毁:1、超时,默认30分钟
2、执行api:session.invalidate()将session对象销毁、setMaxInactiveInterval(int interval) 设置有效时间,单位秒
3、服务器非正常关闭
自杀,直接将JVM马上关闭
如果正常关闭,session就会被持久化(写入到文件中,因为session默认的超时时间为30分钟,正常关闭后,就会将session持久化,等30分钟后,就会被删除)
位置: D:\java\tomcat\apache-tomcat-7.0.53\work\Catalina\localhost\test01\SESSIONS.ser
7、session id的URL重写
当浏览器将cookie禁用,基于cookie的session将不能正常工作,每次使用request.getSession() 都将创建一个新的session。达不到session共享数据的目的,但是我们知道原理,只需要将session id 传递给服务器session就可以正常工作的。
解决:通过URL将session id 传递给服务器:URL重写
- 手动方式: url;jsessionid=…
- api方式:
encodeURL(java.lang.String url) 进行所有URL重写
encodeRedirectURL(java.lang.String url) 进行重定向 URL重写
如果浏览器禁用cooke,api将自动追加session id ,如果没有禁用,api将不进行任何修改。
8、小结
8.1、cookie工作原理,
可以看上面讲解cookie的那张图,cookie是由服务器端创建发送回浏览器端的,并且每次请求服务器都会将cookie带过去,以便服务器知道该用户是哪一个。其cookie中是使用键值对来存储信息的,并且一个cookie只能存储一个键值对。所以在获取cookie时,是会获取到所有的cookie,然后从其中遍历。
8.2、session的工作原理
session的工作原理就是依靠cookie来做支撑,第一次使用request.getSession()时session被创建,并且会为该session创建一个独一无二的sessionid存放到cookie中,然后发送会浏览器端,浏览器端每次请求时,都会带着这个sessionid,服务器就会认识该sessionid,知道了sessionid就找得到哪个session。以此来达到共享数据的目的。 这里需要注意的是,session不会随着浏览器的关闭而死亡,而是等待超时时间。
8.3 session与cookies的联系与区别
cookie机制采用的是在客户端保持状态的方案
session机制采用的是在服务器端保持状态的方案,同进session机制可能需要借助于cookie机制来达到保存标识的目的,session在保存一个sesionid在cookie中。
以上都是传统的项目,比如前后端不分离,前端和后端在同一个域名下的情况
二、cookies的同源策略,导致cookes跨域写入失败的原因
1.协议相同
2.域名相同
3.端口相同
cookes跨域写入失败
当后端项目向浏览器写入cookies时,后端项目协议、域名、端口必须相同时才能写到浏览器
比如我访问一个地址为 http://test.clock.bone:8080
的页面地址,这个页面地址 请求一个后端服务如:http://test.clock.bone:8080/getinfo
,这个接口向浏览器写入了cookie 。 只有当浏览器地址和协议、域名、端口和请求的后端服务的协议、域名、端口一致时,这个cookie才能写成功,即使其它都 一样 ,但端口不一样也不会成功。
所以如果 浏览器访问的是 http://test.clock.bone:8080
,这个页面请求了后端服务http://test.clock.bone:8081/getinfo
写入了cookies也不会成功的原因。
session跨域每次获取sessionid不一样
我们知道session也依赖于cookie,当服务端创建了sessionid 要写入浏览器cookies时,如果不同源,那么sessionid会写入失败,下次请求时 浏览器无法携带session,服务端没有获取到sessionid ,于是又会重新创建一个sessionId,这就是为什么跨域请求 每次得到的sessionid不一致的原因。
三、多服务器共享session
再回顾一下 ,服务端创建session的过程:
1、浏览器请求服务器
2、服务端getsession,检查浏览器是否携带sessionid,
如果有sessionid (我们知道这些属性是存储在每个服务端的文件中的) ,证明用户已经访问过
如果没有sessionid,那么会创建一个新的,证明浏览器是第一次访问
3、我们通常通过sessionid来保存用户登录信息,根据sessionid 能取到用户信息 那么登录了,如果没有取到就没有登录
这时一个网站部署了多台服务器,多台服务配置映射同一个域名,
浏览器随机访问服务A,是第一次访问,没有携带了sessionId, 于是服务器创建了一个sessionid,根据sessionid 获取用户信息,发现没有取到 于是要求用户登录,
用户登录后 通过getsession.setAttribute(“user”,userinfo)将用户信息写到服务器session。
用户 继续访问网站,此时 随机跳转到了服务B,
此时浏览器有sessionid,服务B不会再新创建sessionid,于是通过getsession.getattrite(“user”)查找用户信息,没有找到,因为此时session是存储在服务器A上的,在服务器B上找 肯定找不到。于是认为用户没有登录,又要求用户去登录。但我分明已经登录过了。于是就出现了session不共享的问题。
解决session的共享的方案通常是把这个sessionid存储到第三方存储系统比如redis。
可以引用spring-session-redis,这个依赖。在创建了sessionid后,会把session存到redis中。当我们getsessionId,框架自动会先去redis中查找sessionid,这样就实现了多服务session共享了。
当然你可以简单配置一个策略:就是相同的ip 一直访问同一个后端服务器,这个session不用存储在第三方redis中 ,也能保证session不丢失。
将session存储到redis
四、如何解决跨域问题
1、为什么会有跨域
跨域实际上是浏览器同源策略的自我保护。比如一个域名 Ahttp://clock.bone:8080 下部署的Js ,用ajax去请求另一个后端服务 B :http://clock.bone.9090/userInfo 的服务,浏览器此时就会报错:
但实际上 这个后端服务请求到了,并返回了数据。但浏览器 发现这个请求的这个服务端口和 当前所在服务的端口不一致,就会认为不安全,就报错了。(因为后端服务 可能不允许 其它网站的服务 请求自己的服务。)
跨域: 域名、端口 有一个不同 都认为是跨域。
如果让服务B允许,需要在响应头添加 Access-Control-Allow-Origin
1.1、通过nginx添加
后端spring boot
前端vue
nginx部署
server
{
listen 3002;
server_name localhost;
location /ok {
proxy_pass http://localhost:3000;
# 指定允许跨域的方法,*代表所有
add_header Access-Control-Allow-Methods *;
# 预检命令的缓存,如果不缓存每次会发送两次请求
add_header Access-Control-Max-Age 3600;
# 带cookie请求需要加上这个字段,并设置为true
add_header Access-Control-Allow-Credentials true;
# 表示允许这个域跨域调用(客户端发送请求的域名和端口)
# $http_origin动态获取请求客户端请求的域 不用*的原因是带cookie的请求不支持*号
add_header Access-Control-Allow-Origin $http_origin;
# 表示请求头的字段 动态获取
add_header Access-Control-Allow-Headers
$http_access_control_request_headers;
# OPTIONS预检命令,预检命令通过时才发送请求
# 检查请求的类型是不是预检命令
if ($request_method = OPTIONS){
return 200;
}
}
}
现在所有前端项目访问后端服务不用指向ip,直接用
1.2、后端允许跨域访问
后端:
@Override
public void addCorsMappings(CorsRegistry registry) {
//只有这些路径下的 资源响应头中才会加上`Access-Control-Allow-Origin`
registry.addMapping("/api/**")
.allowedOrigins("http://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true);
}
前端:
设置{‘withCredentials’:true}
五、题外
这次cookes问题主要是由于前后端分离后图片验证码 校验问题来的
5.1、流程
流程是这样的:要做一个用户登录的接口。在登录页面,前端先请求图片验证码,然后输入用户名密码和验证码之后,请求登录接口。
这里存在两个接口,验证码接口和登录接口。在验证码接口中我用session保存验证码,在登录接口中我从session取出验证码进行校验。
或者用cookies保存一个verifyid, 根据这个verifyid 去redis中获取验证码
5.2、代码session实现
@RequestMapping("/getverifyCode")
public void getverifyCode(HttpServletRequest request,
HttpServletResponse response) throws IOException {
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control",
"no-store, no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");
response.setContentType("image/jpeg");
String capText = captchaProducer.createText();
request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY,capText);
logger.info("code is "+capText+" session id is "+request.getSession().getId());
BufferedImage bi = captchaProducer.createImage(capText);
ServletOutputStream out = response.getOutputStream();
ImageIO.write(bi, "jpg", out);
try {
out.flush();
} finally {
out.close();
}
}
@RequestMapping(value = "/login",method = RequestMethod.POST)
public Response login(HttpServletRequest request){
String userName = request.getParameter("userName");
String password = request.getParameter("password");
String verifyCode= request.getParameter("verifyCode");
String sessionCode = (String) request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
logger.info("input code is "+verifyCode+" session id is "+request.getSession().getId());
if(StringUtils.isEmpty(verifyCode)){
Response.setMsg("验证码不能为空");
return Response;
}
if(!verifyCode.equals(sessionCode)){
Response.setMsg("验证码不能为空");
return Response;
}
try {
User user = userService.checkLogin(userName, password);
if (user == null) {
Response.setMsg("用户不存在");
return Response;
}
Response.setMsg("登录成功");
Response.setData(user);
request.getSession().setAttribute("user",user);
}catch (GeneralException g){
g.printStackTrace();
}catch (Exception e){
e.printStackTrace();
}
return Response;
}
5.3、通过cookie,redis实现
@PostMapping("getVerifyCode")
@ResponseBody
public ResponseEntity getverifyCode(HttpServletResponse response,HttpServletRequest request) throws IOException{
String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
String verifyId = UUID.randomUUID().toString();
CookieUtil.addCookie(response, RedisKeyEnum.COOKIE_KEY_VERIFY, verifyId);
//存到redis中
cacheService.set(verifyId, verifyCode, 120);
String base64 = VerifyCodeUtils.outputImageAsBase64(100, 38, verifyCode);
return ResponseEntity.ok(base64);
}
@PostMapping("login")
@ResponseBody
@ApiOperation(notes = "登录", value = "登录")
public ResponseEntity login(HttpServletRequest request, HttpServletResponse response
, @Validated String userName,String pwd ,String verifyCode,
,@CookieValue(value = RedisKeyEnum.COOKIE_KEY_VERIFY) String verifyId
) throws IOException {
//如果从cookeis获取不到verifyId,那么会报错,根据verifyid 从redis中获取生成的verifycode
String ckCode = cacheService.getAndDel(verifyId), StringUtils.EMPTY);
if (StringUtils.isEmpty(dto.getVerify()) || StringUtils.isEmpty(ckCode) || !ckCode.equalsIgnoreCase(dto.getVerify())) {
return ResponseEntity.errorMsg("验证码输入错误或已失效").build();
}
return Respoinse.ok();
这种方法在跨域的情况下都无法实现,因为sessionId也用到了cookies。
需要解决跨域的问题
参考:
https://www.cnblogs.com/whgk/p/6422391.html
https://blog.csdn.net/zhaoenweiex/article/details/77814918
更多推荐
所有评论(0)