跳到主要内容
版本:4.0.0 🚧

守卫

从 v3.6.0 开始,Midway 提供守卫能力。

守卫会根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理。

普通的应用程序中,一般会在中间件中处理这些逻辑,但是中间件的逻辑过于通用,同时也无法很优雅的去和路由方法进行结合,为此我们在中间件之后,进入路由方法之前设计了守卫,可以方便的进行方法鉴权等处理。

下面的代码,我们将以 @midwayjs/koa 举例。

编写守卫

一般情况下,我们会在 src/guard 文件夹中编写守卫。

创建一个 src/guard/auth.guard.ts ,用于验证路由是否能被用户访问。

➜  my_midway_app tree
.
├── src
│ ├── controller
│ │ ├── user.controller.ts
│ │ └── home.controller.ts
│ ├── interface.ts
│ ├── guard
│ │ └── auth.guard.ts
│ └── service
│ └── user.service.ts
├── test
├── package.json
└── tsconfig.json

Midway 使用 @Guard 装饰器标识守卫,示例代码如下。

import { IMiddleware, Guard, IGuard } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';

@Guard()
export class AuthGuard implements IGuard<Context> {
async canActivate(context: Context, supplierClz, methodName: string): Promise<boolean> {
// ...
}
}

canActivate 方法用于在请求中验证是否可以访问后续的方法,当返回 true 时,后续的方法会被执行,当 canActivate 返回 false 时,会抛出 403 错误码。

使用守卫

守卫可以被应用到不同的框架上,在 http 下,可以应用到全局,Controller 和方法上,在其他的 Framework 实现中,仅能在方法上使用。

路由守卫

在写完守卫之后,我们需要把它应用到各个控制器路由之上。

使用 UseGuard 装饰器,我们可以应用到类和方法上。

import { Controller } from '@midwayjs/core';
import { AuthGuard } from '../guard/auth.guard';

@UseGuard(AuthGuard)
@Controller('/')
export class HomeController {

}

在方法上应用守卫。

import { Controller, Get } from '@midwayjs/core';
import { ReportMiddleware } from '../middleware/report.middlweare';
import { AuthGuard } from '../guard/auth.guard';

@Controller('/')
export class HomeController {

@UseGuard(AuthGuard)
@Get('/', { middleware: [ ReportMiddleware ]})
async home() {
}
}

也可以传入数组。

@UseGuard([AuthGuard, Auth2Guard])

全局守卫

我们需要在应用启动前,加入当前框架的守卫列表中,useGuard 方法,可以把守卫加入到守卫列表中。

// src/configuration.ts
import { App, Configuration } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { AuthGuard } from './guard/auth.guard';

@Configuration({
imports: [koa]
// ...
})
export class MainConfiguration {

@App()
app: koa.Application;

async onReady() {
this.app.useGuard(AuthGuard);
}
}

同理可以添加多个守卫。

async onReady() {
this.app.useGuard([AuthGuard, Auth2Guard]);
}

自定义错误

默认情况下,守卫的 canActivate 方法当返回 false 时,框架会抛出 403 错误(ForbiddenError)。

你也可以在守卫中自行决定需要抛出的错误。

import { IMiddleware, Guard, IGuard, httpError } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';

@Guard()
export class AuthGuard implements IGuard<Context> {
async canActivate(context: Context, supplierClz, methodName: string): Promise<boolean> {
// ...
if (methodName ==='xxx') {
throw new httpError.ForbiddenError();
}

return true;
}
}
提示

注意全局错误处理器也会拦截守卫抛出的错误。

和中间件的区别

守卫会在全局中间件 之后,路由方法业务逻辑 之前 执行。

中间件一般编写通用的处理逻辑,比如登录,用户识别,安全校验等,而守卫由于在路由内部,更适合做基于路由的权限控制。

中间件中虽然有路由信息,但是无法明确得知具体进入的是哪个实际的路由控制器(除非额外查询匹配),而守卫已经进入了路由方法,在性能方面有比较大的优势。

基于角色的鉴权示例

一般情况下,我们会把方法访问和角色关联起来,下面我们来简单实现一个基于用户角色的访问控制。

首先,我们定义一个 @Role 装饰器,用于设定方法的访问权限。

// src/decorator/role.decorator.ts
import { savePropertyMetadata } from '@midwayjs/core';

export const ROLE_META_KEY = 'role:name'

export function Role(roleName: string | string[]): MethodDecorator {
return (target, propertyKey, descriptor) => {
roleName = [].concat(roleName);
// 只保存元数据
savePropertyMetadata(ROLE_META_KEY, roleName, target, propertyKey);
};
}

编写一个守卫,用于角色鉴权。

import { IMiddleware, Guard, IGuard, getPropertyMetadata } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import { ROLE_META_KEY } from '../decorator/role.decorator.ts';

@Guard()
export class AuthGuard implements IGuard<Context> {
async canActivate(context: Context, supplierClz, methodName: string): Promise<boolean> {
// 从类元数据上获取角色信息
const roleNameList = getPropertyMetadata<string[]>(ROLE_META_KEY, supplierClz, methodName);
if (roleNameList && roleNameList.length && context.user.role) {
// 假设中间件已经拿到了用户角色信息,保存到了 context.user.role 中
// 直接判断是否包含该角色
return roleNameList.includes(context.user.role);
}

return false;
}
}

在路由上使用该守卫。

import { Controller, Get } from '@midwayjs/core';
import { ReportMiddleware } from '../middleware/report.middlweare';
import { AuthGuard } from '../guard/auth.guard';

@UseGuard(AuthGuard)
@Controller('/user')
export class HomeController {

// 只允许 admin 访问
@Role(['admin'])
@Get('/getUserRoles')
async getUserRoles() {
// ...
}
}

只有当 ctx.user.role 返回了 admin 的时候,才会被允许访问 /getUserRoles 路由。