nuxt3: 记录一次跨域导致请求头丢失的问题

6

技术栈:
node: 18.18.2
nuxt: 3.12.3
@sidebase/nuxt-auth
本地启动了A和B两个项目, A是前端, B是后端提供api接口

总结

总结下来就是nuxt服务端未允许跨域, 浏览器发送跨域请求不会携带认证信息, 预检请求需要服务端做额外处理的问题.
解决方法:

  1. nuxt通过设置routeRules开启跨域支持.参考文档
  2. routeRules额外设置headers添加'Access-Control-Allow-Credentials': 'true'允许携带认证信息.
  3. 在中间件中判断跨域请求并自动放行.
    if (event.method === 'OPTIONS') {
    setResponseStatus(event, 204); // 204 No Content:预检成功,无内容
    return; // 终止后续中间件和路由执行
    }

项目结构

nuxt3写了个前后端一体的项目, 提供api, 记做B服务. 本地启动后的路径是http://localhost:3001
B服务使用@sidebase/nuxt-auth做登陆鉴权, 所以有个服务端的中间件:

// 1.auth.ts
export default eventHandler(async (event) =>{
	// 放行白名单(登录接口在白名单)
	    for (const url of whiteList) {
        if(event.path?.startsWith(url)){
            return
        }
    }
	// 根据请求头中的Authorization获取已登陆的用户
	    const session = await $fetch("/api/auth/session",{
        method: 'get',
        headers: event.headers
    })

    // console.log('中间件',session)
    if (!session) {
        throw createError({ statusMessage: 'Unauthenticated', statusCode: 403 })
    }
	// 把用户放到上下文中
    event.context.user = session.user
})

B服务还有几个接口, A服务需要调用这些接口:


文件地址: ~/server/api/sysUser/profile
请求路径:  http://localhost:3001/api/sysUser/profile
注意: 这个路径并不在中间件的白名单中.
/**
 * 获取用户的个人信息
 */
export default defineEventHandler(async (event: H3Event) => {
    let hedaers_inevent = await getRequestHeaders(event)
    console.log(hedaers_inevent)
	// 从上下文中获取登陆用户
    let user: sys_users = event.context.user
    let userInDb = await db.query.sys_users.findFirst(...查询用户...)
    return RespApi.success(userInDb)
})
// 还有个登录接口
// 请求路径 http://localhost:3001/api/auth/login
...省略代码

登陆提示跨域

