nuxt3: 记录一次跨域导致请求头丢失的问题
技术栈:
node: 18.18.2
nuxt: 3.12.3
@sidebase/nuxt-auth
本地启动了A和B两个项目, A是前端, B是后端提供api接口
总结
总结下来就是nuxt服务端未允许跨域, 浏览器发送跨域请求不会携带认证信息, 预检请求需要服务端做额外处理的问题.
解决方法:
- nuxt通过设置routeRules开启跨域支持.参考文档
- routeRules额外设置headers添加'Access-Control-Allow-Credentials': 'true'允许携带认证信息.
- 在中间件中判断跨域请求并自动放行.
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调用会出现跨域错误
报错的意思是响应头中缺少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服务的登录接口成功返回, 此时我以为跨域问题解决了(其实没有)
继续联调接口
发现问题: A服务调用B服务的获取用户信息报错.
浏览器控制台展示为cors错误, B服务后端展示为Authorization请求头获取不到
f12明明显示携带了Authorization
可B后台debug却获取不到请求头.
用apifox调用却能成功, B后台能成功获取Authorization
继续测试
apifox能成功, 浏览器却不行. 换个浏览器试试. 一开始默认的edge, 现在换成chrome.
现在发现chrome会多发送一个预检请求, 奇怪的是这个预检请求并没有携带Authorization
403报错是因为B后台的中间件获取不到Authorization抛出的
预检请求问题
网上搜索: 谷歌浏览器预检请求不携带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.
问了下豆包, 大概意思是浏览器对跨域请求两次, 第一次是预检, 用来检查服务端是否允许跨域, 当服务端返回成功时再发送真正的请求.
问题就出在第一次预检上面, 因为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, 问题解决
小插曲
解决过程中不知道从哪看到要在A服务的fetch上面添加options.credentials = 'include',开启之前是没有的
我加上后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'.
解决方法就是不设置options.credentials = 'include'. 豆包给的提示说:
Access-Control-Allow-Origin 与 credentials 冲突
若前端设置 credentials: 'include'(携带 Cookie),后端 Access-Control-Allow-Origin 不能使用 *,必须指定具体域名(如 http://localhost:3000)。
若使用 *,浏览器会拒绝接收响应。
复盘
有几个问题记录下:
- Access-Control-Allow-Credentials允许携带认证信息设置为true, 为什么预检请求还是不携带?
我的理解是这个响应头是对正式请求来说的, 关闭后应该会自动的排除认证信息, 可测试后发现会携带. 不知道什么原因
- 为什么login接口没事, 获取用户信息就出事
很简单,login默认在白名单里面,并且也不需要Authorization头, 所以能调通就行.
其他接口在走中间件的时候, 拿不到Authorization头就自然而然的报错了. - 跨域请求的认识不够
以前写java遇到跨域请求就是写个注解或者基础框架自动处理好了. 真遇到了报错就对这些响应头/请求头不理解, 导致走了很多弯路.
浏览器的报错信息也没认证读, 看到cors就去胡乱测试, 不看具体的原因. - 有问题还是要用谷歌浏览器
一开始edge都没发预检, 再加上我登陆接口成功, 就默认不是跨域的问题. 在看到chrome的两条请求之后才重新解决跨域问题. - 要多翻mdn等官方文档
官方的解释很清楚, 知道了原理再去搜解决方案.