文章目录
- 前言
- 一、会话机制
- 1. 何为一次会话,会话从什么时候开始,从什么时候结束?
- 2.cookies如何保持会话,它的工作流程?
- 3、session原理分析:
- 实例记录sessionid变化(前后端不分离网站,同一个域名不存在跨域问题)
- 4、session的生命周期
- 5、控制session有效时间
- 6、 session的生命周期就是
- 7、session id的URL重写
- 解决:通过URL将session id 传递给服务器:URL重写
- 8、小结
- 8.1、cookie工作原理,
- 8.2、session的工作原理
- 8.3 session与cookies的联系与区别
- 二、cookies的同源策略,导致cookes跨域写入失败的原因
- cookes跨域写入失败
- session跨域每次获取sessionid不一样
- 三、多服务器共享session
- 四、如何解决跨域问题
- 3.1、nginx
- 3.2、前后端允许跨域访问,并携带cookie
- 后端:
- 前端:
- 五、题外
- 5.1、流程
- 5.2、代码session实现
- 5.3、通过cookie,redis实现
前言
现在大部分项目都采用的前后端分离,比哪后台用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,信息
1 | sessionId:8038E64DE4036536341C7EB784AC1AA7,getLastAccessedTime:2020-06-09,getMaxInactiveInterval:1800 |
3、刷新一下 http://127.0.0.1:8085/login 这个接口
4、后台打印sessionid信息;
1 | sessionId:8038E64DE4036536341C7EB784AC1AA7,getLastAccessedTime:2020-06-09,getMaxInactiveInterval:1800 |
5、登录后sessionid也是同一个
6、后台打印也是同一个
1 | 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的有效时间
1 2 3 | <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时,后端项目协议、域名、端口必须相同时才能写到浏览器
比如我访问一个地址为
所以如果 浏览器访问的是
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
四、如何解决跨域问题
3.1、nginx
后端spring boot
前端vue
nginx部署
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | upstream clock-server { //后端服务 server 10.2.22.45:8098; } server { listen 80; server_name clock.bone.com; location =/ { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; //允许携带cookie proxy_set_header Cookie $http_cookie; //vue 打包后的路径 root /data/vue/; } location ~ .*\.(css|js|ico|gif|jpg|jpeg|png|bmp|swf|ico|html|htm)$ { root /data/vue/; } //以server开头的服务都会转发到后端服务 location ~^/server/ { //携带cookes proxy_set_header Cookie $http_cookie; //转发到后端服务上 proxy_pass http://clock-server; } } |
现在所有前端项目访问后端服务不用指向ip,直接用 http://clock.bone.com/server/getinfo 就可以了。 写cookies正常。
3.2、前后端允许跨域访问,并携带cookie
后端:
官网
1 2 3 4 5 6 7 8 9 | @Override public void addCorsMappings(CorsRegistry registry) { 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实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | @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实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | @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