React客户端未回传HTTP-only Cookie致Express服务器401 Unauthorized问题
React与Express项目中遇到API请求返回401 Unauthorized状态码的问题:服务器已配置HTTP-only Cookie和CORS,客户端能成功接收token,但调用需要认证的API时未将Cookie回传至服务器;无需token的API请求运行正常。已确认.env文件配置正确,Chrome开发者工具中启用了禁用缓存,并手动清除了浏览器中保存的HTTP-only Cookie,请求协助排查问题。
请求信息
Request URL: http://localhost:8000/api/listing/list/66325435f974e1389b48d311 Request Method: GET Status Code: 401 Unauthorized Remote Address: [::1]:8000 Referrer Policy: strict-origin-when-cross-origin ### 响应头 HTTP/1.1 401 Unauthorized X-Powered-By: Express Content-Security-Policy: default-src 'self';script-src 'self' http://localhost:3000 'sha256-hash-of-script-content';object-src 'none';upgrade-insecure-requests;base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;script-src-attr 'none';style-src 'self' https: 'unsafe-inline' Cross-Origin-Opener-Policy: same-origin Cross-Origin-Resource-Policy: cross-origin Origin-Agent-Cluster: ?1 Referrer-Policy: no-referrer Strict-Transport-Security: max-age=31536000; includeSubDomains X-Content-Type-Options: nosniff X-DNS-Prefetch-Control: off X-Download-Options: noopen X-Frame-Options: SAMEORIGIN X-Permitted-Cross-Domain-Policies: none X-XSS-Protection: 0 Access-Control-Allow-Origin: http://localhost:3000 Vary: Origin, Accept-Encoding Access-Control-Allow-Credentials: true Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept Content-Type: application/json; charset=utf-8 ETag: W/"72f-Cf8NP8RMMXaFfkEWgmAlCOlVVQo" Content-Encoding: gzip Date: Sun, 01 Sep 2024 08:37:03 GMT Connection: keep-alive Keep-Alive: timeout=5 Transfer-Encoding: chunked ### 请求头 GET /api/listing/list/66325435f974e1389b48d311 HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate, br, zstd Accept-Language: en-US,en;q=0.9,de;q=0.8 Cache-Control: no-cache Connection: keep-alive Content-Security-Policy: default-src 'self'; connect-src 'self' http://localhost:8000; Content-Type: application/json Host: localhost:8000 Origin: http://localhost:3000 Pragma: no-cache Referer: http://localhost:3000/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-site User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 X-Content-Type-Options: nosniff X-Frame-Options: DENY X-XSS-Protection: 1; mode=block sec-ch-ua: "Not)A;Brand";v="99", "Google Chrome";v="127", "Chromium";v="127" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows"
客户端代码
请求头配置
const __baseURL = import.meta.env.VITE_APP_API_ENDPOINT; export const fetchHeaders: Record<string, string> = { "Content-Type": "application/json", "X-XSS-Protection": "1; mode=block", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", "Content-Security-Policy": `default-src 'self'; connect-src 'self' ${__baseURL};` };
API请求配置
const options: RequestInit = { method: httpMethod.toUpperCase(), headers: fetchHeaders, credentials: 'include' // for CORS };
完整API管理代码
import xss from "xss"; const __baseURL = import.meta.env.VITE_APP_API_ENDPOINT; export const fetchHeaders: Record<string, string> = { "Content-Type": "application/json", "X-XSS-Protection": "1; mode=block", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", "Content-Security-Policy": `default-src 'self'; connect-src 'self' ${__baseURL};` }; type TApi = { urlPath: string; httpMethod: 'get' | 'post' | 'delete' | 'put'; apiParam?: object | string; }; export async function apiManager({ httpMethod, urlPath, apiParam }: TApi): Promise<{ data: any, error: string | null, status: number }> { try { let fullUrl = `${__baseURL}${urlPath}`; const options: RequestInit = { method: httpMethod.toUpperCase(), headers: fetchHeaders, credentials: 'include' // for CORS }; if ((httpMethod === 'get' || httpMethod === 'delete') && apiParam) { if (typeof apiParam === 'object') { const queryString = new URLSearchParams(apiParam as Record<string, string>).toString(); fullUrl = `${fullUrl}?${xss(queryString)}`; } else if (typeof apiParam === 'string') { fullUrl = `${fullUrl}?${xss(apiParam)}`; } } else if ((httpMethod === 'post' || httpMethod === 'put') && apiParam) { options.body = xss((typeof apiParam === 'object') ? JSON.stringify(apiParam) : apiParam); } console.log('fullUrl:', fullUrl) console.log('options:', options) const res = await fetch(fullUrl, options); const status = res.status; let data: any = null; let error: string | null = null; try { data = await res.json(); } catch (jsonError) { error = `Failed to parse response as JSON. Status: ${status}`; } if (!res.ok) { switch (status) { case 400: error = 'Bad Request. Please check your input and try again.'; break; case 401: error = 'Unauthorized. Please log in to access this resource.'; break; case 403: error = 'Forbidden. You do not have permission to access this resource.'; break; case 404: error = 'Not Found. The resource you are looking for might have been removed.'; break; case 409: error = 'Conflict. There was a conflict with the current state of the resource.'; break; case 422: error = 'Unprocessable Entity. Validation errors occurred.'; break; case 500: error = 'Internal Server Error. Please try again later.'; break; case 502: error = 'Bad Gateway. Received an invalid response from the upstream server.'; break; case 503: error = 'Service Unavailable. The server is currently unable to handle the request.'; break; case 504: error = 'Gateway Timeout. The server did not receive a timely response from the upstream server.'; break; default: error = `HTTP error! Status: ${status}`; break; } if (data.message) error += '\n- ' + data.message } return { data, error, status }; } catch (networkError: unknown) { console.error('Network/API error:', networkError); let errorMessage = 'An unknown error occurred'; if (networkError instanceof Error) { errorMessage = networkError.message; } return { data: null, error: errorMessage, status: 0 }; } }
API调用示例
const { data, error } = await apiManager({ urlPath: '/api/listing/list/' + currentUser?._id, httpMethod: 'get', }); if (data?.success === false || error) { toast.error(error, toastBody) return }
服务器代码
import cors from "cors"; const corsOptions = { origin: envManager.ORIGIN, credentials: true, optionsSuccessStatus: 200 }; app.use(cors(corsOptions)); app.use((req, res, next) => { res.setHeader('Cross-Origin-Opener-Policy', 'same-origin-allow-popups'); res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); res.setHeader('Referrer-Policy', 'no-referrer-when-downgrade'); next(); }); // 登录控制器响应 res .cookie(__SERVER_ACCESS_TOKEN, token, { httpOnly: true, secure: process.env?.NODE_ENV === 'production', sameSite: 'none', path: '/', domain: envManager.ORIGIN, }) .status(200) .json(rest);
核心排查点及修复建议
- Cookie Domain配置错误:登录接口中
domain: envManager.ORIGIN如果是http://localhost:3000,属于无效配置——domain只能是主机名(如localhost),不能带协议和端口。浏览器会拒绝绑定此类Cookie,导致后续请求无法携带。
修复:将domain改为localhost,或在开发环境下直接省略该字段:res.cookie(__SERVER_ACCESS_TOKEN, token, { httpOnly: true, secure: process.env?.NODE_ENV === 'production', sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax', path: '/', // domain: envManager.ORIGIN, // 注释或改为'localhost' }) - SameSite与Secure冲突:
sameSite: 'none'要求必须配合secure: true(HTTPS环境),开发环境下secure为false,会导致Cookie被浏览器标记为无效。开发环境建议将sameSite改为'lax'或'strict'。 - 移除客户端冗余请求头:客户端
fetchHeaders中的Content-Security-Policy是响应头字段,不需要在请求中发送,建议移除,避免干扰浏览器策略。 - 验证Cookie存储状态:在Chrome开发者工具
Application -> Cookies -> http://localhost:8000下,确认Cookie是否存在,检查domain、path、httpOnly等属性是否符合预期。
内容的提问来源于stack exchange,提问作者user769371
相关产品推荐
相关产品推荐

