Bearer Token 认证
要点
- Bearer Token 是 HTTP 认证方案,Token 通过
Authorization: Bearer <token>传递 - Bearer 意味着「持有者」——谁持有 Token 谁就有权限,不需要额外证明
- JWT、OAuth Access Token、API Key 都可以用 Bearer 方案传递
- Bearer Token 必须通过 HTTPS 传输,防止中间人窃取
- Token 的撤销和刷新是 Bearer 方案的主要挑战
- AI 项目的 API 服务通常使用 Bearer JWT 或 Bearer API Key
1. Bearer 认证原理
1.1 HTTP Authorization Header
Bearer Token 通过 HTTP Authorization Header 传递:
GET /api/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Bearer:认证方案(scheme)- 后面是 Token 字符串
1.2 与其他方案的对比
| 方案 | 用途 | 格式 |
|---|---|---|
| Bearer | 通用 Token | Bearer <token> |
| Basic | 用户名密码 | Basic base64(user:pass) |
| Digest | 用户名密码(更安全) | Digest ... |
| Token | Kubernetes 等 | Token <token> |
Bearer 是最通用的方案,JWT、OAuth、API Key 都用它。
2. Hono 实现
2.1 基本实现
import { Hono } from 'hono'
import { verify } from 'hono/jwt'
const app = new Hono()
// Bearer Token 认证中间件
app.use('/api/*', async (c, next) => {
const authHeader = c.req.header('Authorization')
if (!authHeader) {
return c.json({ error: 'Missing Authorization header' }, 401)
}
// 解析 Bearer Token
const parts = authHeader.split(' ')
if (parts.length !== 2 || parts[0] !== 'Bearer') {
return c.json({ error: 'Invalid Authorization header format' }, 401)
}
const token = parts[1]
try {
// 验证 JWT
const payload = await verify(token, process.env.JWT_SECRET!)
// 设置上下文
c.set('user', payload)
await next()
} catch (error) {
return c.json({ error: 'Invalid token' }, 401)
}
})
// 受保护的路由
app.get('/api/profile', (c) => {
const user = c.get('user')
return c.json(user)
})2.2 使用 Hono 内置中间件
Hono 提供了 bearerAuth 中间件:
import { Hono } from 'hono'
import { bearerAuth } from 'hono/bearer-auth'
const app = new Hono()
// 简单的 Bearer Token 验证
const token = 'my-secret-token'
app.use('/api/*', bearerAuth({ token }))
app.get('/api/profile', (c) => {
return c.json({ message: 'Authenticated' })
})这个中间件适合简单的 Token 验证(如 API Key),不适合 JWT。
2.3 结合 JWT
import { Hono } from 'hono'
import { jwt } from 'hono/jwt'
const app = new Hono()
// JWT 中间件自动解析 Bearer Token
app.use('/api/*', jwt({
secret: process.env.JWT_SECRET!,
alg: 'HS256',
}))
app.get('/api/profile', (c) => {
const payload = c.get('jwtPayload')
return c.json(payload)
})3. Token 类型
3.1 JWT
JWT 是最常见的 Bearer Token:
import { sign, verify } from 'hono/jwt'
// 签发
const token = await sign(
{
userId: 'user-1',
email: '[email protected]',
role: 'admin',
exp: Math.floor(Date.now() / 1000) + 60 * 60, // 1 小时
},
process.env.JWT_SECRET!
)
// 验证
const payload = await verify(token, process.env.JWT_SECRET!)特点:
- 无状态,服务端不需要存储
- 自包含,Token 里有用户信息
- 过期后需要刷新
3.2 OAuth Access Token
OAuth 流程返回的 Access Token:
// OAuth 回调
app.get('/auth/callback', async (c) => {
const code = c.req.query('code')
// 换取 Access Token
const response = await fetch('https://oauth.provider.com/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
client_id: process.env.CLIENT_ID!,
client_secret: process.env.CLIENT_SECRET!,
redirect_uri: 'https://api.example.com/auth/callback',
}),
})
const { access_token, token_type, expires_in } = await response.json()
// token_type 通常是 "Bearer"
// 存储 access_token,后续请求使用
})3.3 API Key
API Key 也可以用 Bearer 方案:
app.use('/api/*', async (c, next) => {
const authHeader = c.req.header('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return c.json({ error: 'Missing API key' }, 401)
}
const apiKey = authHeader.substring(7)
// 验证 API Key
const key = await validateApiKey(apiKey)
if (!key) {
return c.json({ error: 'Invalid API key' }, 401)
}
c.set('apiKey', key)
await next()
})4. Token 刷新
4.1 Access Token + Refresh Token
短期 Access Token + 长期 Refresh Token:
// 登录
app.post('/auth/login', async (c) => {
const { email, password } = await c.req.json()
const user = await verifyUser(email, password)
if (!user) {
return c.json({ error: 'Invalid credentials' }, 401)
}
// Access Token(15 分钟)
const accessToken = await sign(
{
userId: user.id,
type: 'access',
exp: Math.floor(Date.now() / 1000) + 15 * 60,
},
process.env.JWT_SECRET!
)
// Refresh Token(7 天)
const refreshToken = await sign(
{
userId: user.id,
type: 'refresh',
exp: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60,
},
process.env.JWT_SECRET!
)
// 存储 Refresh Token
await db.insert(refreshTokens).values({
userId: user.id,
token: refreshToken,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
})
return c.json({
accessToken,
refreshToken,
expiresIn: 15 * 60,
})
})4.2 刷新逻辑
app.post('/auth/refresh', async (c) => {
const { refreshToken } = await c.req.json()
// 验证 Refresh Token
try {
const payload = await verify(refreshToken, process.env.JWT_SECRET!)
if (payload.type !== 'refresh') {
return c.json({ error: 'Invalid token type' }, 401)
}
// 检查是否在数据库中
const stored = await db.query.refreshTokens.findFirst({
where: eq(refreshTokens.token, refreshToken),
})
if (!stored || stored.expiresAt < new Date()) {
return c.json({ error: 'Invalid refresh token' }, 401)
}
// 签发新的 Access Token
const newAccessToken = await sign(
{
userId: payload.userId,
type: 'access',
exp: Math.floor(Date.now() / 1000) + 15 * 60,
},
process.env.JWT_SECRET!
)
return c.json({
accessToken: newAccessToken,
expiresIn: 15 * 60,
})
} catch {
return c.json({ error: 'Invalid refresh token' }, 401)
}
})4.3 前端自动刷新
前端在 Access Token 过期前自动刷新:
// 前端代码
let accessToken = localStorage.getItem('accessToken')
let refreshToken = localStorage.getItem('refreshToken')
async function fetchWithAuth(url: string) {
// 检查 Access Token 是否快过期
if (isTokenExpiringSoon(accessToken)) {
await refreshAccessToken()
}
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (response.status === 401) {
// Token 过期,尝试刷新
await refreshAccessToken()
// 重试请求
return fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
}
return response
}
async function refreshAccessToken() {
const response = await fetch('/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
})
const data = await response.json()
accessToken = data.accessToken
localStorage.setItem('accessToken', accessToken)
}
function isTokenExpiringSoon(token: string): boolean {
const payload = JSON.parse(atob(token.split('.')[1]))
const expiresAt = payload.exp * 1000
const now = Date.now()
// 5 分钟内过期
return expiresAt - now < 5 * 60 * 1000
}5. Token 撤销
5.1 黑名单
把撤销的 Token 加入黑名单:
// 登出时撤销 Token
app.post('/auth/logout', async (c) => {
const user = c.get('user')
const token = c.req.header('Authorization')?.substring(7)
if (token) {
// 把 Token 加入黑名单
const payload = await verify(token, process.env.JWT_SECRET!)
const expiresAt = payload.exp
await redis.setex(
`blacklist:${token}`,
expiresAt - Math.floor(Date.now() / 1000),
'1'
)
}
// 删除 Refresh Token
await db
.delete(refreshTokens)
.where(eq(refreshTokens.userId, user.userId))
return c.json({ message: 'Logged out' })
})
// 验证时检查黑名单
app.use('/api/*', async (c, next) => {
const token = c.req.header('Authorization')?.substring(7)
// 检查黑名单
const isBlacklisted = await redis.get(`blacklist:${token}`)
if (isBlacklisted) {
return c.json({ error: 'Token has been revoked' }, 401)
}
// 继续验证...
})5.2 Token 版本
使用版本号撤销所有 Token:
// 用户表添加 tokenVersion 字段
export const users = pgTable('users', {
id: uuid('id').primaryKey(),
tokenVersion: integer('token_version').default(0).notNull(),
})
// 签发 Token 时包含版本号
const token = await sign(
{
userId: user.id,
tokenVersion: user.tokenVersion,
exp: Math.floor(Date.now() / 1000) + 60 * 60,
},
process.env.JWT_SECRET!
)
// 验证时检查版本号
app.use('/api/*', async (c, next) => {
const payload = c.get('jwtPayload')
const user = await db.query.users.findFirst({
where: eq(users.id, payload.userId),
})
if (payload.tokenVersion !== user?.tokenVersion) {
return c.json({ error: 'Token has been revoked' }, 401)
}
await next()
})
// 撤销所有 Token
app.post('/auth/revoke-all', async (c) => {
const user = c.get('user')
await db
.update(users)
.set({ tokenVersion: sql`${users.tokenVersion} + 1` })
.where(eq(users.id, user.userId))
return c.json({ message: 'All tokens revoked' })
})6. 安全考虑
6.1 HTTPS
Bearer Token 必须通过 HTTPS 传输:
❌ HTTP 传输,Token 容易被窃听
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
✅ HTTPS 传输,加密保护
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...6.2 Token 存储
前端存储 Token 的方式:
// ❌ localStorage,容易被 XSS 攻击读取
localStorage.setItem('token', token)
// ✅ httpOnly Cookie,JavaScript 无法访问
setCookie(c, 'token', token, {
httpOnly: true,
secure: true,
sameSite: 'Strict',
})
// ⚠️ 内存存储,刷新后丢失
let token = token // 变量推荐:
- Web 应用:httpOnly Cookie(需要 CSRF 保护)
- 移动应用:安全存储(Keychain、Keystore)
- SPA:内存 + 自动刷新
6.3 Token 泄露
Token 泄露后的应对:
- 短过期时间:减少泄露窗口
- 刷新机制:定期更换 Token
- 撤销机制:立即撤销泄露的 Token
- 监控:检测异常使用
7. AI 项目的实践
7.1 OpenAI 风格
// 请求
POST /v1/chat/completions
Authorization: Bearer sk-proj-abc123...
// 验证
app.use('/v1/*', async (c, next) => {
const authHeader = c.req.header('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return c.json({
error: {
message: 'Missing API key',
type: 'invalid_request_error',
},
}, 401)
}
const apiKey = authHeader.substring(7)
// 验证 API Key
const key = await validateApiKey(apiKey)
if (!key) {
return c.json({
error: {
message: 'Invalid API key',
type: 'authentication_error',
},
}, 401)
}
c.set('apiKey', key)
await next()
})7.2 响应格式
// 成功
{
"id": "chatcmpl-123",
"object": "chat.completion",
"model": "gpt-4",
"choices": [...]
}
// 错误
{
"error": {
"message": "Invalid API key",
"type": "authentication_error",
"code": "invalid_api_key"
}
}总结
Bearer Token 是最通用的 HTTP 认证方案。JWT、OAuth Token、API Key 都可以用 Bearer 方案传递。
这一节涉及到的几个实践:
- Bearer 原理:
Authorization: Bearer <token> - Token 类型:JWT、OAuth Access Token、API Key
- Token 刷新:Access Token + Refresh Token
- Token 撤销:黑名单、版本号
- 安全考虑:HTTPS、存储方式、泄露应对
- AI 项目:OpenAI 风格的 API Key
Bearer Token 是 API 认证的标准方案。配合短过期时间、刷新机制和撤销机制,可以构建安全的认证系统。
下一篇看 RBAC 权限模型——角色定义、权限分配、角色继承。