feat(iam): 完整实现 iam 身份认证与权限服务
包含 jwt/jwks/audit/grpc、rbac、cache、redis/kafka 配置等完整实现
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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. 未命中则查 DB(role_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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user