- JWT 引入 sid,单会话精确强退(sid 黑名单)与按用户踢全部(tokenVersion) - User 新增 lockedAt/lockReason,登录/刷新/守卫统一拒绝锁定 - sessions 模块:活跃会话列表 + 单会话强退;users 新增 lock/unlock/kick-sessions - 资源注册 + syncTenantResources 存量租户补齐脚本 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
56 lines
2.1 KiB
TypeScript
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;
|
|
}
|
|
}
|