diff --git a/src/modules/core/helpers/app.ts b/src/modules/core/helpers/app.ts index 454a7cb..810f5e9 100644 --- a/src/modules/core/helpers/app.ts +++ b/src/modules/core/helpers/app.ts @@ -1,9 +1,9 @@ import { BadGatewayException, Global, Module, ModuleMetadata, Type } from '@nestjs/common'; -import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; +import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; import { useContainer } from 'class-validator'; -import { omit } from 'lodash'; +import { isNil, omit } from 'lodash'; import { ConfigModule } from '@/modules/config/config.module'; import { Configure } from '@/modules/config/configure'; @@ -85,6 +85,13 @@ export async function createBootModule( }); } + if (!isNil(globals.guard)) { + providers.push({ + provide: APP_GUARD, + useClass: globals.guard, + }); + } + return CreateModule('BootModule', () => ({ imports, providers, diff --git a/src/modules/core/types.ts b/src/modules/core/types.ts index 989a788..4c81f59 100644 --- a/src/modules/core/types.ts +++ b/src/modules/core/types.ts @@ -1,4 +1,5 @@ import { ModuleMetadata, PipeTransform, Type } from '@nestjs/common'; +import { IAuthGuard } from '@nestjs/passport'; import { NestFastifyApplication } from '@nestjs/platform-fastify'; import dayjs from 'dayjs'; @@ -28,6 +29,11 @@ export interface CreateOptions { interceptor?: Type | null; filter?: Type | null; + + /** + * 全局守卫 + */ + guard?: Type; }; providers?: ModuleMetadata['providers']; diff --git a/src/modules/user/constants.ts b/src/modules/user/constants.ts index ea36d53..5cad298 100644 --- a/src/modules/user/constants.ts +++ b/src/modules/user/constants.ts @@ -38,3 +38,5 @@ export const TokenConst = { USER_REFRESH_TOKEN_EXPIRED: 'USER_REFRESH_TOKEN_EXPIRED', DEFAULT_USER_REFRESH_TOKEN_EXPIRED: 'my-refresh-secret', }; + +export const ALLOW_GUEST = 'allowGuest'; diff --git a/src/modules/user/decorators/guest.decorator.ts b/src/modules/user/decorators/guest.decorator.ts new file mode 100644 index 0000000..ccb9885 --- /dev/null +++ b/src/modules/user/decorators/guest.decorator.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from '@nestjs/common'; + +import { ALLOW_GUEST } from '@/modules/user/constants'; + +export const Guest = () => SetMetadata(ALLOW_GUEST, true); diff --git a/src/modules/user/decorators/user.request.decorator.ts b/src/modules/user/decorators/user.request.decorator.ts new file mode 100644 index 0000000..350f317 --- /dev/null +++ b/src/modules/user/decorators/user.request.decorator.ts @@ -0,0 +1,8 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +import { UserEntity } from '../entities/UserEntity'; + +export const RequestUser = createParamDecorator(async (_data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return request.user as ClassToPlain; +}); diff --git a/src/modules/user/guards/jwt.auth.guard.ts b/src/modules/user/guards/jwt.auth.guard.ts new file mode 100644 index 0000000..1f783af --- /dev/null +++ b/src/modules/user/guards/jwt.auth.guard.ts @@ -0,0 +1,82 @@ +import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; + +import { isNil } from 'lodash'; + +import { ExtractJwt } from 'passport-jwt'; + +import { TokenService } from '@/modules/user/services/token.service'; + +import { ALLOW_GUEST } from '../constants'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') { + constructor( + protected ref: Reflector, + protected tokenService: TokenService, + ) { + super(); + } + + async canActivate(context: ExecutionContext) { + const allowGuest = this.ref.getAllAndOverride(ALLOW_GUEST, [ + context.getHandler(), + context.getClass(), + ]); + if (allowGuest) { + return true; + } + const request = this.getRequest(context); + + const requestToken = ExtractJwt.fromAuthHeaderAsBearerToken()(request); + if (isNil(requestToken)) { + throw new UnauthorizedException(); + } + + const accessToken = await this.tokenService.checkAccessToken(requestToken); + if (isNil(accessToken)) { + throw new UnauthorizedException(); + } + + try { + return (await super.canActivate(context)) as boolean; + } catch { + const response = this.getResponse(context); + const token = await this.tokenService.refreshToken(accessToken, response); + if (isNil(token)) { + throw new UnauthorizedException(); + } + if (token.accessToken) { + request.headers.authorization = `Bearer ${token.accessToken.value}`; + } + return (await super.canActivate(context)) as boolean; + } + } + + /** + * 自动请求处理 + * 如果请求中有错误则抛出错误 + * 如果请求中没有用户信息则抛出401异常 + * @param err + * @param user + * @param _info + */ + handleRequest(err: any, user: any, _info: any) { + if (err || isNil(user)) { + if (isNil(user)) { + throw new UnauthorizedException(); + } + throw err; + } + return user; + } + + getRequest(context: ExecutionContext) { + return context.switchToHttp().getRequest(); + } + + getResponse(context: ExecutionContext) { + return context.switchToHttp().getResponse(); + } +} diff --git a/src/options.ts b/src/options.ts index c909b44..a563394 100644 --- a/src/options.ts +++ b/src/options.ts @@ -16,6 +16,7 @@ import { MeiliModule } from './modules/meilisearch/meili.module'; import { Restful } from './modules/restful/restful'; import { RestfulModule } from './modules/restful/restful.module'; import { ApiConfig } from './modules/restful/types'; +import { JwtAuthGuard } from './modules/user/guards/jwt.auth.guard'; export const createOptions: CreateOptions = { commands: () => [...Object.values(dbCommands)], @@ -26,7 +27,7 @@ export const createOptions: CreateOptions = { await RestfulModule.forRoot(configure), await ContentModule.forRoot(configure), ], - globals: {}, + globals: { guard: JwtAuthGuard }, builder: async ({ configure, BootModule }) => { const container = await NestFactory.create( BootModule,