安装相关依赖
虽然仅使用@nestjs/jwt就能实现身份验证的功能,但是使用passport能在更高层次上提供更多便利。Passport 拥有丰富的 strategies 生态系统,实现了各种身份验证机制。虽然概念简单,但你可以选择的 Passport 策略集非常丰富且种类繁多。Passport 将这些不同的步骤抽象为一个标准模式,@nestjs/passport
模块将这个模式封装并标准化为熟悉的 Nest 结构。
安装相关依赖:
npm i passport passport-jwt @nestjs/passport @nestjs/jwt
npm i --save-dev @types/passport-jwt
创建身份验证模块
我们将从生成一个 AuthModule
开始,然后在其中生成一个 AuthService
和一个 AuthController
。我们将使用 AuthService
来实现身份验证逻辑,使用 AuthController
来公开身份验证端点。
$ nest g module auth
$ nest g controller auth
$ nest g service auth
local身份验证
首先我们需要安装所需的包。Passport 提供了一种称为 passport-local 的策略,它实现了用户名/密码身份验证机制,适合我们对这部分用例的需求。安装本地验证策略依赖:
npm install --save passport-local
npm install --save-dev @types/passport-local
实现本地验证策略:
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
//指定username为account字段
super({ usernameField: 'account' });
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
在AuthService
中实现验证账户和密码的功能:
async validateUser(account: string, password: string): Promise<UserEntity> {
let user = null
if (validPhone(account)) {
// 手机登录
user = await this.userService.findOneByPhone(account)
} else if (validEmail(account)) {
// 邮箱
user = await this.userService.findOneByEmail(account)
} else {
// 帐号
user = await this.userService.findOneByAccount(account)
}
if (!user) throw new ApiException(ApiErrorCode.USER_PASSWORD_INVALID, '帐号或密码错误')
const checkPassword = await compare(password, user.password)
if (!checkPassword) throw new ApiException(ApiErrorCode.USER_PASSWORD_INVALID, '帐号或密码错误')
if (user.status === 0)
throw new ApiException(ApiErrorCode.USER_ACCOUNT_FORBIDDEN, '您已被禁用,如需正常使用请联系管理员')
return user
}
创建本地认证守卫local-auth.guard.ts,并注册到auth.module.ts:
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
我们把登录接口的实现放到AuthController,需要使用我们创建的本地验证守卫:
import { Body, Controller, Post, Req } from '@nestjs/common'
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'
import { AllowAnon } from '../common/decorators/allow-anon.decorator'
import { ApiResult } from '../common/decorators/api-result.decorator'
import { LoginUser } from './dto/login-user.dto'
import { CreateTokenDto } from './dto/create-token.dto'
import { AuthService } from './auth.service'
import { LocalAuthGuard } from 'src/common/guards/local-auth.guard'
@ApiTags('登录')
@Controller()
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@ApiOperation({ summary: '登录' })
@ApiResult(CreateTokenDto)
@UseGuards(LocalAuthGuard)
async login(@Req() req:any, @Body() loginUser:LoginUser ): Promise<CreateTokenDto> {
return await this.authService.login(req.user)
}
}
我们把登录逻辑放到AuthService
,仅生成并返回access token等数据:
async login(user: any): Promise<CreateTokenDto> {
// 生成 token
const data = this.genToken({ id: user.id })
return data
}
/**
* 生成 token 与 刷新 token
* @param payload
* @returns
*/
genToken(payload: { id: string }): CreateTokenDto {
const accessToken = `Bearer ${this.jwtService.sign(payload)}`
const refreshToken = this.jwtService.sign(payload, { expiresIn: this.config.get('jwt.refreshExpiresIn') })
return { accessToken, refreshToken }
}
jwt身份验证
要实现我们的jwt身份验证策略jwt.strategy.ts:
import { PassportStrategy } from '@nestjs/passport'
import { Strategy, ExtractJwt } from 'passport-jwt'
import { ConfigService } from '@nestjs/config'
import { UnauthorizedException, Injectable } from '@nestjs/common'
import { AuthService } from './auth.service'
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
/**
* 这里的构造函数向父类传递了授权时必要的参数,在实例化时,父类会得知授权时,客户端的请求必须使用 Authorization 作为请求头,
* 而这个请求头的内容前缀也必须为 Bearer,在解码授权令牌时,使用秘钥 secretOrKey: 'secretKey' 来将授权令牌解码为创建令牌时的 payload。
*/
constructor(private readonly authService: AuthService,
private readonly config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.get('jwt.secretkey'),
})
}
/**
* validate 方法实现了父类的抽象方法,在解密授权令牌成功后,即本次请求的授权令牌是没有过期的,
* 此时会将解密后的 payload 作为参数传递给 validate 方法,这个方法需要做具体的授权逻辑,比如这里我使用了通过用户名查找用户是否存在。
* 当用户不存在时,说明令牌有误,可能是被伪造了,此时需抛出 UnauthorizedException 未授权异常。
* 当用户存在时,会将 user 对象添加到 req 中,在之后的 req 对象中,可以使用 req.user 获取当前登录用户。
*/
async validate(payload: { id: string }) {
const user = await this.authService.findUser(payload)
// 如果用用户信息,代表 token 没有过期,没有则 token 已失效
if (!user) throw new UnauthorizedException()
return user
}
}
我们需要配置 AuthModule
以使用我们刚刚定义的 Passport 功能。将 auth.module.ts
更新为如下所示:
@Module({
controllers:[AuthController],
imports: [
UserModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (config: ConfigService) => ({
secret: config.get('jwt.secretkey'),
signOptions: {
expiresIn: config.get('jwt.expiresin'),
},
}),
inject: [ConfigService],
}),
PassportModule.register({ defaultStrategy: 'jwt' }),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [PassportModule, AuthService],
})
export class AuthModule {}
将 AuthModule 绑定到 AppModule 上后,我们可以在 controller 上使用守卫装饰器 @UseGuards(AuthGuard)进行验证。但是每个controller或者路由上都要标注一下很不方便。这时候我们可以使用全局守卫:
//app.module.ts
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
现在要求接口请求头都要带上token,但是登录注册接口是不需要token的,因此我们需要一种机制来取消接口的校验。
创建一个装饰器allow-anon.decorator.ts:
import { SetMetadata } from '@nestjs/common'
export const ALLOW_ANON = 'allowAnon'
/**
* 允许 接口 不校验 token
*/
export const AllowAnon = () => SetMetadata(ALLOW_ANON, true)
继承守卫,并重新实现canActivate,我们需要 JwtAuthGuard
在找到 "
AllowAnon"
元数据时返回 true
,否则使用内置的实现:
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(
private readonly reflector: Reflector,
//private readonly userService: UserService
) {
super()
}
canActivate(ctx: ExecutionContext) {
// 函数,类 是否允许 无 token 访问
const allowAnon = this.reflector.getAllAndOverride<boolean>(ALLOW_ANON, [
ctx.getHandler(),
ctx.getClass()
])
if (allowAnon) return true
return super.canActivate(ctx)
}
}
当然,如果你想自定义异常提示,可以在前面进行判断,并抛出自定义的异常信息:
canActivate(ctx: ExecutionContext) {
// 函数,类 是否允许 无 token 访问
const allowAnon = this.reflector.getAllAndOverride<boolean>(ALLOW_ANON, [
ctx.getHandler(),
ctx.getClass()
])
if (allowAnon) return true
const req = ctx.switchToHttp().getRequest()
const accessToken = req.get('Authorization')
if (!accessToken) throw new ForbiddenException('请先登录')
const atUserId = this.authService.verifyToken(accessToken)
if (!atUserId) throw new UnauthorizedException('当前登录已过期,请重新登录')
return super.canActivate(ctx)
}
swagger添加 jwt token
因为有些接口需要登录才能访问,所以需要在 swagger 中配置 token才能成功调用。只需要在 main.ts 再加个 addBearerAuth()函数即可。
const swaggerOptions = new DocumentBuilder()
.setTitle('Nest-Admin App')
.setDescription('Nest-Admin App 接口文档')
.setVersion('2.0.0')
.addBearerAuth()
.build()
然后在需要认证的接口加上@ApiBearerAuth()
装饰器即可,比如:
@Post('/update/token')
@ApiOperation({ summary: '刷新token' })
@ApiResult(CreateTokenDto)
@ApiBearerAuth()
async updateToken(@Req() req:any): Promise<CreateTokenDto> {
return await this.authService.updateToken(req.user.id)
}
点击Authorization
,将调用登录接口取得的 token 输入进去即可调用加了权限的接口了
调用它:
我们发现验证通过了。如果我们把token清除掉,再调用则提示我们未登录: