sysmgr-nest/src/auth/jwt-auth.guard.ts
liuyun 513688a5e6 fix(security): P0 安全越权堵漏(C1/H1-H6/H10/H11/H13/M1/M4/M6/M8-M11/M22)
- 平台管理员模型:super_admin=平台超管、tenant_super_admin=租户顶层(保留码),tenant:* 仅平台可授予
- 软删改名回收唯一键 + P2002→409
- 删除角色级联清理关联 + 聚合过滤已删/停用角色资源
- tokenVersion 会话吊销(重置密码/停用/删除即时踢下线)
- data_scope self 按 userId 过滤 + 详情数据权限校验
- 组织树防环、seed 不重置密码、日志脱敏补 phone/email
- JWT 密钥强校验、pageSize 上限、DTO 枚举校验、Prisma 错误映射、授权事务化

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 16:10:00 +08:00

53 lines
2.0 KiB
TypeScript

import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { IS_PUBLIC_KEY } from './decorators.js';
import { AuthService } from './auth.service.js';
export interface AuthUser {
userId: string;
tenantCode: string;
username: string;
}
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private readonly jwt: JwtService,
private readonly reflector: Reflector,
private readonly auth: AuthService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
const request = context.switchToHttp().getRequest();
const url: string = request.originalUrl ?? request.url ?? '';
if (url.startsWith('/docs')) return true;
const token = this.extractToken(request);
if (!token) throw new UnauthorizedException('未登录');
try {
const payload = await this.jwt.verifyAsync<{ sub: string; tenantCode: string; username: string; ver?: number }>(token);
const active = await this.auth.checkUserActive(payload.tenantCode, payload.sub, payload.ver ?? 0);
if (!active) throw new UnauthorizedException('账号已停用或登录已失效');
request.user = { userId: payload.sub, tenantCode: payload.tenantCode, username: payload.username } satisfies AuthUser;
return true;
} catch (e) {
if (e instanceof UnauthorizedException) throw e;
throw new UnauthorizedException('token 无效或已过期');
}
}
private extractToken(request: { headers?: Record<string, string | undefined> }): string | undefined {
const auth = request.headers?.authorization;
if (!auth) return undefined;
const [type, token] = auth.split(' ');
return type === 'Bearer' && token ? token : undefined;
}
}