Remix.run问题:Loader中Cookie已存在但返回Null
我有独立的Elixir/Phoenix后端,用户登录后会设置Cookie,随后重定向到Remix应用的/profile页面:
conn |> put_resp_cookie("x-auth-token", %{jobseeker_id: generated_user.id}, path: "/", max_age: one_week, http_only: true, secure: true, same_site: "strict", domain: "localhost", sign: true ) |> redirect(external: "http://localhost:3000/profile")
在Remix的/profile路由组件中,loader代码如下:
export const loader = async ({ request }: LoaderFunctionArgs) => { const cookieHeader = request.headers.get('Cookie'); console.log('cccccccccccccc', cookieHeader); // 首次加载打印 null const response = await fetch(`${process.env.BACKEND_URL!}/api/v1/me`, { method: 'GET', credentials: 'include', headers: { ...(cookieHeader ? { Cookie: cookieHeader } : {}), }, }); const { data } = (await response.json()) as { data: Jobseeker }; return json({ jobseeker: data, }); };
但Remix加载/profile页面时,request.headers.get("Cookie")返回空值,尽管Cookie确实存在。首次加载的request.headers如下:
{ accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9', 'cache-control': 'max-age=0', connection: 'keep-alive', host: 'localhost:3000', 'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"macOS"', 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'cross-site', 'sec-fetch-user': '?1', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' }
只有刷新页面后,request.headers.get("Cookie")才会返回有效值,此时的request.headers包含Cookie:
{ accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9', connection: 'keep-alive', cookie: 'x-auth-token=SFMyNTY.g2gDdAAAAAF3DGpvYnNlZWtlcl9pZGEBbgYA8_4dzYwBYgAJOoA.Gta1vseP3eKCGE09OBZwU0RzgwlmvUy6XxaB__bibmc', host: 'localhost:3000', purpose: 'prefetch', 'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"macOS"', 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'none', 'sec-fetch-user': '?1', 'sec-purpose': 'prefetch;prerender', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' }
该问题在HTTPS生产环境中同样存在。
临时解决方案:
重定向到新的/login_success路由,在该路由中用window.location.href = "/profile"触发完整页面刷新:
import { useEffect } from 'react'; export default function Index({}) { useEffect(() => { window.location.href = '/profile'; }, []); return <div className="flex flex-col w-full">Please wait...</div>; }
原因分析
跨域重定向时,浏览器不会自动将SameSite=Strict的Cookie携带到后续的跨站导航请求中。而页面刷新属于同源请求,浏览器会正常携带Cookie,因此刷新后Remix能读取到。
修复方案
1. 调整Cookie的SameSite属性
将Phoenix中Cookie的same_site从"strict"改为"lax"。Lax模式允许在顶级页面跳转(如跨域重定向)时携带Cookie,既保留一定安全性,又能解决当前问题:
conn |> put_resp_cookie("x-auth-token", %{jobseeker_id: generated_user.id}, path: "/", max_age: one_week, http_only: true, secure: true, same_site: "lax", // 修改此处 domain: "localhost", sign: true ) |> redirect(external: "http://localhost:3000/profile")
2. 改用前端跳转替代后端跨域重定向
让Phoenix登录接口返回成功响应,由前端发起跳转。这样Cookie设置完成后,前端的同源跳转会自动携带Cookie:
- Phoenix后端返回JSON响应:
conn |> put_resp_cookie("x-auth-token", %{jobseeker_id: generated_user.id}, path: "/", max_age: one_week, http_only: true, secure: true, same_site: "strict", domain: "localhost", sign: true ) |> json(%{success: true, redirect_to: "http://localhost:3000/profile"})
- 前端登录页面收到响应后执行跳转:
// 假设登录请求用fetch发送 const loginResponse = await fetch('/api/login', { method: 'POST', body: formData }); const data = await loginResponse.json(); if (data.success) { window.location.href = data.redirect_to; }
3. 优化临时方案(保留Strict模式时)
如果因安全要求必须使用SameSite=Strict,可以保留页面刷新的逻辑,但简化流程:直接让Phoenix重定向到Remix的/login_success路由,无需额外中间跳转,或者在该路由中用更简洁的方式触发刷新。
内容的提问来源于stack exchange,提问作者bigpotato

