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

文章目录

  • 前言
  • 一、会话机制
    • 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如何保持会话,它的工作流程?

工作流程:

  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,信息

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时,后端项目协议、域名、端口必须相同时才能写到浏览器
比如我访问一个地址为 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

在这里插入图片描述

四、如何解决跨域问题

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