Koa.js 零信任安全架构与身份认证实践
引言
传统的边界安全模式已经无法满足现代应用的安全需求。零信任架构(Zero Trust Architecture)提出了"永不信任,始终验证"的原则。本文将介绍如何在 Koa.js 中实现零信任安全架构和现代身份认证。
零信任架构概述
零信任的核心原则:
| 原则 | 含义 | 实现方式 |
|---|---|---|
| 永不信任 | 不基于网络位置默认信任 | 每次请求都验证身份 |
| 最小权限 | 只授予必要权限 | RBAC + ABAC |
| 微分段 | 精细化访问控制 | 资源级别权限 |
| 持续验证 | 实时评估信任度 | 动态风险评估 |
统一身份认证中间件
实现多因素认证和会话管理:
// 身份认证中间件
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
class AuthMiddleware {
constructor(config) {
this.jwtSecret = config.jwtSecret;
this_accessTokenTTL = config.accessTokenTTL || 3600;
this.refreshTokenTTL = config.refreshTokenTTL || 604800;
this.mfaEnabled = config.mfaEnabled || false;
}
// 生成令牌对
async generateTokenPair(user, factors = []) {
const accessToken = jwt.sign(
{
sub: user.id,
email: user.email,
roles: user.roles,
permissions: user.permissions,
mfa: factors,
type: 'access'
},
this.jwtSecret,
{ expiresIn: this.accessTokenTTL }
);
const refreshToken = crypto.randomBytes(32).toString('hex');
// 存储刷新令牌
await this.storeRefreshToken(user.id, refreshToken, {
issuedAt: Date.now(),
expiresAt: Date.now() + this.refreshTokenTTL * 1000,
factors
});
return { accessToken, refreshToken };
}
// 验证访问令牌
async verifyAccessToken(ctx) {
const token = this.extractToken(ctx);
if (!token) {
throw new AuthError('No token provided', 401);
}
try {
const decoded = jwt.verify(token, thisjwtSecret);
if (decoded.type !== 'access') {
throw new AuthError('Invalid token type', 401);
}
// 检查令牌是否已被撤销
const isRevoked = await this.isTokenRevoked(decoded.sub, token);
if (isRevoked) {
throw new AuthError('Token revoked', 401);
}
return decoded;
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new AuthError('Token expired', 401);
}
throw new AuthError('Invalid token', 401);
}
}
// 刷新令牌
async refreshTokens(refreshToken) {
const stored = await this.getRefreshToken(refreshToken);
if (!stored) {
throw new AuthError('Invalid refresh token', 401);
}
if (Date.now() > stored.expiresAt) {
await this.deleteRefreshToken(refreshToken);
throw new AuthError('Refresh token expired', 401);
}
// 验证用户仍存在且未被禁用
const user = await this.getUser(stored.userId);
if (!user || user.status === 'disabled') {
throw new AuthError('User not found or disabled', 401);
}
// 吊销旧刷新令牌
await this.deleteRefreshToken(refreshToken);
// 生成新令牌对
return this.generateTokenPair(user, stored.factors);
}
// 吊销所有用户令牌
async revokeAllUserTokens(userId) {
await this.revokeAllTokens(userId);
// 可以选择向用户发送安全通知
await this.sendSecurityNotification(userId, 'All sessions revoked');
}
// 从请求中提取令牌
extractToken(ctx) {
const authHeader = ctx.headers.authorization;
if (!authHeader) return null;
const [type, token] = authHeader.split(' ');
if (type !== 'Bearer' || !token) return null;
return token;
}
// 认证中间件
async authenticate(ctx, next) {
try {
const user = await this.verifyAccessToken(ctx);
ctx.state.user = user;
ctx.state.userId = user.sub;
await next();
} catch (error) {
if (error instanceof AuthError) {
ctx.status = error.statusCode;
ctx.body = { error: error.message };
} else {
ctx.status = 500;
ctx.body = { error: 'Internal server error' };
}
}
}
}
多因素认证实现
支持 TOTP 和短信验证码:
const speakeasy = require('speakeasy');
// 多因素认证服务
class MFAService {
constructor(config) {
this.digits = config.digits || 6;
this.step = config.step || 30;
}
// 生成 TOTP 密钥
generateTOTPSecret(userId) {
const secret = speakeasy.generateSecret({
name: `MyApp:${userId}`,
length: 20
});
return {
secret: secret.base32,
qrcode: secret.otpauth_url,
backupCodes: this.generateBackupCodes(8)
};
}
// 验证 TOTP 码
verifyTOTP(token, secret) {
return speakeasy.totp.verify({
secret: secret,
encoding: 'base32',
token: token,
digits: this.digits,
step: this.step,
window: 1 // 允许前后 1 个时间步
});
}
// 生成短信验证码
async sendSMSCode(phone) {
const code = Math.floor(100000 + Math.random() * 900000).toString();
// 存储验证码(带过期时间)
await this.storeVerificationCode(phone, code, {
type: 'sms',
expiresIn: 300, // 5分钟
maxAttempts: 3
});
// 发送短信(实际调用短信服务)
// await smsService.send(phone, `验证码:${code}`);
console.log(`SMS code for ${phone}: ${code}`); // 开发环境
return { sent: true, expiresIn: 300 };
}
// 验证短信验证码
async verifySMSCode(phone, code) {
const stored = await this.getVerificationCode(phone, 'sms');
if (!stored) {
return { valid: false, reason: 'Code not found' };
}
if (Date.now() > stored.expiresAt) {
return { valid: false, reason: 'Code expired' };
}
if (stored.attempts >= stored.maxAttempts) {
return { valid: false, reason: 'Too many attempts' };
}
if (stored.code !== code) {
await this.incrementAttempts(phone, 'sms');
return { valid: false, reason: 'Invalid code' };
}
// 验证成功,删除验证码
await this.deleteVerificationCode(phone, 'sms');
return { valid: true };
}
// MFA 验证中间件
requireMFA(ctx, next) {
const user = ctx.state.user;
// 如果用户设置了 MFA 但会话中没有验证
if (user.mfa && !user.mfa.verified) {
ctx.status = 403;
ctx.body = {
error: 'MFA required',
mfaTypes: user.mfa.enabledTypes
};
return;
}
return next();
}
}
// MFA 路由示例
router.post('/auth/mfa/setup', authMiddleware, async (ctx) => {
const mfaService = new MFAService({});
const secret = await mfaService.generateTOTPSecret(ctx.state.userId);
// 存储密钥(临时)
await ctx.db.tempMFA.create({
userId: ctx.state.userId,
secret: secret.secret,
backupCodes: secret.backupCodes,
expiresAt: Date.now() + 600000 // 10分钟有效
});
ctx.body = {
secret: secret.secret,
qrcode: secret.qrcode,
backupCodes: secret.backupCodes
};
});
router.post('/auth/mfa/verify', authMiddleware, async (ctx) => {
const { code, type } = ctx.request.body;
const mfaService = new MFAService({});
// 获取用户 MFA 配置
const mfaConfig = await ctx.db.userMFA.findOne({
userId: ctx.state.userId
});
let valid = false;
if (type === 'totp') {
valid = mfaService.verifyTOTP(code, mfaConfig.totpSecret);
} else if (type === 'sms') {
const result = await mfaService.verifySMSCode(mfaConfig.phone, code);
valid = result.valid;
}
if (valid) {
// 更新会话,标记 MFA 已验证
ctx.state.user.mfa = {
...ctx.state.user.mfa,
verified: true,
verifiedAt: Date.now()
};
ctx.body = { success: true };
} else {
ctx.status = 401;
ctx.body = { error: 'Invalid MFA code' };
}
});
细粒度权限控制
实现 RBAC + ABAC 权限模型:
// 权限控制服务
class PermissionService {
constructor() {
this.rolePermissions = new Map();
this.attributeRules = [];
}
// 初始化角色权限
initRolePermissions(permissions) {
for (const [role, perms] of Object.entries(permissions)) {
this.rolePermissions.set(role, perms);
}
}
// 检查权限
async checkPermission(user, resource, action) {
// 1. 角色权限检查
const hasRolePermission = this.checkRolePermission(user.roles, resource, action);
if (!hasRolePermission) {
return false;
}
// 2. 属性规则检查(ABAC)
for (const rule of this.attributeRules) {
if (this.matchResource(resource, rule.resource)) {
const allowed = await this.evaluateRule(rule, user, resource, action);
if (!allowed) {
return false;
}
}
}
return true;
}
// 角色权限检查
checkRolePermission(roles, resource, action) {
for (const role of roles) {
const permissions = this.rolePermissions.get(role) || [];
for (const perm of permissions) {
if (this.matchPermission(perm, resource, action)) {
return true;
}
}
}
return false;
}
// 权限匹配
matchPermission(perm, resource, action) {
const permParts = perm.split(':');
const [permResource, permAction] = permParts;
// 通配符匹配
if (permResource === '*') return true;
if (permResource === resource && (permAction === '*' || permAction === action)) {
return true;
}
// 前缀匹配
if (resource.startsWith(permResource + '/')) {
return permAction === '*' || permAction === action;
}
return false;
}
// ABAC 规则评估
async evaluateRule(rule, user, resource, action) {
const { conditions } = rule;
// 时间条件
if (conditions.time) {
const now = new Date();
const { start, end, daysOfWeek } = conditions.time;
if (start && now < new Date(start)) return false;
if (end && now > new Date(end)) return false;
if (daysOfWeek && !daysOfWeek.includes(now.getDay())) return false;
}
// IP 条件
if (conditions.ip) {
const userIP = user.ip;
const allowedIPs = conditions.ip;
if (!this.matchIP(userIP, allowedIPs)) {
return false;
}
}
// 资源属性条件
if (conditions.resource) {
for (const [attr, value] of Object.entries(conditions.resource)) {
if (resource[attr] !== value) {
return false;
}
}
}
// 用户属性条件
if (conditions.user) {
for (const [attr, value] of Object.entries(conditions.user)) {
if (user[attr] !== value) {
return false;
}
}
}
return true;
}
// IP 地址匹配
matchIP(ip, patterns) {
for (const pattern of patterns) {
if (pattern.includes('/')) {
// CIDR 格式
if (this.cidrMatch(ip, pattern)) return true;
} else if (pattern === ip) {
return true;
}
}
return false;
}
// 权限中间件
requirePermission(resource, action) {
return async (ctx, next) => {
const user = ctx.state.user;
if (!user) {
ctx.status = 401;
ctx.body = { error: 'Authentication required' };
return;
}
const hasPermission = await this.checkPermission(user, resource, action);
if (!hasPermission) {
ctx.status = 403;
ctx.body = { error: 'Permission denied' };
return;
}
await next();
};
}
}
// 权限配置示例
const permissionService = new PermissionService();
permissionService.initRolePermissions({
admin: [
'*:*', // 所有权限
'user:read', 'user:write', 'user:delete',
'order:read', 'order:write', 'order:delete',
'report:read', 'report:export'
],
manager: [
'order:read', 'order:write',
'customer:read', 'customer:write',
'report:read'
],
employee: [
'order:read',
'customer:read'
]
});
// 添加 ABAC 规则(只允许工作时间访问)
permissionService.attributeRules.push({
resource: 'report:*',
conditions: {
time: {
daysOfWeek: [1, 2, 3, 4, 5], // 工作日
start: '09:00',
end: '18:00'
}
}
});
// 使用
router.get('/admin/users',
permissionService.requirePermission('user', 'read'),
userController.list
);
请求安全验证
输入验证和安全检查:
const z = require('zod');
constrateLimit = require('koa-ratelimit');
// 输入验证
const schemas = {
// 用户创建
createUser: z.object({
email: z.string().email(),
password: z.string().min(8).max(100),
name: z.string().min(1).max(100),
role: z.enum(['admin', 'manager', 'employee']).optional()
}),
// 订单创建
createOrder: z.object({
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive().max(1000)
})).min(1).max(50),
shippingAddress: z.object({
street: z.string().max(200),
city: z.string().max(50),
state: z.string().max(50),
zipCode: z.string().max(20),
country: z.string().length(2)
}),
paymentMethod: z.enum(['card', 'paypal', 'bank'])
})
};
// 验证中间件
function validate(schema) {
return async (ctx, next) => {
try {
const data = schema.parse(ctx.request.body);
ctx.request.body = data;
await next();
} catch (error) {
if (error instanceof z.ZodError) {
ctx.status = 400;
ctx.body = {
error: 'Validation failed',
details: error.errors.map(e => ({
field: e.path.join('.'),
message: e.message
}))
};
return;
}
throw error;
}
};
}
// 速率限制
const rateLimiter = rateLimit({
driver: 'redis',
db: redis,
duration: 60000,
errorMessage: 'Too many requests',
id: (ctx) => ctx.ip,
max: 100,
headers: {
remaining: 'RateLimit-Remaining',
reset: 'RateLimit-Reset',
total: 'RateLimit-Total'
}
});
// CSRF 保护
const csrf = require('koa-csrf');
app.use(csrf());
// 安全头中间件
const helmet = require('koa-helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// 完整安全中间件组合
app.use(koaCompress());
app.use(helmet());
app.use(rateLimiter);
app.use(validateBody()); // 输入验证
app.use(sanitizeInput()); // XSS 防护
app.use(authMiddleware.authenticate()); // 认证
app.use(permissionService.requirePermission(resource, action)); // 权限
app.use(csrfMiddleware); // CSRF 保护
审计日志
完整的安全审计追踪:
// 审计日志服务
class AuditLogger {
constructor(options = {}) {
this.storage = options.storage || 'database';
this.retentionDays = options.retentionDays || 365;
}
// 记录审计事件
async log(ctx, event) {
const record = {
timestamp: new Date().toISOString(),
eventId: this.generateEventId(),
// 事件信息
eventType: event.type,
eventCategory: event.category,
// 操作者信息
userId: ctx.state.userId,
userEmail: ctx.state.user?.email,
userIP: ctx.ip,
userAgent: ctx.headers['user-agent'],
// 请求信息
method: ctx.method,
path: ctx.path,
query: this.sanitizeQuery(ctx.query),
requestBody: this.sanitizeBody(ctx.request.body),
// 资源信息
resourceType: event.resourceType,
resourceId: event.resourceId,
// 结果
success: event.success !== false,
errorMessage: event.error?.message,
// 风险评估
riskLevel: this.assessRisk(ctx, event),
riskFactors: this.getRiskFactors(ctx, event)
};
await this.save(record);
// 高风险事件实时告警
if (record.riskLevel === 'high') {
await this.alert(record);
}
return record;
}
// 风险评估
assessRisk(ctx, event) {
let score = 0;
// 异常时间
const hour = new Date().getHours();
if (hour < 6 || hour > 22) score += 20;
// 敏感操作
const sensitiveActions = ['delete', 'export', 'admin', '批量'];
if (sensitiveActions.some(a => event.type?.includes(a))) {
score += 30;
}
// 敏感数据访问
if (event.resourceType?.match(/user|password|credit/i)) {
score += 25;
}
// 批量操作
if (event.requestBody?.length > 10) {
score += 15;
}
// IP 异常(新 IP)
if (ctx.state.user?.lastLoginIP !== ctx.ip) {
score += 10;
}
// 评估风险等级
if (score >= 60) return 'high';
if (score >= 30) return 'medium';
return 'low';
}
// 查询审计日志
async queryAuditLogs(filters = {}) {
return await this.db.auditLogs.find(filters).sort({ timestamp: -1 });
}
}
// 审计中间件
function auditLogger(auditLogger) {
return async (ctx, next) => {
const startTime = Date.now();
await next();
const duration = Date.now() - startTime;
// 记录审计日志
await auditLogger.log(ctx, {
type: `${ctx.method}:${ctx.path}`,
category: getCategory(ctx.path),
resourceType: getResourceType(ctx.path),
resourceId: ctx.params.id,
success: ctx.status < 400,
error: ctx.status >= 400 ? new Error(ctx.body?.error) : undefined
});
};
}
最佳实践建议
- 默认安全:所有接口默认需要认证
- 最小权限:精细化的权限控制
- 持续验证:不仅在登录时验证,持续评估信任度
- 完整审计:记录所有敏感操作的审计日志
- 多层防御:认证、授权、审计多层次防护
总结
零信任安全架构在 Koa.js 中的实现要点:
- 统一认证:JWT + 刷新令牌机制
- 多因素认证:TOTP + 短信验证码
- 细粒度权限:RBAC + ABAC 组合
- 输入验证:严格的数据校验
- 安全审计:完整的操作追踪
通过零信任架构,可以显著提升应用的安全性,适应现代安全威胁。