Nest.js 授权验证的方法示例


Posted in Javascript onFebruary 22, 2021

0x0 前言

系统授权指的是登录用户执行操作过程,比如管理员可以对系统进行用户操作、网站帖子管理操作,非管理员可以进行授权阅读帖子等操作,所以实现需要对系统的授权需要身份验证机制,下面来实现最基本的基于角色的访问控制系统。

0x1 RBAC 实现

基于角色的访问控制(RBAC)是围绕角色的特权和定义的策略无关的访问控制机制,首先创建个代表系统角色枚举信息 role.enum.ts:

export enum Role {
 User = 'user',
 Admin = 'admin'
}

如果是更复杂的系统,推荐角色信息存储到数据库更好管理。

然后创建装饰器和使用 @Roles() 来运行指定访问所需要的资源角色,创建roles.decorator.ts:

import { SetMetadata } from '@nestjs/common'
import { Role } from './role.enum'

export const ROLES_KEY = 'roles'
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles)

上述创建一个名叫 @Roles() 的装饰器,可以使用他来装饰任何一个路由控制器,比如用户创建:

@Post()
@Roles(Role.Admin)
create(@Body() createUserDto: CreateUserDto): Promise<UserEntity> {
 return this.userService.create(createUserDto)
}

最后创建一个 RolesGuard 类,它会实现将分配给当前用户角色和当前路由控制器所需要角色进行比较,为了访问路由角色(自定义元数据),将使用 Reflector 工具类,新建 roles.guard.ts:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'

import { Role } from './role.enum'
import { ROLES_KEY } from './roles.decorator'

@Injectable()
export class RolesGuard implements CanActivate {
 constructor(private reflector: Reflector) {}

 canActivate(context: ExecutionContext): boolean {
 const requireRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [context.getHandler(), context.getClass()])
 if (!requireRoles) {
  return true
 }
 const { user } = context.switchToHttp().getRequest()
 return requireRoles.some(role => user.roles?.includes(role))
 }
}

假设 request.user 包含 roles 属性:

class User {
 // ...other properties
 roles: Role[]
}

然后 RolesGuard 在控制器全局注册:

providers: [
 {
 provide: APP_GUARD,
 useClass: RolesGuard
 }
]

当某个用户访问超出角色范畴内的请求出现:

{
 "statusCode": 403,
 "message": "Forbidden resource",
 "error": "Forbidden"
}

0x2 基于声明的授权

创建身份后,系统可以给身份分配一个或者多个声明权限,表示告诉当前用户可以做什么,而不是当前用户是什么,在 Nest 系统里实现基于声明授权,步骤和上面 RBAC 差不多,但有个区别是,需要比较权限,而不是判断特定角色,每个用户分配一组权限,比如定一个 @RequirePermissions() 装饰器,然后访问所需的权限属性:

@Post()
@RequirePermissions(Permission.CREATE_USER)
create(@Body() createUserDto: CreateUserDto): Promise<UserEntity> {
 return this.userService.create(createUserDto)
}

Permission 表示类似 PRAC 中的 Role 枚举,包含其中系统可访问的权限组:

export enum Role {
 CREATE_USER = ['add', 'read', 'update', 'delete'],
 READ_USER = ['read']
}

0x3 集成 CASL

CASL 是一个同构授权库,可以限制客户端访问的路由控制器资源,安装依赖:

yarn add @casl/ability

下面使用最简单的例子来实现 CASL 的机制,创建 User 和 Article 俩个实体类:

class User {
 id: number
 isAdmin: boolean
}

User 实体类俩个属性,分别是用户编号和是否具有管理员权限。

class Article {
 id: number
 isPublished: boolean
 authorId: string
}

Article 实体类有三个属性,分别是文章编号和文章状态(是否已经发布)以及撰写文章的作者编号。

根据上面俩个最简单的例子组成最简单的功能:

  • 具有管理员权限的用户可以管理所有实体(创建、读取、更新和删除)
  • 用户对所有内容只有只读访问权限
  • 用户可以更新自己撰写的文章 authorId === userId
  • 已发布的文章无法删除 article.isPublished === true

针对上面功能可以创建 Action 枚举,来表示用户对实体的操作:

export enum Action {
 Manage = 'manage',
 Create = 'create',
 Read = 'read',
 Update = 'update',
 Delete = 'delete',
}

manage 是 CASL 中的特殊关键字,表示可以进行任何操作。

实现功能需要二次封装 CASL 库,执行 nest-cli 进行创建需要业务:

nest g module casl
nest g class casl/casl-ability.factory

定义 CaslAbilityFactory 的 createForUser() 方法,来未用户创建对象:

