You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React+Express-GraphQL项目无法设置Refresh Token Cookie求助

我正在开发基于React.js的应用,后端采用Express-GraphQL服务器,通过JWT的Access Token和Refresh Token实现身份验证,客户端使用Apollo执行查询。目前遇到Refresh Token Cookie无法在客户端正确设置的问题,查阅相关文档和社区内容后仍未解决。两个应用均运行在localhost,但端口不同,服务器响应头中能看到Cookie字段,但客户端并未成功存储该Cookie。

后端中间件配置

app.use(cookieParser())
app.use(cors(
    {
        credentials: true,
        origin: "*",
    }
))

Apollo 链接配置

export const httpLink = createHttpLink({
    uri:  API_LINK + "/graphql",
});

export const authLink = setContext((_, { headers }) => {
    const token = localStorage.getItem("accessToken");
    if(token && (Date.now() >= jwtDecode(token).exp * 1000)){
        return { headers: {
                ...headers,
            }}
    }
    if(!token){
        return {
            headers: {
                ...headers,
                withCredentials: true
            },
        };
    }
    return {
        headers: {
            ...headers,
            authorization: token ? `Bearer ${token}` : "",
        },
    };
});

export const getNewToken = async () => {
    try {
        const response = await fetch(API_LINK + "/graphql", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            credentials: "include",
            body: JSON.stringify({
                query: `
            mutation {
            refreshToken{
            type 
            token
                }
                }
                `
            })});
        return response.json();
    } catch (error) {
        console.log(error);
    }
};

export const errorLink = onError(
    ({ graphQLErrors, networkError, operation, forward }) => {
        if(networkError?.statusCode === 401 || networkError?.statusCode === 500 ) {
            console.log(networkError)
            return fromPromise(
                getNewToken().then(({data}) =>{
                    console.log(data)
                    localStorage.setItem("accessToken", data.refreshToken.token)
                    return data.refreshToken.token
                }).catch((error) => {
                    console.log(e)
                    return;
                })
            )
                .filter((value) => Boolean(value))
                .flatMap((accessToken) => {
                    const oldHeaders = operation.getContext().headers;
                    operation.setContext({
                        headers: {
                            ...oldHeaders,
                            authorization: `Bearer ${accessToken}`,
                        },
                    });
                    return forward(operation);
                });
        }
        if (graphQLErrors){
            graphQLErrors.forEach(({message, locations, path}) =>{
                    if(message === "Помилка авторизації") {
                        return   fromPromise(
                            getNewToken().then((data) =>{
                                console.log(data)
                                if(!data?.refreshToken?.token){
                                    return null
                                }
                                localStorage.setItem("accessToken", data?.refreshToken?.token)
                                return data?.refreshToken?.token
                            }).catch((error) => {
                                console.log(error)
                                return;
                            })
                        )
                            .filter((value) => Boolean(value))
                            .flatMap((accessToken) => {
                                const oldHeaders = operation.getContext().headers;
                                operation.setContext({
                                    headers: {
                                        ...oldHeaders,
                                        authorization: `Bearer ${accessToken}`,
                                    },
                                });
                                return forward(operation);
                            });
                    }
                    console.log(
                        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
                    )
            }
            );
        }


        if (networkError) {
            console.log(`[Network error]: ${networkError}`);
        }
    }
);

服务器端发送Cookie代码

res.set("Access-Control-Expose-Headers" ,"Set-Cookie")
res.cookie('refreshToken', tokens.refreshToken, {maxAge: 30 * 24 * 60 * 60 * 1000 , httpOnly: true , secure: false})

ApolloClient 初始化代码

const client  = new ApolloClient({
    link: from([errorLink , authLink, httpLink]),
    cache: new InMemoryCache(),
    connectToDevTools: true,
    credentials: "include"
})

解决方案

1. 修复CORS的origin配置

当开启credentials: true时,浏览器不允许使用"*"作为origin值,必须指定具体的客户端地址:

app.use(cors({
    credentials: true,
    origin: "http://localhost:3000" // 替换为你的React应用实际端口
}))

若有多个客户端地址,可使用数组或函数动态校验来源。

2. 给httpLink添加credentials配置

ApolloClient的全局credentials配置可能不生效到具体链接,需给createHttpLink单独设置:

export const httpLink = createHttpLink({
    uri: API_LINK + "/graphql",
    credentials: "include" // 新增该行
});

3. 移除authLink中无效的withCredentials设置

withCredentials并非请求头字段,不需要放在headers对象里,删除以下代码块:

if(!token){
    return {
        headers: {
            ...headers,
            withCredentials: true // 该行无意义,直接删除
        },
    };
}

4. 显式配置Cookie的sameSite属性

Express默认将sameSite设为'Lax',跨端口场景下可显式指定避免浏览器默认行为冲突:

res.cookie('refreshToken', tokens.refreshToken, {
    maxAge: 30 * 24 * 60 * 60 * 1000,
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production', // 生产环境开启HTTPS时设为true
    sameSite: 'Lax' // 显式声明,适配跨端口场景
})

5. 确认所有请求携带credentials

你的getNewToken函数已正确设置credentials: "include",确保所有与后端交互的请求都遵循该配置。


内容的提问来源于stack exchange,提问作者exxxamply

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 14:03:14