因为A服务也是本地启动(地址是http://localhost:3000), A->B调用会出现跨域错误
1751422879668-2025-7-2-102119.png
报错的意思是响应头中缺少Access-Control-Allow-Origin属性

Access to fetch at 'http://localhost:3001/api/auth/login' from origin
'http://localhost:3000' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

所以需要让B支持cors, 找到官方文档, 按要求新增routeRules配置

参考: https://nuxt.com/docs/guide/concepts/rendering 中的Route Rules小节

// https://nuxt.com/docs/api/configuration/nuxt-config
import {defineNuxtConfig} from 'nuxt/config'

export default defineNuxtConfig({
    devtools: {enabled: false},
	ssr: false,
    modules: ['xx"],
    devServer:{
        port: 3001
    },
    build: {
        transpile: ['xxx']
    },
    routeRules: {
        // Enable CORS for all API routes
        '/api/**': {
            cors: true
        }
    },
    compatibilityDate: '2024-07-14'
})

开启cors后再测试, A调用B服务的登录接口成功返回, 此时我以为跨域问题解决了(其实没有)
1751423203672-2025-7-2-102643.png

继续联调接口

发现问题: A服务调用B服务的获取用户信息报错.

浏览器控制台展示为cors错误, B服务后端展示为Authorization请求头获取不到

f12明明显示携带了Authorization
1751381250475-2025-7-1-224730.png

可B后台debug却获取不到请求头.
1751381339475-2025-7-1-224859.png

用apifox调用却能成功, B后台能成功获取Authorization
1751381447475-2025-7-1-225047.png

继续测试

apifox能成功, 浏览器却不行. 换个浏览器试试. 一开始默认的edge, 现在换成chrome.
1751381687476-2025-7-1-225447.png

现在发现chrome会多发送一个预检请求, 奇怪的是这个预检请求并没有携带Authorization

403报错是因为B后台的中间件获取不到Authorization抛出的

1751381737500-2025-7-1-225537.png

预检请求问题

网上搜索: 谷歌浏览器预检请求不携带Authorization, 找到一篇csdn文章:https://blog.csdn.net/weixin_64309182/article/details/130933937
大概说的意思就是post预检请求默认不会携带认证信息. MDN上有详细说明

给出的方案是服务端设置Access-Control-Allow-Origin为true

允许预检请求携带认证信息

前面在B服务添加cors后可以发现, 会自动的给响应头添加4个:

access-control-allow-origin: *
access-control-allow-methods: *
access-control-allow-headers: *
access-control-max-age: 0

并没有需要的Access-Control-Allow-Origin, nuxt文档也说可以添加额外的headers, 所以加上试试

//修改后的nuxt.config.ts文件
import {defineNuxtConfig} from 'nuxt/config'

export default defineNuxtConfig({
    devtools: {enabled: false},
	ssr: false,
    modules: ['xx"],
    devServer:{
        port: 3001
    },
    build: {
        transpile: ['xxx']
    },
    routeRules: {
        // Enable CORS for all API routes
        '/api/**': {
            cors: true,
			headers: {
                'Access-Control-Allow-Credentials': 'true'
            }
        }
    },
    compatibilityDate: '2024-07-14'
})

还是不行

测试后发现还是报错cors, 但提示信息换了:

 Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

1751425070662-2025-7-2-105750.png
问了下豆包, 大概意思是浏览器对跨域请求两次, 第一次是预检, 用来检查服务端是否允许跨域, 当服务端返回成功时再发送真正的请求.
1751425428675-2025-7-2-110348.png
问题就出在第一次预检上面, 因为1.auth.ts这个中间件没做处理, 预检请求没带Authorization就会一直报错. 所以需要对预检请求放行

import {createError, eventHandler} from "h3";

const whiteList = ['xxx]

export default eventHandler(async (event) =>{
    let hhh = await getRequestHeaders(event)
    console.log(hhh)
    // const { context, node: { req } } = event
    for (const url of whiteList) {
        if(event.path?.startsWith(url)){
            return
        }
    }
	// 添加预检请求的判断
    if (event.method === 'OPTIONS') {
        setResponseStatus(event, 204); // 204 No Content:预检成功,无内容
        return; // 终止后续中间件和路由执行
    }
    let headers =   getRequestHeader(event, 'authorization')
    console.log('headers',headers)
    /// 其他代码
})

再次测试, 后台能获取到authorization, 问题解决
1751425895663-2025-7-2-111135.png

小插曲

解决过程中不知道从哪看到要在A服务的fetch上面添加options.credentials = 'include',开启之前是没有的
1751382284475-2025-7-1-230444.png
我加上后f12又换了报错信息:

Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

1751426139662-2025-7-2-111539.png

解决方法就是不设置options.credentials = 'include'. 豆包给的提示说:

Access-Control-Allow-Origin 与 credentials 冲突
若前端设置 credentials: 'include'(携带 Cookie),后端 Access-Control-Allow-Origin 不能使用 *,必须指定具体域名(如 http://localhost:3000)。
若使用 *,浏览器会拒绝接收响应。

复盘

有几个问题记录下:

  • Access-Control-Allow-Credentials允许携带认证信息设置为true, 为什么预检请求还是不携带?
    我的理解是这个响应头是对正式请求来说的, 关闭后应该会自动的排除认证信息, 可测试后发现会携带. 不知道什么原因
    1751426561671-2025-7-2-112241.png
  • 为什么login接口没事, 获取用户信息就出事
    很简单,login默认在白名单里面,并且也不需要Authorization头, 所以能调通就行.
    其他接口在走中间件的时候, 拿不到Authorization头就自然而然的报错了.
  • 跨域请求的认识不够
    以前写java遇到跨域请求就是写个注解或者基础框架自动处理好了. 真遇到了报错就对这些响应头/请求头不理解, 导致走了很多弯路.
    浏览器的报错信息也没认证读, 看到cors就去胡乱测试, 不看具体的原因.
  • 有问题还是要用谷歌浏览器
    一开始edge都没发预检, 再加上我登陆接口成功, 就默认不是跨域的问题. 在看到chrome的两条请求之后才重新解决跨域问题.
  • 要多翻mdn等官方文档
    官方的解释很清楚, 知道了原理再去搜解决方案.