sysmgr-nest/src/auth/jwt-auth.guard.ts
liuyun 86832ce32d feat(session): 在线会话强踢 + 账号锁定/解锁
- JWT 引入 sid,单会话精确强退(sid 黑名单)与按用户踢全部(tokenVersion)
- User 新增 lockedAt/lockReason,登录/刷新/守卫统一拒绝锁定
- sessions 模块:活跃会话列表 + 单会话强退;users 新增 lock/unlock/kick-sessions
- 资源注册 + syncTenantResources 存量租户补齐脚本

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

56 lines
2.1 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; sid?: string }>(token);
const active = await this.auth.checkUserActive(payload.tenantCode, payload.sub, payload.ver ?? 0);
if (!active) throw new UnauthorizedException('账号已停用或登录已失效');
if (payload.sid && (await this.auth.isSessionRevoked(payload.sid))) {
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;
}
}