type Subjects = InferSubjects<typeof Article | typeof User> | 'all'

export type AppAbility = Ability<[Action, Subjects]>

@Injectable()
export class CaslAbilityFactory {
 createForUser(user: User) {
 const { can, cannot, build } = new AbilityBuilder<
  Ability<[Action, Subjects]>
 >(Ability as AbilityClass<AppAbility>);

 if (user.isAdmin) {
  can(Action.Manage, 'all') // 允许任何读写操作
 } else {
  can(Action.Read, 'all') // 只读操作
 }

 can(Action.Update, Article, { authorId: user.id })
 cannot(Action.Delete, Article, { isPublished: true })

 return build({
  // 详细:https://casl.js.org/v5/en/guide/subject-type-detection#use-classes-as-subject-types
  detectSubjectType: item => item.constructor as ExtractSubjectType<Subjects>
 })
 }
}

然后在 CaslModule 引入:

import { Module } from '@nestjs/common'
import { CaslAbilityFactory } from './casl-ability.factory'

@Module({
 providers: [CaslAbilityFactory],
 exports: [CaslAbilityFactory]
})
export class CaslModule {}

然后在任何业务引入 CaslModule 然后在构造函数注入就可以使用了:

constructor(private caslAbilityFactory: CaslAbilityFactory) {}

const ability = this.caslAbilityFactory.createForUser(user)
if (ability.can(Action.Read, 'all')) {
 // "user" 对所有内容可以读写
}

如果当前用户是普通权限非管理员用户,可以阅读文章,但不能创建新的文章和删除现有文章:

const user = new User()
user.isAdmin = false

const ability = this.caslAbilityFactory.createForUser(user)
ability.can(Action.Read, Article) // true
ability.can(Action.Delete, Article) // false
ability.can(Action.Create, Article) // false

这样显然有问题,当前用户如果是文章的作者,应该可以对此进行操作:

const user = new User()
user.id = 1

const article = new Article()
article.authorId = user.id

const ability = this.caslAbilityFactory.createForUser(user)
ability.can(Action.Update, article) // true

article.authorId = 2
ability.can(Action.Update, article) // false

0x4 PoliceiesGuard

上述简单的实现,但在复杂的系统中还是不满足更复杂的需求,所以配合上一篇的身份验证文章来进行扩展类级别授权策略模式,在原有的 CaslAbilityFactory 类进行扩展:

import { AppAbility } from '../casl/casl-ability.factory'

interface IPolicyHandler {
 handle(ability: AppAbility): boolean
}

type PolicyHandlerCallback = (ability: AppAbility) => boolean

export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback

提供支持对象和函数对每个路由控制器进行策略检查:IPolicyHandler 和 PolicyHandlerCallback。

然后创建一个 @CheckPolicies() 装饰器来运行指定访问特定资源策略:

export const CHECK_POLICIES_KEY = 'check_policy'
export const CheckPolicies = (...handlers: PolicyHandler[]) => SetMetadata(CHECK_POLICIES_KEY, handlers)

创建 PoliciesGuard 类来提取并且执行绑定路由控制器所有策略:

@Injectable()
export class PoliciesGuard implements CanActivate {
 constructor(
 private reflector: Reflector,
 private caslAbilityFactory: CaslAbilityFactory,
 ) {}

 async canActivate(context: ExecutionContext): Promise<boolean> {
 const policyHandlers =
  this.reflector.get<PolicyHandler[]>(
  CHECK_POLICIES_KEY,
  context.getHandler()
  ) || []

 const { user } = context.switchToHttp().getRequest()
 const ability = this.caslAbilityFactory.createForUser(user)

 return policyHandlers.every((handler) =>
  this.execPolicyHandler(handler, ability)
 )
 }

 private execPolicyHandler(handler: PolicyHandler, ability: AppAbility) {
 if (typeof handler === 'function') {
  return handler(ability)
 }
 return handler.handle(ability)
 }
}

假设 request.user 包含用户实例,policyHandlers 是通过装饰器 @CheckPolicies() 分配,使用 aslAbilityFactory#create 构造 Ability 对象方法,来验证用户是否具有足够的权限来执行特定的操作,然后将此对象传递给策略处理方法,该方法可以实现函数或者是类的实例 IPolicyHandler,并且公开 handle() 方法返回布尔值。

@Get()
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Article))
findAll() {
 return this.articlesService.findAll()
}

同样可以定义 IPolicyHandler 接口类:

export class ReadArticlePolicyHandler implements IPolicyHandler {
 handle(ability: AppAbility) {
 return ability.can(Action.Read, Article)
 }
}

使用如下:

