守卫
守卫,守护保卫在路由方法之前的一道关卡。
一个被 @Injectable
注解装饰的类,实现了 CanActivate
就能被称为守卫。
守卫最多的应用场景,就是用来校验访问者的权限,如果不具有某个权限,可以将当前请求拒绝掉。
守卫的设计基于 AOP
思想实现,所以非常类似 异常过滤器、管道和拦截器,它和中间件有些定义上的差异。
在 Express
、Koa
等 NodeJS
后端框架中,通常由中间件来完成守卫的工作。中间件来验证身份是不错的选择,但中间件来做这件事情本质上是愚蠢的,因为中间件不知道调用的 next()
函数后执行哪个函数方法。而守卫存在 ExecutionContext
参数,可以确切知道下一步要执行的内容。
熟悉前端开发的同学来说,守卫听起来与 Vue
的前置路由守卫很像。守卫可以类比前置路由守卫,在 Vue
中能做的事,那么守卫也能做
使用守卫
正如前面所述,授权对于守卫来说是一个很好的应用场景,所以我们这里定义一个简单逻辑的守卫验证,它将会验证请求是否包含 token
请求头来决定请求是否继续:
// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { Observable } from "rxjs";
import { Request } from "express";
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const authorization = request.header("Authorization");
return !!authorization;
}
}
每个守卫必须实现接口并完善 canActivate
函数,该函数应该返回一个布尔值,来决定是否允许请求。
- true:请求将被处理。
- false:
Nest
将会拒绝该请求,并抛出错误。
接下来将守卫注册使用,绑定在控制器上:
// app.controller.ts
import { Controller, Get, UseGuards } from "@nestjs/common";
import { AppService } from "./app.service";
import { AuthGuard } from "./guards/auth.guard";
@UseGuards(AuthGuard)
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
当用户请求接口时,如果未传递请求头 Authorization
字段,将会返回错误信息给用户:
当满足预期传递了指定请求头时,守卫将会允许该请求继续并处理后续程序:
绑定位置
守卫与其他的组件一样,能在局部、全局进行绑定。
局部:
- 控制器
- 路由方法
// 1.绑定控制器
@UseGuards(AuthGuard)
@Controller()
export class AppController {
// 2.绑定路由方法
@UseGuards(AuthGuard)
@Get()
getHello(): string {}
// ...
}
全局:
- 主入口全局绑定,使用
app.useGlobalGuards
- 根模块依赖注入
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 1.主入口全局绑定
app.useGlobalGuards(new AuthGuard());
await app.listen(3000);
}
bootstrap();
// ---------------------------------------------
@Module({
imports: [],
controllers: [AppController],
providers: [
// 2.根模块依赖注入绑定
AppService,
{
provide: APP_GUARD,
useClass: AuthGuard
}
]
})
export class AppModule {}
JWT
JWT(JSON Web Token)
是目前最流行的跨域认证解决方案,是一种基于 Token
的认证授权机制。
TIP
关于 Token
的更多知识,请点击这里学习。
Token
最常见的场景就是发放授权,用户在请求登录时,服务端会发放 Token
作为用户的凭证。后续用户的每个请求都会携带该凭证,服务端在处理用户请求时,优先安全校验凭证是否存在、合法,来决定该请求是否继续处理。
实现一个发放用户 Token
的简单案例,该案例会模拟发放 Token
、过滤异常信息,实际生产会比这复杂好几个维度。
Nest
集成了 JWT
,使其成为了一个内置模块,我们仅需要安装并从依赖中引入。
pnpm i @nestjs/jwt -S
接着定义一组模拟数据,方便后续测试:
点击查看代码 src/constants/user.mock.ts
export const users = [
{
id: 1,
name: "王洋",
password:"wy123456"
},
{
id: 2,
name: "李秀英",
password:"lxy123456"
},
{
id: 3,
name: "杨敏",
password:"ym123456"
},
{
id: 4,
name: "吴静",
password:"wj123456"
},
{
id: 5,
name: "黄敏",
password:"hm123456"
}
];
把获取用户数据的操作存放在 User
模块中,把进行权限校验的操作放在 Auth
中,这会让我们更加省事,使用脚手架命令生成:
nest g res user auth --no-spec
定义登录、获取用户接口提供用户访问。
// 抛出定义的秘钥
export const TOKEN_SECRET = "guard example test token secret";
import { Module } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { AuthController } from "./auth.controller";
import { UserService } from "../user/user.service";
import { JwtModule } from "@nestjs/jwt";
import { TOKEN_SECRET } from "../constants/config";
import { UserModule } from "../user/user.module";
@Module({
imports: [
// jwtModule动态注册
// 注册在 authModule 是为了不用在providers中引入
JwtModule.registerAsync({
useFactory: () => ({
secret: TOKEN_SECRET,
global: true,
signOptions: { expiresIn: "1d" }
})
}),
UserModule
],
controllers: [AuthController],
providers: [AuthService, UserService]
})
export class AuthModule {}
// auth.controller.ts 权限模块控制器
import { Body, Controller, Post } from "@nestjs/common";
import { AuthService } from "./auth.service";
@Controller("auth")
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post("login")
login(@Body() body: Record<string, any>) {
return this.authService.signIn(body.name, body.password);
}
}
// auth.service.ts 权限服务
import { ForbiddenException, Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { UserService } from "../user/user.service";
import { AUTH_PWD_OR_ACCOUNT_ERROR } from "../filters/error-code";
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService
) {}
/**
* 对用户信息做验证,合法发放token,非法抛出异常4030000003
* @param name
* @param pwd
*/
async signIn(name: string, pwd: string) {
const user = await this.userService.findOneByName(name);
if (!user || user.name !== name || user.password !== pwd) {
throw new ForbiddenException({
errorCode: AUTH_PWD_OR_ACCOUNT_ERROR.errorCode,
message: AUTH_PWD_OR_ACCOUNT_ERROR.message,
code: AUTH_PWD_OR_ACCOUNT_ERROR.code
});
}
// 发放token
const payload = { sub: user.id, username: user.name };
return {
access_token: await this.jwtService.signAsync(payload)
};
}
}
// user.service.ts 用户服务
import { Injectable } from "@nestjs/common";
import { users } from "../constants/user.mock";
@Injectable()
export class UserService {
findOne(id: number) {
return users.find((item) => item.id === id);
}
findOneByName(name: string) {
return users.find((item) => item.name === name);
}
}
HttpExceptionFilter
、AUTH_PWD_OR_ACCOUNT_ERROR
在异常过滤器定义过。
可以看到,发送正确的请求,会响应 Token
字符串给回客户端。
TIP
在使用 JWT
用到动态模块注册,在后续的篇章会介绍如何使用。请点击这里学习
凭证校验
上面已经学习过守卫的作用,也学习了如何发放 Token
。接下来将它们结合在一起,完成案例,当用户在登录时,如未携带凭证,服务端拒绝掉此次请求。
定义一个接口,获取用户的个人信息(通常如不是开放的接口,均需要凭证验证):
import { Controller, Get, Param, Req, UseGuards } from "@nestjs/common";
import { UserService } from "./user.service";
import { users } from "../constants/user.mock";
import { Request } from "express";
@Controller("user")
export class UserController {
constructor(private readonly userService: UserService) {}
@Get("self")
findOne(@Req() req: Request & Record<any, any>) {
const { user } = req;
return user;
}
}
在请求获取用户信息接口时,携带我们刚才获得的 access_token
。
我们可以实现最后一个需求,实现一个认证守卫。
校验用户是否携带 Token
,是否合法,定义一个 auth
守卫:
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Observable } from "rxjs";
import { Request } from "express";
import { AUTH_PWD_OR_ACCOUNT_ERROR } from "../filters/error-code";
import { JwtService } from "@nestjs/jwt";
import { TOKEN_SECRET } from "../constants/config";
@Injectable()
export class AuthGuard1 implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new ForbiddenException({
errorCode: AUTH_PWD_OR_ACCOUNT_ERROR.errorCode,
message: AUTH_PWD_OR_ACCOUNT_ERROR.message,
code: AUTH_PWD_OR_ACCOUNT_ERROR.code
});
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: TOKEN_SECRET
});
// 在这里我们将 payload 挂载到请求对象上
// 以便我们可以在路由处理器中访问它
request["user"] = payload;
} catch {
throw new ForbiddenException({
errorCode: AUTH_PWD_OR_ACCOUNT_ERROR.errorCode,
message: AUTH_PWD_OR_ACCOUNT_ERROR.message,
code: AUTH_PWD_OR_ACCOUNT_ERROR.code
});
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(" ") ?? [];
return type === "Bearer" ? token : undefined;
}
}
把守卫注册在需要验证的接口,比如不开放的获取用户信息的接口:
import { Controller, Get, Param, Req, UseGuards } from "@nestjs/common";
import { UserService } from "./user.service";
import { users } from "../constants/user.mock";
import { Request } from "express";
@Controller("user")
export class UserController {
constructor(private readonly userService: UserService) {}
@UseGuards(AuthGuard)
@Get("self")
findOne(@Req() req: Request & Record<any, any>) {
const { user } = req;
return user;
}
}
发送一个携带错误 access_token
的请求,结果如下:
接着我们发送一个正确的,结果如预期一样,返回用户的信息数据:
TIP
关于更多的守卫授权的案例学习,请点击这里