歡迎持續關注NestJs學習之旅系列文章typescript
傳統的Web應用中去檢測用戶登陸、權限判斷等等都是在控制器層或者中間件層作的,而在目前比較推薦的模塊化與組件化架構中,不一樣職責的功能建議拆分到不一樣的類文件中去。數據庫
經過前幾篇的學習能夠發現NestJs在這方面作的很好,傳統的express/koa應用中,須要開發者去思考項目結構以及代碼組織,而NestJs不須要你這樣作,下降了開發成本,另外也統一了開發風格。express
熟悉Vue,React的夥伴應該比較熟悉這個概念,通俗的說就是在訪問指定的路由以前回調一個處理函數,若是該函數返回true或者**調用了next()**就會放行當前訪問,不然阻斷當前訪問。json
NestJs中路由守衛也是如此,經過繼承CanActive接口便可定義一個路由守衛。bootstrap
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
class AppGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
複製代碼
路由守衛本質上也是中間件的一種,koa或者express開發中接口鑑權就是基於中間件開發的,若是當前請求是不被容許的,當前中間件將不會調用後續中間件,達到阻斷請求的目的。架構
可是中間件的職責是不明確的,中間件能夠幹任何事(數據校驗,格式轉化,響應體壓縮等等),這致使只能經過名稱來識別中間件,項目迭代比較久之後,有比較高的維護成本。app
因爲單一職責的關係,路由守衛只能返回true和false來決定放行/阻斷當前請求,不能夠修改request/response對象,由於一旦破壞單一職責的原則,排查問題比較麻煩。框架
若是須要修改request對象,能夠結合中間件一塊兒使用。koa
路由守衛在全部中間件執行完畢以後開始執行。async
如下是一個結合路由守衛和中間件的例子。
// auth.middleware.ts
// 中間件職責:讀取請求頭Authorization,若是存在且有效的話,設置user對象到request中
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class AuthMiddleware implements NestMiddleware<Request|any, Response> {
constructor(private readonly userService: UserService) {}
async use(req: Request|any, res: Response, next: Function) {
const token = req.header('authorization');
if(!token) {
next();
return;
}
const user = await this.userService.getUserByToken(token);
if(!user) {
next();
return;
}
request.user = user;
next();
}
}
複製代碼
// user.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Request } from 'express';
@Injectable()
export class UserGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest<Request | any>();
// 直接檢測是否有user對象,由於無user對象證實無token或者token無效
return !!request.user;
}
}
複製代碼
以上例子是筆者經常使用的一種方法,這樣職責比較清晰,並且user對象能夠在其餘中間件中讀取。
NestJs使用**@UseGuards()**裝飾器來注入路由守衛。支持全局守衛、控制器級別守衛、方法級別守衛。
下面以一個實際的例子來演示路由守衛的工做過程。
// user.service.ts
@Injetable()
export class UserService {
// 模擬校驗,這裏直接返回true,實際開發中自行實現便可
validateToken(token: string) {
return true;
}
}
複製代碼
// user.guard.ts
@Injetable()
export class UserGuard implements CanActive {
constructor(private readonly userService: UserService) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest<Request>();
// 讀取token
const authorization = request.header('authorization');
if (!authorization) {
return false;
}
return this.userService.validateToken(authorization);
}
}
複製代碼
@Controller('user')
export class UserController {
// 請求登陸
@Post('login')
login() {
return {token:'fake_token'}; // 直接下發token,真實場景下須要驗證帳號密碼
}
// 查看當前用戶信息
@Get('info')
@UseGuards(UserGuard) // 方法級路由守衛
info() {
return {username: 'fake_user'};
}
}
複製代碼
一個完整的路由守衛應用實例就已經出來了,雖然我們的路由守衛沒啥邏輯都是直接放行的,可是實際開發中也是基於這種思路去開發的,只不過校驗的邏輯不同罷了。
該級別會對被裝飾控制器的全部路由方法生效。
@Controller('user')
@UseGuards(UserGuard)
export class UserController {
// 查看當前用戶信息
@Get('info')
info() {
return {username: 'fake_user'};
}
}
複製代碼
該級別只對被裝飾的方法生效。
@Get('info')
@UseGuards(UserGuard)
info() {
return {username: 'fake_user'};
}
複製代碼
與全局異常過濾器相似,該級別對全部控制器的全部路由方法生效。該方法與全局異常過濾器同樣不會對WebSocket和GRPC生效。
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 因爲main.ts啓動時並未初始化依賴注入容器,因此依賴必須手動傳入,通常狀況下不建議使用全局守衛,由於依賴注入得本身解決。
app.useGlobalGuards(new UserGuard(new UserService()));
await app.listen(3000);
}
bootstrap();
複製代碼
CanActive接口的方法中有一個ExecutionContext對象,該對象爲請求上下文對象,該對象定義以下:
export interface ExecutionContext extends ArgumentsHost {
getClass<T = any>(): Type<T>;
getHandler(): Function;
}
複製代碼
能夠看到繼承了ArgumentHost,ArgumentHost在以前的異常處理文章中已經提到過了,這裏再也不贅述。
例如訪問 /user/info 時,getClass()將返回UserController對象(不是實例),getHandler()將返回info()函數的引用。
這個特性有什麼做用呢?
NestJs中可使用反射來獲取定義在方法、屬性、類等等上面的自定義屬性,這一點和Java的註解有點相似。
被角色裝飾器裝飾的控制器或者方法在訪問時,路由守衛會讀取當前用戶的角色,與裝飾器傳入的角色相匹配,若是匹配失敗,將阻斷請求,不然將放行請求。
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
複製代碼
假設咱們有一個只容許管理員訪問的建立用戶的接口:
@Post('create')
@Roles('admin')
async create(@Body() createUserDTO: CreateUserDTO) {
this.userService.create(createUserDTO);
}
複製代碼
// role.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 獲取roles元數據,roles與roles.decorator.ts中SetMetadata()第一個參數一致
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) { // 未被裝飾器裝飾,直接放行
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user; // 讀取請求對象的user,該user對象能夠經過中間件來設置(本文前面有例子)
const hasRole = () => user.roles.some((role) => roles.includes(role));
return user && user.roles && hasRole();
}
}
複製代碼
以上就是讀取自定義裝飾器數據開發RBAC的例子,寫的比較簡陋,可是原理是同樣的,代碼量少的話便於理解核心。
路由守衛返回false時框架會拋出ForbiddenException,客戶端收到的默認響應以下:
{
"statusCode": 403,
"message": "Forbidden resource"
}
複製代碼
若是須要拋出其餘異常,好比UnauthorizedException,能夠直接在路由守衛的canActive()方法中拋出。
另外,在這裏拋出的異常時能夠被異常過濾器捕獲而且處理的,因此咱們能夠自定義異常類型以及輸出自定義響應數據。
本文除了路由守衛以外另外一個重要的知識是【自定義元數據裝飾器】的使用,基於該裝飾器能夠開發不少使人驚豔的功能,這個就看各位看官的實現了。
若是您以爲有所收穫,分享給更多須要的朋友,謝謝!
若是您想交流關於NestJs更多的知識,歡迎加羣討論!