认证授权
认证授权与传统后端完全对齐:令牌由传统后端认证服务签发,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 提取令牌:
- 统一 Guard
- 全局注册
- 身份取用
// 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;
}
}
// app.module.ts
@Module({
imports: [AuthModule],
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard },
],
})
export class AppModule {}
全局注册后,所有 REST 端点、WS 事件、GraphQL 查询默认受保护;公开端点用自定义装饰器豁免:
// common/decorators/public.decorator.ts
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// 在 Guard 开头判断
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
// common/decorators/current-user.decorator.ts
export const CurrentUser = createParamDecorator(
(data: keyof AuthPayload, context: ExecutionContext) => {
const type = context.getType<'http' | 'ws' | 'graphql'>();
if (type === 'graphql') {
return GqlExecutionContext.create(context).getContext().req.user;
}
if (type === 'ws') return context.switchToWs().getClient().data.user;
return context.switchToHttp().getRequest().user;
},
);
// Resolver / Controller / Gateway 中统一用法
@Query(() => Dashboard)
dashboard(@CurrentUser() user: AuthPayload) { ... }
@SubscribeMessage('notification:subscribe')
handle(@CurrentUser() user: AuthPayload) { ... }
权限判定
角色与数据权限在 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())并携带新令牌 - 服务端对敏感事件(如加入新租户房间)二次校验令牌有效期
参见
- 接口设计规范 · 鉴权约定
- WebSocket · 连接鉴权
- SSE · 客户端(原生 EventSource 不支持请求头,需
@microsoft/fetch-event-source传递令牌)