跳到主要内容

认证授权

认证授权与传统后端完全对齐:令牌由传统后端认证服务签发,Node.js 服务只做校验与权限判定,不签发、不刷新、不存储用户凭证。

模型​

  • 签发方唯一:仅 Spring 认证服务可签发 JWT
  • 校验方式:非对称算法(ES256),Node.js 以公钥验签(环境变量注入 PEM),无私钥泄露面
  • 令牌内容:sub(用户 ID)、tenantId(租户)、roles(角色)、exp(过期时间)
注意

Node.js 服务禁止实现登录、注册、令牌刷新端点;认证入口统一在传统后端。

JWT 校验配置​

pnpm add @nestjs/jwt
// common/auth/auth.module.ts
import { JwtModule } from '@nestjs/jwt';

@Global()
@Module({
imports: [
JwtModule.registerAsync({
useFactory: (config: ConfigService) => ({
publicKey: config.get<string>('JWT_PUBLIC_KEY')?.replaceAll(String.raw`\n`, '\n'),
verifyOptions: { algorithms: ['ES256'] },
}),
inject: [ConfigService],
}),
],
exports: [JwtService],
})
export class AuthModule {}
  • 只配置 publicKey、不配置 secret / privateKey:服务仅具备验签能力,无法签发令牌
  • algorithms: ['ES256'] 显式锁定验签算法,拒绝令牌自声明的算法(防 alg 混淆攻击)
  • 公钥经环境变量注入(PEM,换行 \n 转义)
  • 公钥未配置时认证禁用(Guard 注入匿名身份)——仅限本地开发,生产环境必须注入

四类入口统一鉴权​

同一套 Guard 逻辑适配 HTTP、WebSocket、GraphQL 三种上下文(REST 与 SSE 同属 HTTP 上下文,共四类入口),通过 GqlExecutionContext 与 WsExecutionContext 提取令牌:

// common/guards/jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly config: ConfigService,
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const type = context.getType<'http' | 'ws' | 'graphql'>();
const target = this.getRequest(context);
const token = this.extractToken(target, type);

let payload: AuthPayload;
if (this.config.get<string>('JWT_PUBLIC_KEY') === undefined) {
payload = ANONYMOUS_USER;
} else {
try {
payload = await this.jwtService.verifyAsync<AuthPayload>(token);
} catch {
throw new UnauthorizedException({ code: 40101, message: '令牌无效或已过期' });
}
}

if (type === 'ws') target.data.user = payload;
else target.user = payload;
return true;
}

private getRequest(context: ExecutionContext) {
const type = context.getType<'http' | 'ws' | 'graphql'>();
if (type === 'graphql') return GqlExecutionContext.create(context).getContext().req;
if (type === 'ws') return context.switchToWs().getClient();
return context.switchToHttp().getRequest();
}

private extractToken(target: any, type: string): string | undefined {
if (type === 'ws') return target.handshake?.auth?.token;
const header = target.headers?.authorization;
return header?.startsWith('Bearer ') ? header.slice(7) : undefined;
}
}

权限判定​

角色与数据权限在 JWT payload 中声明,Node.js 服务只做消费方判定:

// common/guards/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
const required = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!required) return true;
const { user } = this.getRequest(context);
return required.some((role) => user.roles?.includes(role));
}
}
  • 角色定义与分配归传统后端,Node.js 不维护角色表
  • 数据级权限(如“仅本租户”)在 service 层用 tenantId 过滤,不依赖前端传参

WebSocket 连接续期​

长连接场景下,令牌过期不强制断开,但敏感操作需重新校验:

  • 连接建立时校验令牌(handleConnection)
  • 连接期间,客户端在令牌过期前主动重连(socket.disconnect().connect())并携带新令牌
  • 服务端对敏感事件(如加入新租户房间)二次校验令牌有效期
参见