KOA技术分享

专注 Koa.js 框架的编程知识分享

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 中的实现要点:

通过零信任架构,可以显著提升应用的安全性,适应现代安全威胁。

← 下一篇:Koa.js 响应式编程与高性能并发处理