Nestjs 实现passport策略jwt鉴权(全局鉴权和白名单)
前言
Nestjs文档中有两种鉴权方式,一种是自定义一个守卫,在守卫中自己从上下文header中取出token信息,然后自己解析判定。
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { Request } from 'express';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(
token,
{
secret: jwtConstants.secret
}
);
// 💡 在这里我们将 payload 挂载到请求对象上
// 以便我们可以在路由处理器中访问它
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
然后官方又引出了一个passport
集成,本次文章也是主要讲解passport这个东西。
什么是passport
passport 是一个用于Express框架的身份验证中间件,由于没看它的具体实现,个人猜测应该使用了策略模式,它会有一个接口约束所有策略的实现,并且要求在验证成功后返回一个用户信息数据,然后这个数据会被赋值到express的request
对象上去,且属性固定为user
。
然后他会有很多策略,google、facebook、weixin等等,而我们本次需要的只是jwt策略。
安装依赖
pnpm i @nestjs/passport passport passport-jwt
pnpm i @types/passport-jwt -D
实现一个jwt策略
创建一个jwt.strategy.ts
文件,我是把这个文件放在了utils/passport
目录中。
import { Strategy, ExtractJwt } from "passport-jwt";
import type { StrategyOptions } from "passport-jwt";
import { PassportStrategy } from "@nestjs/passport";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, "jwt") {
constructor(
private readonly config: ConfigService,
private readonly prisma: PrismaService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.get("TOKEN_SECRET")
} as StrategyOptions);
}
/** token解析成功后通过sub(id)查询用户并返回 */
async validate({ sub }) {
return await this.prisma.user.findUnique({
where: {
id: sub
}
});
}
}
我们创建一个JwtStrategy
类,继承PassportStrategy(Strategy, "jwt")
,其中"jwt"
是用于命名的,后续通过AuthGuard("jwt")
来使用。
既然是继承,在构造函数中就必须调用super()
,传入两个参数:
一个是用于获取token信息的方法,这个方法passport-jwt
已经提供了,它会获取头信息中Authorization
字段,然后拿到里面的token,这个token配置被称为Bearer Token,是http规范中定义的一个属性,当然如果有的人不用也不是不行,如果不是这种就需要自己写一个函数去实现这个功能了。
查看jwtFromRequest
这个属性的类型:
export interface StrategyOptions {
...
jwtFromRequest: JwtFromRequestFunction;
...
}
export interface JwtFromRequestFunction {
(req: express.Request): string | null;
}
可以看到接受了一个参数,参数就是express.Request
对象,具体自定义怎么实现这个函数不细搞了,自己百度都有现成的。
第二个参数就是生成token用到的密匙,这个我们一样从ConfigService
中取值。
当token被拿到后,Passport就会解析它,解析成功后调用validate
方法,把token解析出的对象传参给它,此时我们需要做的就是返回查询到的用户对象,这个也是Passport所规范的,所有的策略都需要返回内容,至于这个内容是不是必须是用户信息,这个就取决于你需要使用什么信息,最终这个数据会被赋值到request的user属性上。
至此我们策略实现完成。
实现一个白名单功能(公共路由装饰器)
如果我们需要实现一个全局的jwt守卫,就表示所有的路由都需要鉴权,但是明显是不可能的,肯定会有访客接口的,虽然不多,所以我们要实现一个白名单功能给部分路由使用。
实现思路:我们给对象添加一个元数据,如果存在这个元数据就表示它是一个白名单,在安全守卫中就直接放行。
这个其实官方也有提供给我们,文档:安全守卫
我们创建一个文件:public.ts
,我是存放在utils/decorators
目录下。
import { SetMetadata } from "@nestjs/common";
export const IS_PUBLIC_KEY = "isPublic";
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
然后在我们在不需要鉴权的路径中添加:
import { Body, Controller, Get, Post, Request } from "@nestjs/common";
import { RegisterDto } from "./dto/register.dto";
import { LoginDto } from "./dto/login.dto";
import { AuthService } from "./auth.service";
import { Public } from "src/utils/decorators/public";
@Controller("auth")
export class AuthController {
constructor(private readonly authService: AuthService) {}
/** 注册用户 */
@Post("register")
@Public()
async register(@Body() data: RegisterDto) {
const user = await this.authService.register(data);
return user;
}
/** 登录用户 */
@Post("login")
@Public()
async login(@Body() data: LoginDto) {
return await this.authService.login(data);
}
}
实现一个安全守卫
现在我们可以安心实现一个安全守卫了。
创建文件:jwtAuth.guard.ts
,我是存放在utils/guards
目录下。
import { ExecutionContext, Injectable, UnauthorizedException, NotFoundException } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { IS_PUBLIC_KEY } from "../decorators/public";
import { Reflector } from "@nestjs/core";
import { Observable } from "rxjs";
@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
constructor(private reflector: Reflector) {
super();
}
/** 验证token */
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
// 是否是公共路由
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass()
]);
if (isPublic) return true;
// 校验token
return super.canActivate(context);
}
/**
* @description: 验完成后调用
* @param {*} error 这是 Passport 策略执行过程中发生的任何潜在错误。如果在验证过程中没有错误发生,这个值通常是 null
* @param {*} user 这是 Passport 策略验证成功后返回的用户对象。如果验证失败,这个值可能是 false 或 null,具体取决于你使用的 Passport 策略
* @param {*} info 如果验证失败,info通常是一个error对象
* @Date: 2024-01-02 13:14:47
* @Author: mulingyuer
*/
handleRequest(error, user, info) {
if (info || error) throw new UnauthorizedException("token校验失败");
if (!user) throw new NotFoundException("用户不存在");
return user;
}
}
canActivate
钩子函数负责验证token,我们加入白名单处理,剩下的直接使用父类的实现就行。
handleRequest
看注释吧。
导入依赖
功能都实现完成后,我们将依赖注入到app.module.ts
中。
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { AuthModule } from "./auth/auth.module";
import { PrismaModule } from "./prisma/prisma.module";
import { JwtModule } from "@nestjs/jwt";
import { JwtStrategy } from "./utils/passport/jwt.strategy";
import { JwtAuthGuard } from "./utils/guards/jwtAuth.guard";
import { APP_GUARD } from "@nestjs/core";
const NODE_ENV = process.env.NODE_ENV;
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: NODE_ENV === "development" ? ".env.development" : `.env.${NODE_ENV}`
}),
PrismaModule,
JwtModule.registerAsync({
global: true,
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
return {
secret: configService.get("TOKEN_SECRET"),
signOptions: {
expiresIn: "30d"
}
};
}
}),
AuthModule
],
controllers: [],
providers: [
JwtStrategy,
{
provide: APP_GUARD,
useClass: JwtAuthGuard
}
]
})
export class AppModule {}
在providers
注入我们的策略和守卫,其中守卫通过import引入的APP_GUARD
注册为全局的守卫,当然全局守卫是可以多个的。
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard
},
{
provide: APP_GUARD,
useClass: xxx
},
{
provide: APP_GUARD,
useClass: xxx
},
]
如果你ctrl+鼠标左键点进去APP_GUARD
,你会发现他就是Nestjs提供的一个字符串常量。
依赖导入后就行了,现在我们的全局守卫也生效了,白名单也有了。
非全局使用
如果不想使用全局,可以通过@UseGuards(AuthGuard('jwt')
装饰器方式使用:
import { Body, Controller, Get, Post, Request, UseGuards } from "@nestjs/common";
import { RegisterDto } from "./dto/register.dto";
import { LoginDto } from "./dto/login.dto";
import { AuthService } from "./auth.service";
import { Public } from "src/utils/decorators/public";
import { JwtAuthGuard } from "src/utils/guards/jwtAuth.guard";
@Controller("auth")
export class AuthController {
constructor(private readonly authService: AuthService) {}
/** 注册用户 */
@Post("register")
@Public()
async register(@Body() data: RegisterDto) {
const user = await this.authService.register(data);
return user;
}
/** 登录用户 */
@Post("login")
@Public()
async login(@Body() data: LoginDto) {
return await this.authService.login(data);
}
@Get("test")
@UseGuards(JwtAuthGuard)
test() {
return "test";
}
}
由于我们已经自定义了守卫,所以在使用的时候需要在UseGuards
中传入我们自定义的守卫,不要使用@UseGuards(AuthGuard('jwt'))
的形式,它只会触发实现的jwt策略,报错信息就会变成默认值,而不是我们自定义的信息了。
简化操作
@UseGuards(JwtAuthGuard)
这种用法有点复杂,我们可以封装成一个装饰器实现和@Public()
一样简洁的用法。
创建jwtAuth.ts
import { applyDecorators, UseGuards } from "@nestjs/common";
import { JwtAuthGuard } from "src/utils/guards/jwtAuth.guard";
export function JwtAuth() {
return applyDecorators(UseGuards(JwtAuthGuard));
}
然后使用的时候:
import { Body, Controller, Get, Post, Request } from "@nestjs/common";
import { RegisterDto } from "./dto/register.dto";
import { LoginDto } from "./dto/login.dto";
import { AuthService } from "./auth.service";
import { Public } from "src/utils/decorators/public";
import { JwtAuth } from "src/utils/decorators/jwtAuth";
@Controller("auth")
export class AuthController {
constructor(private readonly authService: AuthService) {}
/** 注册用户 */
@Post("register")
@Public()
async register(@Body() data: RegisterDto) {
const user = await this.authService.register(data);
return user;
}
/** 登录用户 */
@Post("login")
@Public()
async login(@Body() data: LoginDto) {
return await this.authService.login(data);
}
@Get("test")
@JwtAuth()
test() {
return "test";
}
}
效果也是ok的。
实现获取request.user属性的参数装饰器
这个其实在Nestjs的官方文档有提供,当我们使用了passport策略后,它会在request的user上赋值我们return的值,但是我们去获取的话每次都要先获取req再来req.user
获取,有点麻烦,我们可以实现一个更便捷的方式。
官方文档:参数装饰器
创建文件:user.ts
,我是存放在utils/decorators
目录下。
import { createParamDecorator } from "@nestjs/common";
import type { ExecutionContext } from "@nestjs/common";
export const User = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
});
使用时:
import { Controller, Get, Req } from "@nestjs/common";
import { User } from "src/utils/decorators";
@Controller("auth")
export class AuthController {
@Get("test")
async test(@Req() req, @User() user) {
console.log("🚀 ~ file: auth.controller.ts:28 ~ req:", req.user, user);
return "";
}
}
除了req.user
,通过@User
也可以拿到user。但是此时user的类型是any,我们可以从prisma中引入user的类型。
import { Controller, Get, Req } from "@nestjs/common";
import { User } from "src/utils/decorators";
import type { user as UserType } from "@prisma/client";
@Controller("auth")
export class AuthController {
@Get("test")
async test(@Req() req, @User() user: UserType) {
console.log("🚀 ~ file: auth.controller.ts:28 ~ req:", req.user, user);
return "";
}
}
因为user重名,只能as重新定义个新类型名字。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
全部评论 1
1
Google Chrome Windows 10