🚀 快速安装

复制以下命令并运行,立即安装此 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);

最佳实践

  1. 绝不存储明文密码:始终使用 bcrypt/argon2 进行哈希处理
  2. 使用 HTTPS:加密传输中的数据
  3. 短期访问令牌:最长 15-30 分钟
  4. 安全 Cookie 设置:使用 httpOnly、secure、sameSite 标志
  5. 验证所有输入:邮箱格式、密码强度
  6. 对认证接口进行速率限制:防止暴力破解
  7. 实现 CSRF 保护:对于基于会话的认证
  8. 定期轮换密钥:JWT 密钥、会话密钥
  9. 记录安全事件:登录尝试、认证失败
  10. 尽可能使用多因素认证:增加额外的安全层

常见陷阱

  • 弱密码:实施强密码策略
  • JWT 存储在 localStorage:易受 XSS 攻击,应使用 httpOnly Cookie
  • 令牌永不过期:令牌应设置过期时间
  • 仅做客户端权限检查:始终在服务端进行验证
  • 不安全的密码重置:使用带过期时间的临时令牌
  • 缺乏速率限制:易受暴力破解攻击
  • 信任客户端数据:始终在服务端进行验证

📄 原始文档

完整文档(英文):

https://skills.sh/wshobson/agents/auth-implementation-patterns

💡 提示:点击上方链接查看 skills.sh 原始英文文档,方便对照翻译。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。