🚀 快速安装
复制以下命令并运行,立即安装此 Skill:
npx skills add https://github.com/wshobson/agents --skill auth-implementation-patterns
💡 提示:需要 Node.js 和 NPM
身份验证与授权实现模式
使用行业标准模式和现代最佳实践构建安全、可扩展的身份验证和授权系统。
何时使用此技能
- 实现用户身份验证系统
- 保护 REST 或 GraphQL API
- 添加 OAuth2/社交登录
- 实现基于角色的访问控制 (RBAC)
- 设计会话管理
- 迁移身份验证系统
- 调试身份验证问题
- 实现单点登录或多租户
核心概念
1. 身份验证与授权
身份验证 (AuthN): 你是谁?
- 验证身份(用户名/密码、OAuth、生物识别)
- 颁发凭证(会话、令牌)
- 管理登录/登出
授权 (AuthZ): 你能做什么?
- 权限检查
- 基于角色的访问控制 (RBAC)
- 资源所有权验证
- 策略执行
2. 身份验证策略
基于会话:
- 服务器存储会话状态
- 会话 ID 存储在 Cookie 中
- 传统、简单、有状态
基于令牌 (JWT):
- 无状态、自包含
- 水平扩展能力强
- 可存储声明信息
OAuth2/OpenID Connect:
- 委托身份验证
- 社交登录(Google、GitHub)
- 企业单点登录
JWT 身份验证
模式 1:JWT 实现
// JWT 结构: 头部.负载.签名
import jwt from "jsonwebtoken";
import { Request, Response, NextFunction } from "express";
interface JWTPayload {
userId: string; // 用户 ID
email: string; // 邮箱
role: string; // 角色
iat: number; // 签发时间
exp: number; // 过期时间
}
// 生成 JWT
function generateTokens(userId: string, email: string, role: string) {
const accessToken = jwt.sign(
{ userId, email, role },
process.env.JWT_SECRET!,
{ expiresIn: "15m" }, // 短期令牌(15分钟)
);
const refreshToken = jwt.sign(
{ userId },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: "7d" }, // 长期刷新令牌(7天)
);
return { accessToken, refreshToken };
}
// 验证 JWT
function verifyToken(token: string): JWTPayload {
try {
return jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new Error("令牌已过期");
}
if (error instanceof jwt.JsonWebTokenError) {
throw new Error("无效的令牌");
}
throw error;
}
}
// 中间件
function authenticate(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ error: "未提供令牌" });
}
const token = authHeader.substring(7);
try {
const payload = verifyToken(token);
req.user = payload; // 将用户信息附加到请求对象
next();
} catch (error) {
return res.status(401).json({ error: "无效的令牌" });
}
}
// 使用示例
app.get("/api/profile", authenticate, (req, res) => {
res.json({ user: req.user });
});
模式 2:刷新令牌流程
interface StoredRefreshToken {
token: string; // 令牌值
userId: string; // 用户 ID
expiresAt: Date; // 过期时间
createdAt: Date; // 创建时间
}
class RefreshTokenService {
// 在数据库中存储刷新令牌
async storeRefreshToken(userId: string, refreshToken: string) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
await db.refreshTokens.create({
token: await hash(refreshToken), // 存储前进行哈希处理
userId,
expiresAt,
});
}
// 刷新访问令牌
async refreshAccessToken(refreshToken: string) {
// 验证刷新令牌
let payload;
try {
payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!) as {
userId: string;
};
} catch {
throw new Error("无效的刷新令牌");
}
// 检查令牌是否存在于数据库中
const storedToken = await db.refreshTokens.findOne({
where: {
token: await hash(refreshToken),
userId: payload.userId,
expiresAt: { $gt: new Date() }, // 未过期
},
});
if (!storedToken) {
throw new Error("刷新令牌不存在或已过期");
}
// 获取用户信息
const user = await db.users.findById(payload.userId);
if (!user) {
throw new Error("用户不存在");
}
// 生成新的访问令牌
const accessToken = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: "15m" },
);
return { accessToken };
}
// 吊销刷新令牌(登出)
async revokeRefreshToken(refreshToken: string) {
await db.refreshTokens.deleteOne({
token: await hash(refreshToken),
});
}
// 吊销用户所有令牌(登出所有设备)
async revokeAllUserTokens(userId: string) {
await db.refreshTokens.deleteMany({ userId });
}
}
// API 端点
app.post("/api/auth/refresh", async (req, res) => {
const { refreshToken } = req.body;
try {
const { accessToken } =
await refreshTokenService.refreshAccessToken(refreshToken);
res.json({ accessToken });
} catch (error) {
res.status(401).json({ error: "无效的刷新令牌" });
}
});
app.post("/api/auth/logout", authenticate, async (req, res) => {
const { refreshToken } = req.body;
await refreshTokenService.revokeRefreshToken(refreshToken);
res.json({ message: "登出成功" });
});
基于会话的身份验证
模式 1:Express 会话
import session from "express-session";
import RedisStore from "connect-redis";
import { createClient } from "redis";
// 设置 Redis 作为会话存储
const redisClient = createClient({
url: process.env.REDIS_URL,
});
await redisClient.connect();
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET!, // 会话密钥
resave: false, // 是否每次请求都重新保存
saveUninitialized: false, // 是否保存未初始化的会话
cookie: {
secure: process.env.NODE_ENV === "production", // 生产环境只使用 HTTPS
httpOnly: true, // 禁止 JavaScript 访问
maxAge: 24 * 60 * 60 * 1000, // 24 小时
sameSite: "strict", // CSRF 保护
},
}),
);
// 登录
app.post("/api/auth/login", async (req, res) => {
const { email, password } = req.body;
const user = await db.users.findOne({ email });
if (!user || !(await verifyPassword(password, user.passwordHash))) {
return res.status(401).json({ error: "邮箱或密码错误" });
}
// 将用户信息存入会话
req.session.userId = user.id;
req.session.role = user.role;
res.json({ user: { id: user.id, email: user.email, role: user.role } });
});
// 会话中间件
function requireAuth(req: Request, res: Response, next: NextFunction) {
if (!req.session.userId) {
return res.status(401).json({ error: "未登录" });
}
next();
}
// 受保护的路由
app.get("/api/profile", requireAuth, async (req, res) => {
const user = await db.users.findById(req.session.userId);
res.json({ user });
});
// 登出
app.post("/api/auth/logout", (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: "登出失败" });
}
res.clearCookie("connect.sid");
res.json({ message: "登出成功" });
});
});
OAuth2 / 社交登录
模式 1:使用 Passport.js 实现 OAuth2
import passport from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { Strategy as GitHubStrategy } from "passport-github2";
// Google OAuth 配置
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID!, // Google 客户端 ID
clientSecret: process.env.GOOGLE_CLIENT_SECRET!, // Google 客户端密钥
callbackURL: "/api/auth/google/callback", // 回调 URL
},
async (accessToken, refreshToken, profile, done) => {
try {
// 查找或创建用户
let user = await db.users.findOne({
googleId: profile.id,
});
if (!user) {
user = await db.users.create({
googleId: profile.id, // Google ID
email: profile.emails?.[0]?.value, // 邮箱
name: profile.displayName, // 姓名
avatar: profile.photos?.[0]?.value, // 头像
});
}
return done(null, user);
} catch (error) {
return done(error, undefined);
}
},
),
);
// 路由
app.get(
"/api/auth/google",
passport.authenticate("google", {
scope: ["profile", "email"], // 请求的权限范围
}),
);
app.get(
"/api/auth/google/callback",
passport.authenticate("google", { session: false }), // 不创建会话
(req, res) => {
// 生成 JWT
const tokens = generateTokens(req.user.id, req.user.email, req.user.role);
// 重定向到前端,携带令牌
res.redirect(
`${process.env.FRONTEND_URL}/auth/callback?token=${tokens.accessToken}`,
);
},
);
授权模式
模式 1:基于角色的访问控制 (RBAC)
enum Role {
USER = "user", // 普通用户
MODERATOR = "moderator", // 版主
ADMIN = "admin", // 管理员
}
// 角色层级关系
const roleHierarchy: Record<Role, Role[]> = {
[Role.ADMIN]: [Role.ADMIN, Role.MODERATOR, Role.USER], // 管理员拥有所有权限
[Role.MODERATOR]: [Role.MODERATOR, Role.USER], // 版主拥有版主和用户权限
[Role.USER]: [Role.USER], // 普通用户只有自己的权限
};
// 检查用户是否有指定角色
function hasRole(userRole: Role, requiredRole: Role): boolean {
return roleHierarchy[userRole].includes(requiredRole);
}
// 中间件:要求特定角色
function requireRole(...roles: Role[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: "未登录" });
}
// 检查用户是否拥有所需角色中的任意一个
if (!roles.some((role) => hasRole(req.user.role, role))) {
return res.status(403).json({ error: "权限不足" });
}
next();
};
}
// 使用示例
app.delete(
"/api/users/:id",
authenticate, // 先验证身份
requireRole(Role.ADMIN), // 再检查权限(仅管理员可删除用户)
async (req, res) => {
await db.users.delete(req.params.id);
res.json({ message: "用户已删除" });
},
);
模式 2:基于权限的访问控制
enum Permission {
READ_USERS = "read:users", // 读取用户
WRITE_USERS = "write:users", // 写入用户
DELETE_USERS = "delete:users", // 删除用户
READ_POSTS = "read:posts", // 读取文章
WRITE_POSTS = "write:posts", // 写入文章
}
// 角色与权限的映射
const rolePermissions: Record<Role, Permission[]> = {
[Role.USER]: [Permission.READ_POSTS, Permission.WRITE_POSTS], // 用户可以读写文章
[Role.MODERATOR]: [
Permission.READ_POSTS,
Permission.WRITE_POSTS,
Permission.READ_USERS, // 版主还可以读取用户
],
[Role.ADMIN]: Object.values(Permission), // 管理员拥有所有权限
};
// 检查用户是否有指定权限
function hasPermission(userRole: Role, permission: Permission): boolean {
return rolePermissions[userRole]?.includes(permission) ?? false;
}
// 中间件:要求特定权限
function requirePermission(...permissions: Permission[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: "未登录" });
}
// 检查用户是否拥有所有要求的权限
const hasAllPermissions = permissions.every((permission) =>
hasPermission(req.user.role, permission),
);
if (!hasAllPermissions) {
return res.status(403).json({ error: "权限不足" });
}
next();
};
}
// 使用示例
app.get(
"/api/users",
authenticate,
requirePermission(Permission.READ_USERS), // 需要读取用户权限
async (req, res) => {
const users = await db.users.findAll();
res.json({ users });
},
);
模式 3:资源所有权
// 检查用户是否为资源所有者
async function requireOwnership(
resourceType: "post" | "comment", // 资源类型
resourceIdParam: string = "id", // URL 参数中的资源 ID
) {
return async (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: "未登录" });
}
const resourceId = req.params[resourceIdParam];
// 管理员可以访问任何资源
if (req.user.role === Role.ADMIN) {
return next();
}
// 检查所有权
let resource;
if (resourceType === "post") {
resource = await db.posts.findById(resourceId);
} else if (resourceType === "comment") {
resource = await db.comments.findById(resourceId);
}
if (!resource) {
return res.status(404).json({ error: "资源不存在" });
}
// 验证当前用户是否为资源所有者
if (resource.userId !== req.user.userId) {
return res.status(403).json({ error: "无权操作此资源" });
}
next();
};
}
// 使用示例
app.put(
"/api/posts/:id",
authenticate,
requireOwnership("post"), // 验证用户是否是文章所有者
async (req, res) => {
const post = await db.posts.update(req.params.id, req.body);
res.json({ post });
},
);
安全最佳实践
模式 1:密码安全
import bcrypt from "bcrypt";
import { z } from "zod";
// 密码验证模式
const passwordSchema = z
.string()
.min(12, "密码长度至少为 12 个字符")
.regex(/[A-Z]/, "密码必须包含大写字母")
.regex(/[a-z]/, "密码必须包含小写字母")
.regex(/[0-9]/, "密码必须包含数字")
.regex(/[^A-Za-z0-9]/, "密码必须包含特殊字符");
// 密码哈希
async function hashPassword(password: string): Promise<string> {
const saltRounds = 12; // 2^12 次迭代
return bcrypt.hash(password, saltRounds);
}
// 验证密码
async function verifyPassword(
password: string,
hash: string,
): Promise<boolean> {
return bcrypt.compare(password, hash);
}
// 注册接口(带密码验证)
app.post("/api/auth/register", async (req, res) => {
try {
const { email, password } = req.body;
// 验证密码强度
passwordSchema.parse(password);
// 检查用户是否已存在
const existingUser = await db.users.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: "邮箱已被注册" });
}
// 对密码进行哈希
const passwordHash = await hashPassword(password);
// 创建用户
const user = await db.users.create({
email,
passwordHash,
});
// 生成令牌
const tokens = generateTokens(user.id, user.email, user.role);
res.status(201).json({
user: { id: user.id, email: user.email },
...tokens,
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ error: error.errors[0].message });
}
res.status(500).json({ error: "注册失败" });
}
});
模式 2:速率限制
import rateLimit from "express-rate-limit";
import RedisStore from "rate-limit-redis";
// 登录接口速率限制
const loginLimiter = rateLimit({
store: new RedisStore({ client: redisClient }), // 使用 Redis 存储
windowMs: 15 * 60 * 1000, // 15 分钟窗口
max: 5, // 最多 5 次尝试
message: "尝试登录次数过多,请稍后再试",
standardHeaders: true,
legacyHeaders: false,
});
// API 通用速率限制
const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 分钟
max: 100, // 每分钟最多 100 个请求
standardHeaders: true,
});
// 应用到路由
app.post("/api/auth/login", loginLimiter, async (req, res) => {
// 登录逻辑
});
app.use("/api/", apiLimiter);
最佳实践
- 绝不存储明文密码:始终使用 bcrypt/argon2 进行哈希处理
- 使用 HTTPS:加密传输中的数据
- 短期访问令牌:最长 15-30 分钟
- 安全 Cookie 设置:使用 httpOnly、secure、sameSite 标志
- 验证所有输入:邮箱格式、密码强度
- 对认证接口进行速率限制:防止暴力破解
- 实现 CSRF 保护:对于基于会话的认证
- 定期轮换密钥:JWT 密钥、会话密钥
- 记录安全事件:登录尝试、认证失败
- 尽可能使用多因素认证:增加额外的安全层
常见陷阱
- 弱密码:实施强密码策略
- JWT 存储在 localStorage:易受 XSS 攻击,应使用 httpOnly Cookie
- 令牌永不过期:令牌应设置过期时间
- 仅做客户端权限检查:始终在服务端进行验证
- 不安全的密码重置:使用带过期时间的临时令牌
- 缺乏速率限制:易受暴力破解攻击
- 信任客户端数据:始终在服务端进行验证
📄 原始文档
完整文档(英文):
https://skills.sh/wshobson/agents/auth-implementation-patterns
💡 提示:点击上方链接查看 skills.sh 原始英文文档,方便对照翻译。
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

评论(0)