我在 React 和 Express 中有一个无法摆脱的 bug...
问题:状态代码:401未经授权。
服务器配置了仅 HTTP 的 cookie 和核心。 客户端收到令牌,但客户端不会将其发送回服务器。
无需令牌的 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
response header:
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
request headers:
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"
- client api call headers:
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};`
};
- client api config
const options: RequestInit = {
method: httpMethod.toUpperCase(),
headers: fetchHeaders,
credentials: 'include' // for CORS
};
server configuration:
import cors from "cors";
const corsOptions = {
origin: envManager.ORIGIN, // env variable = 'http://localhost:3000',
credentials: true, // Enable cookies to be sent with requests
optionsSuccessStatus: 200 // Some legacy browsers choke on 204
};
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();
});
response from login controller:
res
// httpOnly: cookie accessible from server ONLY - not react/client
.cookie(__SERVER_ACCESS_TOKEN, token, {
httpOnly: true, // ensures that cookies are not accessible from client-side JavaScript
secure: process.env?.NODE_ENV === 'production', // ensures that cookies are only sent over HTTPS in production mode
sameSite: 'none', // Change to 'strict' 'lax' or 'none' if needed (default is 'lax')
path: '/', // The path where the cookie is available
domain: envManager.ORIGIN,
})
.status(200)
.json(rest);
我被要求提供完整的客户端代码,所以这里是:
import xss from "xss";
const __baseURL = import.meta.env.VITE_APP_API_ENDPOINT;
export const fetchHeaders: Record<string, string> = {
// Specifies the media type of the resource being sent to the server.
// Setting it to "application/json" indicates that the content is JSON data.
"Content-Type": "application/json",
// Enables the browser's built-in Cross-Site Scripting (XSS) filter.
// It helps prevent XSS attacks by instructing the browser to block pages when it detects potential XSS attacks.
"X-XSS-Protection": "1; mode=block",
// Prevents MIME-sniffing attacks by instructing the browser to honor the declared Content-Type
// and not try to MIME-sniff the response.
"X-Content-Type-Options": "nosniff",
// Provides click jacking protection by indicating whether a browser should be allowed to render a page in a frame.
// Setting it to "DENY" ensures that the page cannot be embedded in a frame.
"X-Frame-Options": "DENY",
// Defines a policy that specifies the valid sources of content that the browser should consider executing or rendering.
// It helps mitigate various types of attacks, including XSS and data injection attacks,
// by restricting the resources that a page can load and execute.
// Here, we've set it to allow resources from the same origin ('self').
// "Content-Security-Policy": "default-src 'self'",
"Content-Security-Policy": `default-src 'self'; connect-src 'self' ${__baseURL};`
};
// const headers_test = {
// 'Accept': '*/*',
// 'Content-Type': 'application/json',
// }
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;
// Attempt to parse JSON response
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);
// Narrow the type of the error
let errorMessage = 'An unknown error occurred';
if (networkError instanceof Error) {
errorMessage = networkError.message;
}
return { data: null, error: errorMessage, status: 0 };
}
}
这就是 apiManager 的用途
const { data, error } = await apiManager({
urlPath: '/api/listing/list/' + currentUser?._id,
httpMethod: 'get',
});
if (data?.success === false || error) {
toast.error(error, toastBody)
return
}
所以: sameSite="none" 需要 HTTPS !!!否则在客户端上使用 lax(HTTP) 和代理进行开发。 (代理:仅用于端口而不是域)