后端开发中关于Cookie和Session跨域的坑

2023年 7月 13日 67.5k 0

本文根据笔者开发过程(java)中遇到的问题整理而来,旨在减少大家查询无用资料的时间,对于一些原理解释可能不是很完美

首先,先让我们来看看Cookie的一些官方文档(rfc6265):

  • 翻译版本:github.com/renaesop/bl…
  • 规范文件原版:datatracker.ietf.org/doc/html/dr…
  • CSRF预防:cheatsheetseries.owasp.org/cheatsheets…
  • 相信你也看得一脸懵,接下来我说一下里面的关键词

  • 5.2.3. The Domain Attribute(作用域)

  • 5.2.5. The Secure Attribute(Secure属性)

  • 4.1.2.7. The SameSite Attribute(SameSite属性)

  • 操作复现:

    环境:Java 11, SpringBoot2.7.0(Tomcat), Edge浏览器, Js/axios(fetch函数)

    服务器端Controller层(API)代码:

    @PostMapping("/api/v1/login")
    public BaseResponse test(HttpSession session,@RequestBody  LoginRequest request){//BaseResponse 为自定义的成功响应
        //执行登录逻辑
        //...
        //假设现在登录成功了
        log.info("登录请求:{}",request);
        Object successUserDO=request;
        session.setAttribute("loginUser",successUserDO);
        return BaseResponse.success();
    }
    
    @GetMapping("/api/v1/detail")
    public BaseResponse getLogin(HttpSession session){
        //前端获取登录信息和验证登录是否失效
        return BaseResponse.success(session.getAttribute("loginUser"));
    }
    

    后端Config配置允许跨域:

    @Configuration
    public class MVCConfig implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            //配置允许跨域的路径
            registry.addMapping("/**")
                    //配置允许访问的跨域资源的请求域名
                    .allowCredentials(true)
                    .allowedOriginPatterns("*")
                    //配置允许访问该跨域资源服务器的请求方法
                    .allowedMethods("*")
                    //配置允许请求 头部head的访问
                    .allowedHeaders("*");
        }
    }
    

    前端调用代码:

    const BaseURL="http://127.0.0.1:8080"
    let data={
        loginRequest:{
            userName:"",
            password:""
        }
    }
    function userLogin(){//用户登录
            fetch(BaseURL+"/api/v1/login",{
                method:"POST",
                headers:{
                    "Content-Type":"application/json",
    
                },
                credentials: "include",//前端跨域
                mode:"cors",
                body:JSON.stringify(data.loginRequest)
            }).then(rep=>rep.json()).then((rep)=>{
                console.log(rep)
            })
    }
    
    function getUserDetail(){//获取用户详情
        fetch(BaseURL+"/api/v1/detail",{
            method:"GET",
            headers:{
                "Content-Type":"application/json",
                
            }
            credentials: "include",//前端跨域
            mode:"cors",
        }).then(rep=>rep.json()).then((rep)=>{
            console.log(rep)
        })
    }
    
    

    写完测试,一看:登录成功,但是请求/api/v1/detail的时候,拿到的却是空对象

    登录:

    图片.png

    图片.png

    获取用户信息:

    图片.png

    这时候大概率就是前后端打架的时候了:

    "我登录成功了为什么也还是拿不到"

    "肯定是前端的问题"

    ......

    我的思路和目前用的两套解决方案

    总结原因:

    这锅,原因有点复杂,但总的来说,是前后端分离+浏览器更新的锅

    分析过程:

    前置知识->后端Session:

    简要来讲,java后端中的Session本质上是一个K-V键值对,也就是一个Map,key是string类型的,value是也是一
    个Map,也就等价于Java中的Session看做对象的话,其实它可以写成
    Map,那么,这个session的key(第一个String)是放在浏览器哪里的呢?
    结合标题我们可以知道(bushi),它是放在浏览器的Cookie中的
    这么做的原因如下:

  • Cookie对于前端开发来说其实是无感的,每次请求由浏览器自带就行了
  • 将Session-Key放Cookie中也便于用户管理
  • 流程分析以及排错:

    当一个http请求后端的时候

    服务器的解析流程:

  • 查看是否Cookie中携带JESSION字段,如果有,则去Map中找,如果有且不为null,则将Session指向那个value
  • 如果没有JESSION字段,就自动生成一个JESSION,然后在Response的时候添加一条
    key:Set-Cookie
    value: JESSSION=xxxx
  • 如果有JESSION字段,但是Map中查询到的内容是null,则执行第二步
  • 图片如下:

    图片.png

    然后就给Controller使用实例化好了的Session对象

    问题查找:

    我们点击Login请求的Cookie
    图片.png

    再点击Detail请求的Cookie
    图片.png

    发现了吗?每次请求都响应了一个Session,并且没有请求cookie,
    也就是说每一次请求对于后端来说都是新的请求!

    解释:

    因为浏览器有个默认行为:检查Set-Cookie这个响应头是否为可接受的,其中和本问题相关的最重要的的就是一个Set-Cookie的属性:SameSite,关于SameSite,可以看看MDN上关于这部分的解释:developer.mozilla.org/zh-CN/docs/…
    本文不过多说明
    只需要知道,在浏览器发展中,已经将这一默认属性替换为SameSite=Lax了,而要使cookie能跨域发送的需要的是SameSite=None

    解决方案:

  • 替换掉Session在请求中存放的位置,由Cookie转为header(token解决方案)
    优点:不用去考虑关于Cookie转session的事了
    缺点:要后端自己去写查找转化逻辑代码(还有考虑过期失效的问题,这部分可以用JWT或者是Redis解决),前端也需要每次手动携带(也可以说要手动封装请求)token
  • 2.前后端分离开发,但上线时将前端打包放进后端:
    优点:代码方面可以完全不用做额外的操作
    缺点:开发的时候需要前端配置代理服务器,上线后可能服务器压力很大(静态资源占用带宽较高)

  • 后端配置过滤器,将Session手动设置
    Tomcat
  • response.addHeader("Set-Cookie","JSESSIONID="+request.getSession().getId()+";SameSite=None;Secure");
    

    Redis作为Session:

    String id = session.getId();
    String encode = Base64.encode(id);//Base64编码
    response.setHeader("Set-Cookie", "SESSION=" + encode + ";Path=/;SameSite=None;Secure");
    

    优点:可前后端分离式开发,相对于第一种来讲前端
    缺点:后端需要有https能访问的域名

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论