Bootstrap

【java】跨域cookie失效问题

1. 现象描述

1.1 问题背景

在现代 Web 应用中,前后端分离架构已经成为一种常见的开发模式。前端通常使用 Vue.js 等框架,而后端则使用 Java 等语言构建 API 服务。在这种架构下,前端和后端可能会部署在不同的域名或端口上,这就引发了跨域请求的问题。跨域请求涉及到浏览器的同源策略,尤其是当涉及到 Cookie 时,问题会变得更加复杂。

1.2 具体现象

当前端应用尝试向后端 API 发送请求并期望后端返回的 Cookie 能够在前端被正常使用时,可能会遇到以下问题:

  • 前端发送请求后,后端正常处理并返回响应,其中包含 Set-Cookie 头部。
  • 浏览器接收到响应,但由于跨域问题,Set-Cookie 头部被忽略,导致 Cookie 未能正确设置。
  • 后续请求由于缺少必要的 Cookie,导致用户会话无法维持或认证失败。
1.3 常见提示信息

在这种情况下,前端开发者可能会在控制台或网络请求面板中看到以下提示信息:

  • HTTP 状态码 400:请求被拒绝,通常是因为缺少必要的认证信息(如 Cookie)。
  • CORS 错误:浏览器控制台中可能会出现跨域资源共享(CORS)相关的错误信息,例如:
    Access to XMLHttpRequest at 'https://api.example.com/resource' from origin 'https://frontend.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
    
  • Cookie 丢失:在网络请求面板中查看响应头部时,可能会发现 Set-Cookie 头部存在,但浏览器并未将其存储。

这些现象表明,尽管后端服务正常响应,但由于跨域问题,前端未能正确接收到或存储 Cookie,导致后续请求失败。

2. 跨域 Cookie 的原理

2.1 什么是 Cookie

Cookie 是一种由服务器发送并存储在客户端的小型数据文件,用于保存用户的状态信息。它们通常用于以下几种用途:

  • 会话管理:如用户登录状态、购物车内容等。
  • 个性化设置:如用户偏好设置、主题选择等。
  • 跟踪:用于分析用户行为和广告投放。

Cookie 由键值对组成,通常包含以下属性:

  • name:Cookie 的名称。
  • value:Cookie 的值。
  • domain:Cookie 所属的域。
  • path:Cookie 的有效路径。
  • expires/max-age:Cookie 的有效期。
  • secure:指示 Cookie 只能通过 HTTPS 传输。
  • HttpOnly:指示 Cookie 不能通过 JavaScript 访问。
  • SameSite:限制跨站请求时 Cookie 的发送。
2.2 Cookie 的作用域

