brandonwise-secure-auth-patterns
掌握 JWT、OAuth2、会话管理和 RBAC 等认证与授权模式,构建安全、可扩展的访问控制系统。
安装 / 下载方式
TotalClaw CLI推荐
totalclaw install totalclaw:totalclaw~brandonwise-secure-auth-patternscURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/totalclaw%3Atotalclaw~brandonwise-secure-auth-patterns/file -o brandonwise-secure-auth-patterns.md## 概述(中文)
掌握 JWT、OAuth2、会话管理和 RBAC 等认证与授权模式,构建安全、可扩展的访问控制系统。
## 技能正文
# 认证与授权模式
掌握 JWT、OAuth2、会话管理和 RBAC 等认证与授权模式,构建安全、可扩展的访问控制系统。
## 描述
适用场景:
- 实现用户认证系统
- 保护 REST 或 GraphQL API
- 添加 OAuth2/社交登录或 SSO
- 设计会话管理
- 实现 RBAC 或权限系统
- 调试认证问题
不适用场景:
- 仅需 UI/登录页样式
- 纯基础设施任务且无身份相关需求
- 无法更改认证策略
---
## 认证 vs 授权
| 认证 (AuthN) | 授权 (AuthZ) |
|------------------------|----------------------|
| "你是谁?" | "你能做什么?" |
| 验证身份 | 检查权限 |
| 颁发凭据 | 执行策略 |
| 登录/登出 | 访问控制 |
---
## 认证策略
| 策略 | 优点 | 缺点 | 最适合 |
|----------|------|------|----------|
| **会话** | 简单、安全 | 有状态、扩展难 | 传统 Web 应用 |
| **JWT** | 无状态、可扩展 | 令牌体积、撤销难 | API、微服务 |
| **OAuth2/OIDC** | 委托、SSO | 配置复杂 | 社交登录、企业 |
---
## JWT 实现
### 生成令牌
```typescript
import jwt from 'jsonwebtoken';
function generateTokens(user: User) {
const accessToken = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: '15m' } // 短生命周期
);
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: '7d' } // 长生命周期
);
return { accessToken, refreshToken };
}
```
### 验证中间件
```typescript
function authenticate(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!);
req.user = payload;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
}
```
### 刷新令牌流程
```typescript
app.post('/api/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
try {
// 验证刷新令牌
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!);
// 检查令牌是否存在于数据库(未撤销)
const storedToken = await db.refreshTokens.findOne({
token: await hash(refreshToken),
expiresAt: { $gt: new Date() }
});
if (!storedToken) {
return res.status(403).json({ error: 'Token revoked' });
}
// 生成新访问令牌
const user = await db.users.findById(payload.userId);
const accessToken = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
res.json({ accessToken });
} catch {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
```
---
## 基于会话的认证
```typescript
import session from 'express-session';
import RedisStore from 'connect-redis';
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: 'Invalid credentials' });
}
req.session.userId = user.id;
req.session.role = user.role;
res.json({ user: { id: user.id, email: user.email } });
});
// 登出
app.post('/api/auth/logout', (req, res) => {
req.session.destroy(() => {
res.clearCookie('connect.sid');
res.json({ message: 'Logged out' });
});
});
```
---
## OAuth2 / 社交登录
```typescript
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
callbackURL: '/api/auth/google/callback'
}, async (accessToken, refreshToken, profile, done) => {
// 查找或创建用户
let user = await db.users.findOne({ googleId: profile.id });
if (!user) {
user = await db.users.create({
googleId: profile.id,
email: profile.emails?.[0]?.value,
name: profile.displayName
});
}
return done(null, user);
}));
// 路由
app.get('/api/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] }));
app.get('/api/auth/google/callback',
passport.authenticate('google', { session: false }),
(req, res) => {
const tokens = generateTokens(req.user);
res.redirect(`${FRONTEND_URL}/auth/callback?token=${tokens.accessToken}`);
});
```
---
## 授权:RBAC
```typescript
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: 'Not authenticated' });
}
if (!roles.some(role => hasRole(req.user.role, role))) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// 用法
app.delete('/api/users/:id',
authenticate,
requireRole(Role.ADMIN),
async (req, res) => {
await db.users.delete(req.params.id);
res.json({ message: 'User deleted' });
}
);
```
---
## 基于权限的访问
```typescript
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 requirePermission(...permissions: Permission[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
const hasAll = permissions.every(p =>
rolePermissions[req.user.role]?.includes(p)
);
if (!hasAll) return res.status(403).json({ error: 'Insufficient permissions' });
next();
};
}
```
---
## 资源所有权
```typescript
function requireOwnership(resourceType: 'post' | 'comment') {
return async (req: Request, res: Response, next: NextFunction) => {
if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
// 管理员可访问任何资源
if (req.user.role === Role.ADMIN) return next();
const resource = await db[resourceType].findById(req.params.id);
if (!resource) return res.status(404).json({ error: 'Not found' });
if (resource.userId !== req.user.userId) {
return res.status(403).json({ error: 'Not authorized' });
}
next();
};
}
// 用法:用户只能更新自己的帖子
app.put('/api/posts/:id', authenticate, requireOwnership('post'), updatePost);
```
---
## 密码安全
```typescript
import bcrypt from 'bcrypt';
import { z } from 'zod';
const passwordSchema = z.string()
.min(12, 'Password must be at least 12 characters')
.regex(/[A-Z]/, 'Must contain uppercase')
.regex(/[a-z]/, 'Must contain lowercase')
.regex(/[0-9]/, 'Must contain number')
.regex(/[^A-Za-z0-9]/, 'Must contain special character');
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 12); // 12 轮
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
```
---
## 最佳实践
### ✅ 应该做
- 全程使用 HTTPS
- 用 bcrypt 哈希密码(12+ 轮)
- 使用短生命周期访问令牌(15-30 分钟)
- 在数据库中存储刷新令牌(可撤销)
- 验证所有输入
- 对认证端点限速
- 记录安全事件
- 使用安全 Cookie 标志(httpOnly、secure、sameSite)
### ❌ 不要做
- 明文存储密码
- 将 JWT 存入 localStorage(易受 XSS)
- 使用弱 JWT 密钥
- 仅信任客户端认证检查
- 在错误中暴露堆栈跟踪
- 跳过服务端验证
- 忽略速率限制
---
## 常