401 未经授权,React 客户端未将纯 HTTP Cookie 发送回 Express 服务器

问题描述 投票:0回答:1

我在 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);

enter image description here

enter image description here

enter image description here

enter image description here

我被要求提供完整的客户端代码,所以这里是:

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
        }
reactjs express authentication http-status-code-401 httponly
1个回答
0
投票

所以: sameSite="none" 需要 HTTPS !!!否则在客户端上使用 lax(HTTP) 和代理进行开发。 (代理:仅用于端口而不是域)

© www.soinside.com 2019 - 2024. All rights reserved.