Loading...
Loading...
设计并实现认证与授权系统。适用于搭建用户登录、JWT令牌、OAuth、会话管理或基于角色的访问控制场景。涵盖密码安全、令牌管理、SSO集成等内容。
npx skill4agent add supercent-io/skills-template authentication-setup请帮我搭建用户认证系统:
- 认证方式: JWT
- 框架: Express.js + TypeScript
- 数据库: PostgreSQL
- MFA: 支持Google Authenticator
- 社交登录: Google, GitHub
- Refresh Token: 使用CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255), -- 仅使用OAuth时为NULL
role VARCHAR(50) DEFAULT 'user',
is_verified BOOLEAN DEFAULT false,
mfa_secret VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
token VARCHAR(500) UNIQUE NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
export async function hashPassword(password: string): Promise<string> {
// 密码强度验证
if (password.length < 8) {
throw new Error('密码长度至少为8位');
}
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumber = /\d/.test(password);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(password);
if (!hasUpperCase || !hasLowerCase || !hasNumber || !hasSpecial) {
throw new Error('密码必须包含大写字母、小写字母、数字和特殊字符');
}
return await bcrypt.hash(password, SALT_ROUNDS);
}
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
return await bcrypt.compare(password, hash);
}import jwt from 'jsonwebtoken';
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET!;
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET!;
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';
interface TokenPayload {
userId: string;
email: string;
role: string;
}
export function generateAccessToken(payload: TokenPayload): string {
return jwt.sign(payload, ACCESS_TOKEN_SECRET, {
expiresIn: ACCESS_TOKEN_EXPIRY,
issuer: 'your-app-name',
audience: 'your-app-users'
});
}
export function generateRefreshToken(payload: TokenPayload): string {
return jwt.sign(payload, REFRESH_TOKEN_SECRET, {
expiresIn: REFRESH_TOKEN_EXPIRY,
issuer: 'your-app-name',
audience: 'your-app-users'
});
}
export function verifyAccessToken(token: string): TokenPayload {
return jwt.verify(token, ACCESS_TOKEN_SECRET, {
issuer: 'your-app-name',
audience: 'your-app-users'
}) as TokenPayload;
}
export function verifyRefreshToken(token: string): TokenPayload {
return jwt.verify(token, REFRESH_TOKEN_SECRET, {
issuer: 'your-app-name',
audience: 'your-app-users'
}) as TokenPayload;
}import { Request, Response, NextFunction } from 'express';
import { verifyAccessToken } from './jwt';
export interface AuthRequest extends Request {
user?: {
userId: string;
email: string;
role: string;
};
}
export function authenticateToken(req: AuthRequest, res: Response, next: NextFunction) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ error: '需要Access Token' });
}
try {
const payload = verifyAccessToken(token);
req.user = payload;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: '令牌已过期' });
}
return res.status(403).json({ error: '无效令牌' });
}
}
// 基于角色的授权中间件
export function requireRole(...roles: string[]) {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: '需要认证' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: '权限不足' });
}
next();
};
}import express from 'express';
import { hashPassword, verifyPassword } from './password';
import { generateAccessToken, generateRefreshToken, verifyRefreshToken } from './jwt';
import { authenticateToken } from './middleware';
const router = express.Router();
// 用户注册
router.post('/register', async (req, res) => {
try {
const { email, password } = req.body;
// 检查邮箱是否已存在
const existingUser = await db.user.findUnique({ where: { email } });
if (existingUser) {
return res.status(409).json({ error: '该邮箱已被注册' });
}
// 密码哈希
const passwordHash = await hashPassword(password);
// 创建用户
const user = await db.user.create({
data: { email, password_hash: passwordHash, role: 'user' }
});
// 生成令牌
const accessToken = generateAccessToken({
userId: user.id,
email: user.email,
role: user.role
});
const refreshToken = generateRefreshToken({
userId: user.id,
email: user.email,
role: user.role
});
// 存储Refresh Token到数据库
await db.refreshToken.create({
data: {
user_id: user.id,
token: refreshToken,
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7天
}
});
res.status(201).json({
user: { id: user.id, email: user.email, role: user.role },
accessToken,
refreshToken
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 用户登录
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// 查询用户
const user = await db.user.findUnique({ where: { email } });
if (!user || !user.password_hash) {
return res.status(401).json({ error: '凭证无效' });
}
// 验证密码
const isValid = await verifyPassword(password, user.password_hash);
if (!isValid) {
return res.status(401).json({ error: '凭证无效' });
}
// 生成令牌
const accessToken = generateAccessToken({
userId: user.id,
email: user.email,
role: user.role
});
const refreshToken = generateRefreshToken({
userId: user.id,
email: user.email,
role: user.role
});
// 存储Refresh Token
await db.refreshToken.create({
data: {
user_id: user.id,
token: refreshToken,
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
}
});
res.json({
user: { id: user.id, email: user.email, role: user.role },
accessToken,
refreshToken
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 令牌刷新
router.post('/refresh', async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: '需要Refresh Token' });
}
// 验证Refresh Token
const payload = verifyRefreshToken(refreshToken);
// 检查数据库中的令牌
const storedToken = await db.refreshToken.findUnique({
where: { token: refreshToken }
});
if (!storedToken || storedToken.expires_at < new Date()) {
return res.status(403).json({ error: 'Refresh Token无效或已过期' });
}
// 生成新的Access Token
const accessToken = generateAccessToken({
userId: payload.userId,
email: payload.email,
role: payload.role
});
res.json({ accessToken });
} catch (error) {
res.status(403).json({ error: '无效的Refresh Token' });
}
});
// 获取当前用户信息
router.get('/me', authenticateToken, async (req: AuthRequest, res) => {
try {
const user = await db.user.findUnique({
where: { id: req.user!.userId },
select: { id: true, email: true, role: true, created_at: true }
});
res.json({ user });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
export default router;项目目录/
├── src/
│ ├── auth/
│ │ ├── password.ts # 密码哈希/验证
│ │ ├── jwt.ts # JWT令牌生成/验证
│ │ ├── middleware.ts # 认证中间件
│ │ └── routes.ts # 认证API端点
│ ├── models/
│ │ └── User.ts # 用户模型
│ └── database/
│ └── schema.sql # 数据库架构
├── .env.example # 环境变量模板
└── README.md # 认证系统文档# JWT密钥(生产环境必须修改)
ACCESS_TOKEN_SECRET=your-access-token-secret-min-32-characters
REFRESH_TOKEN_SECRET=your-refresh-token-secret-min-32-characters
# 数据库
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
# OAuth(可选)
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret请为我的Express.js应用添加JWT认证,使用PostgreSQL数据库,
Access Token过期时间设为15分钟,Refresh Token设为7天。npm install jsonwebtoken bcrypt pg
npm install --save-dev @types/jsonwebtoken @types/bcryptACCESS_TOKEN_SECRET=$(openssl rand -base64 32)
REFRESH_TOKEN_SECRET=$(openssl rand -base64 32)import authRoutes from './auth/routes';
app.use('/api/auth', authRoutes);请创建仅管理员可访问的API,
普通用户访问时需返回403错误。// 管理员专用API
router.delete('/users/:id',
authenticateToken, // 验证认证
requireRole('admin'), // 验证角色
async (req, res) => {
// 用户删除逻辑
await db.user.delete({ where: { id: req.params.id } });
res.json({ message: '用户已删除' });
}
);
// 使用示例
// 普通用户(role: 'user')请求 → 403 Forbidden
// 管理员用户(role: 'admin')请求 → 200 OKACCESS_TOKEN_SECRETREFRESH_TOKEN_SECRETdotenvimport cors from 'cors';
app.use(cors({
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
credentials: true
}));#authentication#authorization#JWT#OAuth#security#backend