Cookie 的作用域定义了它们在何种情况下会被发送到服务器。主要包括以下几方面:

  • 域(Domain):Cookie 只会在其所属域及子域内发送。例如,设置为 example.com 的 Cookie 会在 sub.example.com 也有效。
  • 路径(Path):Cookie 只会在指定路径及其子路径内发送。例如,路径为 /app 的 Cookie 只会在 /app/app/* 下有效。
  • 安全性(Secure):标记为 Secure 的 Cookie 只会在 HTTPS 连接中发送。
  • HttpOnly:标记为 HttpOnly 的 Cookie 不能通过 JavaScript 访问,增加了安全性。
2.3 SameSite 属性

SameSite 属性用于防止跨站请求伪造(CSRF)攻击,控制 Cookie 在跨站请求中的发送行为。该属性有三个值:

  • Strict:完全禁止跨站请求发送 Cookie。只有在与 Cookie 所属站点完全一致的请求中才会发送 Cookie。
  • Lax:在跨站请求中,只有导航到目标站点的 GET 请求会发送 Cookie。这是一个平衡安全性和可用性的选项。
  • None:允许跨站请求发送 Cookie,但必须同时设置 Secure 属性。这种情况下,Cookie 可以在所有跨站请求中发送。

在实际应用中,如果 SameSite 属性设置不当,可能会导致跨域请求中的 Cookie 失效,从而影响用户的会话管理和状态保持。

3. 解决方案

3.1 Java 后端解决方案
3.1.1 配置 SameSite 属性

为了确保 Cookie 能在跨域请求中被正确发送和接收,可以配置 Cookie 的 SameSite 属性。SameSite 属性有三个值:

  • Strict:Cookie 仅在同一站点请求中发送。
  • Lax:Cookie 在同一站点请求和部分跨站请求(如 GET 请求)中发送。
  • None:Cookie 在所有跨站请求中发送,但必须同时设置 Secure 属性。

示例代码:

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;

public void setCookie(HttpServletResponse response) {
    Cookie cookie = new Cookie("key", "value");
    cookie.setPath("/");
    cookie.setHttpOnly(true);
    cookie.setSecure(true);
    cookie.setMaxAge(7 * 24 * 60 * 60); // 1 week
    cookie.setSameSite("None"); // SameSite=None
    response.addCookie(cookie);
}
3.1.2 使用 Spring Boot 设置 Cookie 属性

在 Spring Boot 中,可以通过配置类来设置 Cookie 属性。

示例代码:

import org.springframework.boot.web.server.Cookie.SameSite;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CookieConfig {

    @Bean
    public ServletWebServerFactory servletWebServerFactory() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.addContextCustomizers(context -> {
            context.setSessionCookieConfig(sessionCookieConfig -> {
                sessionCookieConfig.setSameSite(SameSite.NONE.attributeValue());
                sessionCookieConfig.setSecure(true);
            });
        });
        return factory;
    }
}
3.1.3 配置 CORS 解决跨域问题

在 Spring Boot 中,可以通过配置 CORS 来允许跨域请求。

示例代码:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://your-frontend-domain.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowCredentials(true)
                .allowedHeaders("*")
                .maxAge(3600);
    }
}
3.2 前端解决方案
3.2.1 Vue 配置跨域请求

在 Vue 项目中,可以通过配置 vue.config.js 文件来设置代理,以解决开发环境中的跨域问题。

示例代码:

module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://your-backend-domain.com',
        changeOrigin: true,
        secure: false,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }
};
3.2.2 使用 Axios 发送跨域请求

在 Vue 项目中,通常使用 Axios 来发送 HTTP 请求。可以全局配置 Axios 以支持跨域请求。

示例代码:

import axios from 'axios';

axios.defaults.baseURL = 'http://your-backend-domain.com';
axios.defaults.withCredentials = true; // 允许携带 Cookie

export default axios;
3.2.3 设置 withCredentials 属性

在发送具体请求时,也可以单独设置 withCredentials 属性。

示例代码:

axios.get('/api/some-endpoint', {
  withCredentials: true
}).then(response => {
  console.log(response.data);
});
3.3 Nginx 解决方案
3.3.1 配置 Nginx 处理跨域

在 Nginx 配置文件中,可以通过设置响应头来允许跨域请求。

示例代码:

server {
    listen 80;
    server_name your-backend-domain.com;

    location / {
        add_header 'Access-Control-Allow-Origin' 'http://your-frontend-domain.com';
        add_header 'Access-Control-Allow-Credentials' 'true';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';

        if ($request_method = 'OPTIONS') {
            return 204;
        }

        proxy_pass http://backend_server;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
3.3.2 设置 Cookie 属性

在 Nginx 中,可以通过 proxy_cookie_path 指令来设置 Cookie 的 SameSite 属性。

示例代码:

server {
    listen 80;
    server_name your-backend-domain.com;

    location / {
        proxy_pass http://backend_server;
        proxy_cookie_path / "/; SameSite=None; Secure";
    }
}
3.4 使用 window.localStorage 存储数据

window.localStorage 是一种在浏览器中存储数据的机制,它具有以下优点:

  • 持久性:数据存储在浏览器中,关闭浏览器后仍然存在,直到被显式删除。
  • 容量大:相比于 Cookie 的 4KB 限制,localStorage 的存储容量通常为 5MB 或更多。
  • 简单易用:提供了简单的 API 接口,可以方便地存储和读取数据。
3.4.1 代码示例:存储数据

在需要存储数据的页面中,我们可以使用 window.localStorage.setItem 方法将数据存储到 localStorage 中。假设我们有一个 JSON 对象 jsonData,需要将其中的 redirectData 存储起来。

// 假设 jsonData 是我们需要存储的数据对象
const jsonData = {
    redirectData: "exampleData"
};

// 将数据存储到 localStorage 中
window.localStorage.setItem('redirectData', JSON.stringify(jsonData.redirectData));

// 验证数据是否存储成功
console.log('Data stored in localStorage:', window.localStorage.getItem('redirectData'));
3.4.2 代码示例:获取数据

在目标页面中,我们可以使用 window.localStorage.getItem 方法从 localStorage 中读取数据。

// 从 localStorage 中获取数据
const storedData = window.localStorage.getItem('redirectData');

// 检查数据是否存在
if (storedData) {
    const redirectData = JSON.parse(storedData);
    console.log('Data retrieved from localStorage:', redirectData);
} else {
    console.log('No data found in localStorage.');
}
3.4.3 解决方案的工作原理

使用 window.localStorage 解决跨域 Cookie 失效问题的工作原理如下:

  1. 数据存储

    • 在需要传递数据的页面中,使用 window.localStorage.setItem 方法将数据存储到 localStorage 中。localStorage 是基于域名(origin)的存储机制,因此存储的数据在同一域名下的所有页面中都是可访问的。
  2. 数据获取

    • 在目标页面中,使用 window.localStorage.getItem 方法从 localStorage 中读取数据。由于 localStorage 是持久化存储,数据在浏览器关闭后仍然存在,直到被显式删除。
  3. 数据传递

    • 通过在同一域名下的不同页面之间共享 localStorage 数据,我们可以实现跨页面的数据传递,从而解决跨域 Cookie 失效的问题。
3.4.4 使用场景与限制
使用场景
  1. 单页应用(SPA)

    • 在单页应用中,页面切换通常不会引起页面重新加载,因此 localStorage 可以用来在不同视图之间共享数据。
  2. 跨子页面的数据传递

    • 在同一域名下的不同子页面之间传递数据,例如从一个登录页面传递用户信息到主页面。
  3. 临时存储

    • 用于临时存储用户操作数据,例如表单数据、用户偏好设置等。
限制
  1. 域名限制

    • localStorage 只能在同一域名(origin)下的页面之间共享数据,跨域名(不同 origin)的页面无法直接共享 localStorage 数据。
  2. 数据安全性

    • localStorage 中存储的数据是明文的,任何有访问权限的脚本都可以读取。因此,不应存储敏感信息,如用户密码、信用卡信息等。
  3. 存储容量限制

    • 各浏览器对 localStorage 的容量限制通常为 5MB,超过这个限制的数据将无法存储。
  4. 浏览器兼容性

    • 尽管现代浏览器普遍支持 localStorage,但仍需考虑旧版浏览器的兼容性问题。

4. 实践案例

4.1 Java 后端代码示例

在 Java 后端中,我们可以使用 Spring Boot 来设置 Cookie 属性和处理跨域请求。以下是一个简单的示例:

设置 SameSite 属性和跨域配置

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.CrossOrigin;

@RestController
@RequestMapping("/api")
public class CookieController {

    @PostMapping("/set-cookie")
    @CrossOrigin(origins = "https://frontend.example.com", allowCredentials = "true")
    public String setCookie(HttpServletResponse response) {
        Cookie cookie = new Cookie("key", "value");
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setSecure(true);
        cookie.setMaxAge(3600); // 1 hour
        cookie.setDomain("example.com");
        cookie.setComment("SameSite=None; Secure"); // For SameSite=None
        response.addCookie(cookie);
        return "Cookie set";
    }
}

配置 CORS

在 Spring Boot 应用中,可以通过配置类来全局配置 CORS:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**")
                        .allowedOrigins("https://frontend.example.com")
                        .allowedMethods("GET", "POST", "PUT", "DELETE")
                        .allowCredentials(true);
            }
        };
    }
}
4.2 Vue 前端代码示例

在 Vue 项目中,我们通常使用 Axios 进行 HTTP 请求。以下是一个示例,展示如何配置 Axios 以支持跨域请求并携带 Cookie:

安装 Axios

npm install axios

配置 Axios

在 Vue 项目的 main.js 文件中配置 Axios:

import Vue from 'vue';
import App from './App.vue';
import axios from 'axios';

axios.defaults.withCredentials = true;
axios.defaults.baseURL = 'https://api.example.com';

Vue.prototype.$axios = axios;

new Vue({
  render: h => h(App),
}).$mount('#app');

发送跨域请求

在 Vue 组件中使用 Axios 发送请求:

<template>
  <div>
    <button @click="setCookie">Set Cookie</button>
  </div>
</template>

<script>
export default {
  methods: {
    setCookie() {
      this.$axios.post('/api/set-cookie')
        .then(response => {
          console.log(response.data);
        })
        .catch(error => {
          console.error(error);
        });
    }
  }
}
</script>
4.3 综合示例:前后端联调

以下是一个综合示例,展示如何在前后端联调中处理跨域 Cookie 问题。

后端代码

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.CrossOrigin;

@RestController
@RequestMapping("/api")
public class CookieController {

    @PostMapping("/set-cookie")
    @CrossOrigin(origins = "https://frontend.example.com", allowCredentials = "true")
    public String setCookie(HttpServletResponse response) {
        Cookie cookie = new Cookie("key", "value");
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setSecure(true);
        cookie.setMaxAge(3600); // 1 hour
        cookie.setDomain("example.com");
        cookie.setComment("SameSite=None; Secure"); // For SameSite=None
        response.addCookie(cookie);
        return "Cookie set";
    }
}

前端代码

<template>
  <div>
    <button @click="setCookie">Set Cookie</button>
  </div>
</template>

<script>
export default {
  methods: {
    setCookie() {
      this.$axios.post('/api/set-cookie')
        .then(response => {
          console.log(response.data);
        })
        .catch(error => {
          console.error(error);
        });
    }
  }
}
</script>

<script>
import Vue from 'vue';
import App from './App.vue';
import axios from 'axios';

axios.defaults.withCredentials = true;
axios.defaults.baseURL = 'https://api.example.com';

Vue.prototype.$axios = axios;

new Vue({
  render: h => h(App),
}).$mount('#app');
</script>

通过上述配置,前端发送请求时会携带 Cookie,后端也会正确设置和返回 Cookie,从而实现跨域请求中的 Cookie 管理。

5. 常见问题与排查

5.1 Cookie 未正确设置

问题描述:Cookie 未被浏览器保存或发送。
排查步骤

  • 确认 Cookie 的 SameSite 属性设置为 None 并且 Secure 属性设置为 true
  • 检查 Cookie 的路径和域是否正确。
  • 确认服务器响应头中包含 Set-Cookie 字段。
5.2 浏览器限制

问题描述:某些浏览器可能对跨域 Cookie 有额外的限制。
排查步骤

  • 确认浏览器版本是否支持 SameSite=None
  • 检查浏览器的隐私设置,确保没有阻止第三方 Cookie。
  • 使用浏览器开发者工具查看网络请求和响应,确认 Cookie 是否被正确设置和发送。
5.3 服务器配置问题

问题描述:服务器配置错误导致跨域请求失败。
排查步骤

  • 确认服务器的 CORS 配置正确,允许所需的跨域请求。
  • 检查服务器日志,确认没有其他错误影响跨域请求。
  • 确认服务器响应头中包含正确的 CORS 头部信息。
;