跨Docker容器请求时Fetch未携带JSESSIONID Cookie问题解决
我有一个运行在独立Docker容器中的Spring Boot后端,使用JSESSIONID Cookie进行身份认证,该Cookie可在浏览器中查看。但运行在不同端口的Svelte.js前端容器通过fetch()请求后端时,似乎并未携带浏览器中的Cookie,尽管已设置{credentials:'include'}。
我的Svelte服务端代码(运行在Docker容器而非浏览器中):
/** @type {import('./$types').PageServerLoad} */ export async function load({ params }) { const response = await fetch(`${getBaseUrl()}/api/draftgroups`,{credentials:'include',method:'get'}); let txt = await response.text() if(response.headers.get("content-type") != "application/json") { // 判断用户是否登录:未登录会跳转至Google OAuth登录,返回HTML响应 console.log("NOT SIGNED IN") throw redirect('302',`http://localhost:6868/login`) } if (!response.ok) { throw new Error(`Error! status: ${response.status}`); } let responseJSON = await response.json() return responseJSON }
逻辑说明:通过检查响应内容类型判断用户是否登录,未登录时会跳转至Google OAuth登录并返回HTML响应。预期仅首次访问时触发该逻辑,但后续每次访问都会返回登录跳转的HTML,说明请求未携带已认证的Cookie。
疑问:这是否与两个Docker容器属于不同Cookie源有关?该如何修复?
我的docker-compose.yml配置:
# Based off of https://github.com/bezkoder/docker-compose-nodejs-mongodb/tree/master/bezkoder-app # https://www.bezkoder.com/docker-compose-nodejs-mongodb/ version: "3.8" services: mongodb: image: mongo:5.0.2 restart: unless-stopped ports: - 27017:27017 volumes: - db:/data/db spring-boot: image: bracket_backend build: context: ./backend dockerfile: Dev.Dockerfile depends_on: - mongodb ports: - 6868:8080 stdin_open: true tty: true volumes: - ./backend/src:/app/src frontend-svelte: image: bracket_frontend build: context: ./frontend-svelte dockerfile: Dev.Dockerfile ports: - 1234:8080 stdin_open: true tty: true volumes: - ./frontend-svelte/src:/app/src depends_on: - spring-boot volumes: db:
核心原因
问题完全由跨源Cookie规则和请求执行位置导致,两个关键点:
PageServerLoad是在前端Docker容器内执行的,不是用户浏览器。此时的fetch请求是从前端容器发起的,根本访问不到用户浏览器中存储的JSESSIONID Cookie——浏览器Cookie只属于用户本地环境,和前端容器无关。- 即使是浏览器端的
fetch,当前localhost:1234(前端)和localhost:6868(后端)属于不同端口,属于跨源请求,Cookie跨源传输需要满足严格的规则。
分步修复
1. 将请求移至浏览器端执行
把数据获取逻辑从服务端的PageServerLoad转移到浏览器端(比如用onMount钩子),这样fetch是在用户浏览器中发起的,才能读取到浏览器存储的JSESSIONID Cookie。
示例代码(浏览器端):
<script> import { onMount } from 'svelte'; import { getBaseUrl } from './utils'; let draftGroups = []; onMount(async () => { try { const response = await fetch(`${getBaseUrl()}/api/draftgroups`, { credentials: 'include', method: 'GET' }); if (response.headers.get('content-type') !== 'application/json') { window.location.href = 'http://localhost:6868/login'; return; } if (!response.ok) { throw new Error(`请求失败:${response.status}`); } draftGroups = await response.json(); } catch (err) { console.error(err); } }); </script> <!-- 页面渲染内容 --> {#if draftGroups.length} {#each draftGroups as group} <div>{group.name}</div> {/each} {:else} <p>加载中...</p> {/if}
2. 配置Spring Boot后端的CORS支持
在Spring Boot中配置CORS,允许前端源的请求携带Cookie:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.List; @Configuration public class CorsConfig { @Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); // 允许前端的源(开发环境为localhost:1234) config.setAllowedOrigins(List.of("http://localhost:1234")); // 允许的请求方法 config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 允许的请求头 config.setAllowedHeaders(List.of("*")); // 允许携带凭证(Cookie) config.setAllowCredentials(true); // 暴露响应头中的Cookie相关信息,让浏览器可以读取 config.setExposedHeaders(List.of("Set-Cookie")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // 对所有路径应用CORS配置 source.registerCorsConfiguration("/**", config); return source; } }
3. 优化JSESSIONID Cookie的设置(推荐)
在Spring Boot的配置文件application.properties中,调整Cookie的属性,确保跨源场景下能正常传递:
# 设置Cookie的SameSite属性,开发环境用Lax即可 server.servlet.session.cookie.same-site=Lax # 指定Cookie的作用域为localhost(本地开发) server.servlet.session.cookie.domain=localhost # 确保Cookie在HTTPS环境下也能传递(生产环境启用) # server.servlet.session.cookie.secure=true
4. (可选)用反向代理统一域名
如果不想处理跨域问题,可以新增一个Nginx服务作为反向代理,将前端和后端映射到同一个域名的不同路径下,比如:
http://localhost/api/转发至Spring Boot后端(http://spring-boot:8080/)http://localhost/转发至Svelte前端(http://frontend-svelte:8080/)
这样前端和后端同域,Cookie可以自动携带,无需处理跨源问题。对应的docker-compose.yml新增Nginx服务:
nginx: image: nginx:alpine ports: - 80:80 volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf depends_on: - spring-boot - frontend-svelte
nginx.conf配置示例:
server { listen 80; server_name localhost; # 后端API转发 location /api/ { proxy_pass http://spring-boot:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 前端页面转发 location / { proxy_pass http://frontend-svelte:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
内容的提问来源于stack exchange,提问作者Ethan Michel

