diff --git a/src/modules/user/constants.ts b/src/modules/user/constants.ts index e316394..ea36d53 100644 --- a/src/modules/user/constants.ts +++ b/src/modules/user/constants.ts @@ -31,3 +31,10 @@ export enum UserOrderType { CREATED = 'createdAt', UPDATED = 'updatedAt', } + +export const TokenConst = { + USER_TOKEN_SECRET: 'USER_TOKEN_SECRET', + DEFAULT_USER_TOKEN_SECRET: 'my-access-secret', + USER_REFRESH_TOKEN_EXPIRED: 'USER_REFRESH_TOKEN_EXPIRED', + DEFAULT_USER_REFRESH_TOKEN_EXPIRED: 'my-refresh-secret', +}; diff --git a/src/modules/user/guards/local.auth.guard.ts b/src/modules/user/guards/local.auth.guard.ts new file mode 100644 index 0000000..1d596bb --- /dev/null +++ b/src/modules/user/guards/local.auth.guard.ts @@ -0,0 +1,28 @@ +import { BadGatewayException, ExecutionContext, Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { plainToClass } from 'class-transformer'; + +import { validateOrReject } from 'class-validator'; + +import { CredentialDto } from '../dtos/auth.dto'; + +/** + * 用户登录守卫 + */ +@Injectable() +export class LocalAuthGuard extends AuthGuard('local') { + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + try { + await validateOrReject(plainToClass(CredentialDto, request.body), { + validationError: { target: false }, + }); + } catch (error) { + const messages = (error as any[]) + .map((e) => e.constraints ?? {}) + .reduce((o, n) => ({ ...o, ...n }), {}); + throw new BadGatewayException(Object.values(messages)); + } + return super.canActivate(context) as boolean; + } +} diff --git a/src/modules/user/services/token.service.ts b/src/modules/user/services/token.service.ts index 4f3f110..ff47e8b 100644 --- a/src/modules/user/services/token.service.ts +++ b/src/modules/user/services/token.service.ts @@ -16,6 +16,8 @@ import { AccessTokenEntity } from '@/modules/user/entities/access.token.entity'; import { RefreshTokenEntity } from '@/modules/user/entities/refresh.token.entity'; import { JwtConfig, JwtPayload, UserConfig } from '@/modules/user/types'; +import { TokenConst } from '../constants'; + /** * 令牌服务 */ @@ -85,7 +87,10 @@ export class TokenService { const refreshToken = new RefreshTokenEntity(); refreshToken.value = jwt.sign( refreshTokenPayload, - this.configure.env.get('USER_REFRESH_TOKEN_EXPIRED', 'my-refresh-secret'), + this.configure.env.get( + TokenConst.USER_REFRESH_TOKEN_EXPIRED, + TokenConst.DEFAULT_USER_REFRESH_TOKEN_EXPIRED, + ), ); refreshToken.expiredAt = now.add(config.refreshTokenExpired, 'second').toDate(); refreshToken.accessToken = accessToken; @@ -136,7 +141,10 @@ export class TokenService { async verifyAccessToken(token: AccessTokenEntity) { const result = jwt.verify( token.value, - this.configure.env.get('USER_TOKEN_SECRET', 'my-access-secret'), + this.configure.env.get( + TokenConst.USER_TOKEN_SECRET, + TokenConst.DEFAULT_USER_TOKEN_SECRET, + ), ); if (result) { return token.user; @@ -152,7 +160,10 @@ export class TokenService { defaultUserConfig(configure), ); const options: JwtModuleOptions = { - secret: configure.env.get('USER_TOKEN_SECRET', 'my-access-secret'), + secret: configure.env.get( + TokenConst.USER_TOKEN_SECRET, + TokenConst.DEFAULT_USER_TOKEN_SECRET, + ), verifyOptions: { ignoreExpiration: !configure.env.isProd(), }, diff --git a/src/modules/user/strategies/jwt.strategy.ts b/src/modules/user/strategies/jwt.strategy.ts new file mode 100644 index 0000000..fe26c94 --- /dev/null +++ b/src/modules/user/strategies/jwt.strategy.ts @@ -0,0 +1,37 @@ +import { PassportStrategy } from '@nestjs/passport'; + +import { instanceToPlain } from 'class-transformer'; +import { ExtractJwt, Strategy } from 'passport-jwt'; + +import { Configure } from '@/modules/config/configure'; + +import { TokenConst } from '../constants'; +import { UserRepository } from '../repositories/UserRepository'; +import { JwtPayload } from '../types'; + +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + protected configure: Configure, + protected userRepository: UserRepository, + ) { + const secret = configure.env.get( + TokenConst.USER_TOKEN_SECRET, + TokenConst.DEFAULT_USER_TOKEN_SECRET, + ); + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: secret, + }); + } + + /** + * 通过荷载解析出用户ID + * 通过用户ID查询出用户是否存在,并把id放入request方便后续操作 + * @param payload + */ + async validate(payload: JwtPayload) { + const user = await this.userRepository.findOneOrFail({ where: { id: payload.sub } }); + return instanceToPlain(user); + } +} diff --git a/src/modules/user/strategies/local.strategy.ts b/src/modules/user/strategies/local.strategy.ts new file mode 100644 index 0000000..c5059f7 --- /dev/null +++ b/src/modules/user/strategies/local.strategy.ts @@ -0,0 +1,23 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-local'; + +import { AuthService } from '../services/auth.service'; + +/** + * 用户认证本地策略 + */ +@Injectable() +export class LocalStrategy extends PassportStrategy(Strategy) { + constructor(protected authService: AuthService) { + super({ usernameField: 'credential', passwordField: 'password' }); + } + + async validate(credential: string, password: string): Promise { + const user = await this.authService.validateUser(credential, password); + if (!user) { + throw new UnauthorizedException(); + } + return user; + } +}