@Get()
@UseGuards(PoliciesGuard)
@CheckPolicies(new ReadArticlePolicyHandler())
findAll() {
 return this.articlesService.findAll()
}

到此这篇关于Nest.js 授权验证的方法示例的文章就介绍到这了,更多相关Nest.js 授权验证内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木! 

Javascript 相关文章推荐
一个封装js代码-----展开收起效果示例
Jul 03 Javascript
jQuery javaScript捕获回车事件(示例代码)
Nov 07 Javascript
js通过八个点 拖动改变div大小的实现方法
Mar 05 Javascript
javascript移动设备Web开发中对touch事件的封装实例
Jun 05 Javascript
js与jquery回车提交的方法
Feb 03 Javascript
AngularJS内置指令
Feb 04 Javascript
Vue 多层组件嵌套二种实现方式(测试实例)
Sep 08 Javascript
JavaScript实现body内任意节点的自定义属性功能示例
Sep 18 Javascript
vue导出html、word和pdf的实现代码
Jul 31 Javascript
ES6入门教程之let、const的使用方法
Apr 13 Javascript
vue+webpack dev本地调试全局样式引用失效的解决方案
Nov 12 Javascript
vue项目实现减少app.js和vender.js的体积操作
Nov 12 Javascript
使用原生javascript开发计算器实例代码
Feb 21 #Javascript
Nest.js环境变量配置与序列化详解
Feb 21 #Javascript
关于Js中new操作符的作用详解
Feb 21 #Javascript
vue-cli 3如何使用vue-bootstrap-datetimepicker日期插件
Feb 20 #Vue.js
Vue实现todo应用的示例
Feb 20 #Vue.js
JavaScript 绘制饼图的示例
Feb 19 #Javascript
JavaScript 判断浏览器是否是IE
Feb 19 #Javascript
You might like
日本因肺炎疫情影响,这几部动漫推延播放!
2020/03/03 日漫
Smarty安装配置方法
2008/04/10 PHP
一周让你学会PHP 不错的学习资料
2009/02/06 PHP
php在多维数组中根据键名快速查询其父键以及父键值的代码
2011/05/07 PHP
PHP中Header使用的HTTP协议及常用方法小结
2014/11/04 PHP
Zend Framework动作助手Url用法详解
2016/03/05 PHP
php使用PDO事务配合表格读取大量数据插入操作实现方法
2017/02/16 PHP
php PDO判断连接是否可用的实现方法
2017/04/03 PHP
IE6 弹出Iframe层中的文本框“经常”无法获得输入焦点
2009/12/27 Javascript
用jquery实现下拉菜单效果的代码
2010/07/25 Javascript
javascript是怎么继承的介绍
2012/01/05 Javascript
javascript函数式编程程序员的工具集
2015/10/11 Javascript
javascript实现动态标签云
2015/10/16 Javascript
JS延时提示框实现方法详解
2015/11/26 Javascript
基于JavaScript实现高德地图和百度地图提取行政区边界经纬度坐标
2016/01/22 Javascript
JavaScript常用判断写法大全(推荐)
2016/05/30 Javascript
9个让JavaScript调试更简单的Console命令
2016/11/14 Javascript
浅析vue component 组件使用
2017/03/06 Javascript
vue-cli+webpack记事本项目创建
2017/04/01 Javascript
react实现菜单权限控制的方法
2017/12/11 Javascript
详解从NodeJS搭建中间层再谈前后端分离
2018/11/13 NodeJs
vue实现todolist基本功能以及数据存储功能实例详解
2019/04/11 Javascript
微信小程序自定义可滑动顶部TabBar选项卡实现页面切换功能示例
2019/05/14 Javascript
[46:02]DOTA2上海特级锦标赛D组资格赛#2 Liquid VS VP第二局
2016/02/28 DOTA
用Python编写一个每天都在系统下新建一个文件夹的脚本
2015/05/04 Python
Django框架下在视图中使用模版的方法
2015/07/16 Python
对python特殊函数 __call__()的使用详解
2019/07/02 Python
flask框架jinja2模板与模板继承实例分析
2019/08/01 Python
Python代码一键转Jar包及Java调用Python新姿势
2020/03/10 Python
树莓派4B安装Tensorflow的方法步骤
2020/07/16 Python
美国大城市最热门旅游景点门票:CityPASS
2016/12/16 全球购物
商场主管竞聘书
2014/03/31 职场文书
大学生实习鉴定评语
2014/04/25 职场文书
爱国演讲稿500字
2014/05/04 职场文书
学雷锋感言
2015/08/03 职场文书
Netty分布式客户端接入流程初始化源码分析
2022/03/25 Java/Android