引言:为什么 OAuth 2.0 和 JWT 是现代授权的标准
在现代 Web 开发中,安全认证和授权是任何应用程序的关键组成部分。OAuth 2.0 和 JWT(JSON Web Tokens)已成为构建分布式系统的事实标准。OAuth 2.0 提供了委托访问协议,而 JWT 则提供了一种紧凑且自包含的令牌格式。
在本文中,我们将通过 TypeScript 的实际示例,详细讲解如何实现完整的授权流程:从令牌颁发到验证和刷新。您将学习如何保护 API 端点、实现 refresh 令牌以及处理常见错误。
1. 项目基本设置和依赖安装
首先,我们创建一个基于 Node.js 和 TypeScript 的项目。请确保您已安装 Node.js(版本 18+)和 npm。
mkdir oauth-jwt-typescriptcd oauth-jwt-typescriptnpm init -ynpm install express cors dotenv jsonwebtoken bcryptnpm install -D typescript @types/express @types/cors @types/jsonwebtoken @types/bcrypt ts-node nodemonnpx tsc --init配置 tsconfig.json 以支持 ES 模块和严格类型。在 .env 文件中添加密钥:
ACCESS_TOKEN_SECRET=your-super-secret-access-keyREFRESH_TOKEN_SECRET=your-super-secret-refresh-keyACCESS_TOKEN_EXPIRES_IN=15mREFRESH_TOKEN_EXPIRES_IN=7d2. 实现 JWT 令牌的生成和验证
创建一个工具文件 src/utils/jwt.ts 用于处理令牌。我们将使用两种类型的令牌:Access Token(短期有效)和 Refresh Token(长期有效)。
import jwt from 'jsonwebtoken';import dotenv from 'dotenv';
dotenv.config();
interface TokenPayload { userId: string; email: string; role: 'user' | 'admin';}
export const generateAccessToken = (payload: TokenPayload): string => { return jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET!, { expiresIn: process.env.ACCESS_TOKEN_EXPIRES_IN || '15m', });};
export const generateRefreshToken = (payload: TokenPayload): string => { return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET!, { expiresIn: process.env.REFRESH_TOKEN_EXPIRES_IN || '7d', });};
export const verifyAccessToken = (token: string): TokenPayload => { return jwt.verify(token, process.env.ACCESS_TOKEN_SECRET!) as TokenPayload;};
export const verifyRefreshToken = (token: string): TokenPayload => { return jwt.verify(token, process.env.REFRESH_TOKEN_SECRET!) as TokenPayload;};请注意:我们对 payload 使用了严格的类型定义。这确保了令牌中始终包含必要的字段。
3. 创建用于保护路由的中间件
现在,我们实现一个中间件,用于检查 Authorization 头中是否存在有效的 Access Token。创建 src/middleware/auth.ts:
import { Request, Response, NextFunction } from 'express';import { verifyAccessToken } from '../utils/jwt';
export interface AuthRequest extends Request { user?: { userId: string; email: string; role: 'user' | 'admin'; };}
export const authenticate = (req: AuthRequest, res: Response, next: NextFunction): void => { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) { res.status(401).json({ error: '需要访问令牌' }); return; }
try { const decoded = verifyAccessToken(token); req.user = decoded; next(); } catch (error) { res.status(403).json({ error: '访问令牌无效或已过期' }); }};
export const authorizeRole = (...roles: string[]) => { return (req: AuthRequest, res: Response, next: NextFunction): void => { if (!req.user || !roles