feat(auth): 调用秘钥鉴权——AUTH_KEY 环境变量启用,Bearer/X-Api-Key/?key= 三种携带方式,流地址自动附带 key
This commit is contained in:
1 parent
bc1d23cbba
commit
649ebbe12b
5 files changed
+118
-6
No files matched your search
@@ -0,0 +1,40 @@
|
||||
// 全局调用秘钥鉴权 —— AUTH_KEY 环境变量启用(未设置则完全不鉴权)
|
||||
// 客户端携带方式(任一即可):
|
||||
// Authorization: Bearer <AUTH_KEY>(也接受裸值不带 Bearer 前缀)
|
||||
// X-Api-Key: <AUTH_KEY>
|
||||
// ?key=<AUTH_KEY>(查询参数,兼容播放器/下载器直接拉流无法带自定义头的场景)
|
||||
// 豁免:/health、文档(/、/docs*、/redoc、/openapi.json)与 CORS 预检
|
||||
import crypto from 'node:crypto'
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify'
|
||||
|
||||
// 无敏感信息且会被监控系统高频探测的路径豁免鉴权
|
||||
const PUBLIC_PATHS = [/^\/health$/, /^\/$/, /^\/docs(\/|$)/, /^\/redoc$/, /^\/openapi\.json$/]
|
||||
|
||||
const safeEqual = (a: string, b: string) => {
|
||||
const bufA = Buffer.from(a)
|
||||
const bufB = Buffer.from(b)
|
||||
return bufA.length === bufB.length && crypto.timingSafeEqual(bufA, bufB)
|
||||
}
|
||||
|
||||
export const registerAuth = (fastify: any, authKey: string) => {
|
||||
fastify.addHook('onRequest', async(request: FastifyRequest, reply: FastifyReply) => {
|
||||
if (request.method === 'OPTIONS') return
|
||||
const pathname = new URL(request.url, 'http://localhost').pathname
|
||||
if (PUBLIC_PATHS.some(re => re.test(pathname))) return
|
||||
|
||||
const authorization = String(request.headers.authorization || '')
|
||||
const bearer = authorization.startsWith('Bearer ') ? authorization.slice(7).trim() : authorization.trim()
|
||||
const provided = [
|
||||
bearer,
|
||||
String(request.headers['x-api-key'] || ''),
|
||||
String((request.query as any)?.key || ''),
|
||||
].find(Boolean)
|
||||
|
||||
if (!provided || !safeEqual(provided, authKey)) {
|
||||
return reply.code(401).send({
|
||||
code: 401,
|
||||
msg: '无效或缺少调用秘钥:请在 Authorization: Bearer <key>、X-Api-Key 头或 ?key= 参数中携带',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
+3
-2
@@ -136,9 +136,10 @@ export const musicRoutes = async(fastify: any) => {
|
||||
// 取流链:外部音源脚本(SOURCE_ENABLED 启用时)→ SDK 官方直连 → 酷我兜底(跨平台匹配)
|
||||
const finish = (res: any, via: string) => {
|
||||
if (!res?.url) throw new Error('获取播放地址失败')
|
||||
// qs 解密流等代理地址返回相对路径,按请求的协议与 host 补全(兼容本机/局域网播放器)
|
||||
// qs 解密流等代理地址返回相对路径,按请求的协议与 host 补全(兼容本机/局域网播放器);
|
||||
// 启用 AUTH_KEY 时附带 ?key=(播放器直接拉流无法携带自定义头)
|
||||
const url = String(res.url).startsWith('/')
|
||||
? `${request.protocol}://${request.headers.host}${res.url}`
|
||||
? `${request.protocol}://${request.headers.host}${res.url}${process.env.AUTH_KEY ? `${String(res.url).includes('?') ? '&' : '?'}key=${encodeURIComponent(process.env.AUTH_KEY)}` : ''}`
|
||||
: res.url
|
||||
// type 用实际音质(kw 等通道可能降级);via 标记兜底链中真正命中的通道
|
||||
return { type: res.type || quality, url, ...(via !== source ? { via } : {}) }
|
||||
|
||||
@@ -80,6 +80,14 @@ fastify.addHook('onRoute', (routeOptions) => {
|
||||
|
||||
fastify.register(cors, { origin: '*' })
|
||||
|
||||
// ---- 调用秘钥(AUTH_KEY 环境变量启用;未设置则不鉴权)----
|
||||
// 详见 src/http/auth-gate.ts;文档/健康检查豁免,业务接口要求 Bearer / X-Api-Key / ?key=
|
||||
const authKey = process.env.AUTH_KEY
|
||||
if (authKey) {
|
||||
const { registerAuth } = await import('./http/auth-gate.ts')
|
||||
registerAuth(fastify, authKey)
|
||||
}
|
||||
|
||||
fastify.addHook('onRequest', async(_request, reply) => {
|
||||
reply.header('Cache-Control', 'no-store')
|
||||
})
|
||||
@@ -204,6 +212,7 @@ if (!useSource) {
|
||||
export const startServer = async(port = PORT) => {
|
||||
await fastify.listen({ port: Number(port), host: '0.0.0.0' })
|
||||
console.log(`音乐服务器已启动: http://localhost:${port}`)
|
||||
console.log(`调用鉴权: ${authKey ? `已启用(AUTH_KEY,Bearer / X-Api-Key / ?key=)` : '未启用(设置 AUTH_KEY 环境变量开启)'}`)
|
||||
console.log(`取流通道: ${useSource ? '外部音源脚本(sources/,SOURCE_ENABLED)' : 'SDK 官方直连接口'}`)
|
||||
console.log(`接口文档: http://localhost:${port}/docs (Swagger UI)`)
|
||||
console.log(`ReDoc 文档: http://localhost:${port}/redoc`)
|
||||
|
||||
Reference in new issue
Block a user