feat(iam): 完整实现 iam 身份认证与权限服务

包含 jwt/jwks/audit/grpc、rbac、cache、redis/kafka 配置等完整实现
This commit is contained in:
SpecialX
2026-07-10 19:09:39 +08:00
parent 06e0f9139b
commit a35e759d64
32 changed files with 2589 additions and 778 deletions

View File

@@ -5,20 +5,27 @@ import {
} from "@nestjs/common";
import type { Request, Response, NextFunction } from "express";
/**
* 已认证请求:由 AuthMiddleware 从 Gateway 注入的 x-user-* 头部解析。
* 公开端点login/register/refresh/jwks/health不走此中间件。
*/
export interface AuthenticatedRequest extends Request {
userId?: string;
userRoles?: string[];
userDataScope?: string;
}
@Injectable()
export class AuthMiddleware implements NestMiddleware {
use(req: AuthenticatedRequest, res: Response, next: NextFunction): void {
// 从 Gateway 注入的头部读取用户信息
use(req: AuthenticatedRequest, _res: Response, next: NextFunction): void {
const userIdHeader = req.headers["x-user-id"];
const userId = typeof userIdHeader === "string" ? userIdHeader : undefined;
const rolesHeaderRaw = req.headers["x-user-roles"];
const rolesHeader =
typeof rolesHeaderRaw === "string" ? rolesHeaderRaw : undefined;
const dataScopeHeaderRaw = req.headers["x-user-data-scope"];
const dataScopeHeader =
typeof dataScopeHeaderRaw === "string" ? dataScopeHeaderRaw : undefined;
if (!userId) {
throw new UnauthorizedException("Missing x-user-id header");
@@ -26,6 +33,7 @@ export class AuthMiddleware implements NestMiddleware {
req.userId = userId;
req.userRoles = rolesHeader ? rolesHeader.split(",") : [];
req.userDataScope = dataScopeHeader ?? "self";
next();
}
}

View File

@@ -6,43 +6,50 @@ import {
} from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { PermissionDeniedError } from "../shared/errors/application-error.js";
import { PermissionCacheService } from "../shared/cache/permission-cache.service.js";
import { IamRepository } from "../iam/iam.repository.js";
import type { AuthenticatedRequest } from "./auth.middleware.js";
export type Permission =
| "IAM_USER_CREATE"
| "IAM_USER_READ"
| "IAM_USER_UPDATE"
| "IAM_USER_DELETE"
| "IAM_ROLE_MANAGE";
/**
* 权限点常量(对齐 iam-init.sql 种子数据)。
*
* 权限名格式:`<resource>:<action>`(如 `iam:user:read`)。
* DB 中存储的权限名与 controller 装饰器声明的权限名一一对应。
*/
export const Permissions = {
IAM_USER_CREATE: "IAM_USER_CREATE" as const,
IAM_USER_READ: "IAM_USER_READ" as const,
IAM_USER_UPDATE: "IAM_USER_UPDATE" as const,
IAM_USER_DELETE: "IAM_USER_DELETE" as const,
IAM_ROLE_MANAGE: "IAM_ROLE_MANAGE" as const,
};
IAM_USER_READ: "iam:user:read",
IAM_USER_MANAGE: "iam:user:manage",
IAM_ROLE_MANAGE: "iam:role:manage",
IAM_AUDIT_READ: "iam:audit:read",
IAM_VIEWPORT_READ: "iam:viewport:read",
} as const;
export type Permission = (typeof Permissions)[keyof typeof Permissions];
export const PERMISSIONS_KEY = "permissions";
export const RequirePermission = (...permissions: Permission[]) =>
SetMetadata(PERMISSIONS_KEY, permissions);
const ROLE_PERMISSIONS: Record<string, Permission[]> = {
admin: [
Permissions.IAM_USER_CREATE,
Permissions.IAM_USER_READ,
Permissions.IAM_USER_UPDATE,
Permissions.IAM_USER_DELETE,
Permissions.IAM_ROLE_MANAGE,
],
teacher: [Permissions.IAM_USER_READ],
};
/**
* 权限守卫I3 裁决DB 驱动 + Redis 缓存)。
*
* 校验流程:
* 1. 从 req.userId 获取当前用户(由 AuthMiddleware 注入)
* 2. 先查 Redis 缓存TTL 5min
* 3. 未命中则查 DBrole_permissions JOIN并回填缓存
* 4. 检查用户权限列表是否包含所需权限
*
* admin 角色拥有全部权限data_scope=all 时跳过 DB 查询直接放行)。
*/
@Injectable()
export class PermissionGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
constructor(
private readonly reflector: Reflector,
private readonly permissionCache: PermissionCacheService,
private readonly iamRepository: IamRepository,
) {}
canActivate(context: ExecutionContext): boolean {
async canActivate(context: ExecutionContext): Promise<boolean> {
if (process.env.DEV_MODE === "true") {
return true;
}
@@ -57,15 +64,33 @@ export class PermissionGuard implements CanActivate {
}
const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
const roles = request.userRoles ?? [];
for (const role of roles) {
const perms = ROLE_PERMISSIONS[role];
if (perms && requiredPermissions.some((p) => perms.includes(p))) {
return true;
}
const userId = request.userId;
if (!userId) {
throw new PermissionDeniedError("missing user identity");
}
throw new PermissionDeniedError(requiredPermissions.join(", "));
// data_scope=all 的用户admin直接放行
if (request.userDataScope === "all") {
return true;
}
const userPermissions = await this.loadPermissions(userId);
const hasPermission = requiredPermissions.some((p) =>
userPermissions.includes(p),
);
if (!hasPermission) {
throw new PermissionDeniedError(requiredPermissions.join(", "));
}
return true;
}
private async loadPermissions(userId: string): Promise<string[]> {
const cached = await this.permissionCache.getPermissions(userId);
if (cached) return cached;
const perms = await this.iamRepository.getUserPermissions(userId);
const names = perms.map((p) => p.name);
await this.permissionCache.setPermissions(userId, names);
return names;
}
}