前后端分离后产生的跨域问题sessionid丢失,cookies无法写入等

前言

现在大部分项目都采用的前后端分离,比哪后台用spring boot ,前端用vue等。

一、会话机制

session和cookies常用来会话保持。

1. 何为一次会话,会话从什么时候开始,从什么时候结束?

一次会话是指: 好比打电话,当A打给B,电话接通了 会话开始,持断会话结束。 浏览器访问服务器,就如同打电话,浏览器A给服务器发送请求,访问web程序,该次会话就开始,其中不管浏览器发送了多少请求 ,都为一次会话,直到浏览器关闭,本次会话结束。

2.cookies如何保持会话,它的工作流程?

工作流程:

  1. servlet创建cookie,保存少量数据,发送浏览器。
  2. 浏览器获得服务器发送的cookie数据,将自动的保存到浏览器端。
  3. 下次访问时,浏览器将自动携带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

参考 springmvc中解决跨域问题

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

  • 17
    点赞
  • 85
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值