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 相关文章推荐
JavaScript Event学习第二章 Event浏览器兼容性
Feb 07 Javascript
最简单的js图片切换效果实现代码
Sep 24 Javascript
javascript表单验证使用示例(javascript验证邮箱)
Jan 07 Javascript
JavaScript原型链示例分享
Jan 26 Javascript
js全选实现和判断是否有复选框选中的方法
Feb 17 Javascript
js获取隐藏元素宽高的实现方法
May 19 Javascript
javascript中call,apply,bind函数用法示例
Dec 19 Javascript
基于 Immutable.js 实现撤销重做功能的实例代码
Mar 01 Javascript
微信小程序发布新版本时自动提示用户更新的方法
Jun 07 Javascript
Vue实现日历小插件
Jun 26 Javascript
JS实现简单省市二级联动
Nov 27 Javascript
使用原生javascript开发计算器实例代码
Feb 21 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
模拟OICQ的实现思路和核心程序(二)
2006/10/09 PHP
thinkphp项目如何自定义微信分享描述内容
2017/02/20 PHP
提高Laravel应用性能方法详解
2019/06/24 PHP
JavaScript国旗变换效果代码
2008/08/13 Javascript
ajax无刷新动态调用股票信息(改良版)
2008/11/01 Javascript
JavaScript全局函数使用简单说明
2011/03/11 Javascript
js 使用form表单select类实现级联菜单效果
2012/12/19 Javascript
利用JQuery制作符合Web标准的QQ弹出消息
2014/01/14 Javascript
js实现的标题栏新消息闪烁提示效果
2014/06/06 Javascript
无刷新上传文件并返回自定义值
2015/06/11 Javascript
原生javascript实现自动更新的时间日期
2016/02/12 Javascript
ionic2 tabs使用 Modal底部tab弹出框
2016/12/30 Javascript
用director.js实现前端路由使用实例
2017/01/27 Javascript
JS闭包的几种常见形式实例详解
2017/09/16 Javascript
vue自定义一个v-model的实现代码
2018/06/21 Javascript
详解Angular中实现自定义组件的双向绑定的两种方法
2018/11/23 Javascript
js中值引用和地址引用实例分析
2019/06/21 Javascript
基于Vue+ElementUI的省市区地址选择通用组件
2019/11/20 Javascript
[01:27]DOTA2电竞之夜 今夜共饮庆功酒
2014/08/02 DOTA
Python3 能振兴 Python的原因分析
2014/11/28 Python
分享Pycharm中一些不为人知的技巧
2018/04/03 Python
Python利用正则表达式实现计算器算法思路解析
2018/04/25 Python
python导入模块交叉引用的方法
2019/01/19 Python
浅谈SciPy中的optimize.minimize实现受限优化问题
2020/02/29 Python
Python3自动生成MySQL数据字典的markdown文本的实现
2020/05/07 Python
Python脚本破解压缩文件口令实例教程(zipfile)
2020/06/14 Python
Python如何实现后端自定义认证并实现多条件登陆
2020/06/22 Python
基于python实现ROC曲线绘制广场解析
2020/06/28 Python
纯CSS3实现鼠标滑过按钮动画第二节
2020/07/16 HTML / CSS
浅谈three.js中的needsUpdate的应用
2012/11/12 HTML / CSS
美国球鞋寄卖网站:Stadium Goods
2018/05/09 全球购物
舞蹈教育学专业推荐信
2013/11/27 职场文书
幼儿园消防演练方案
2014/02/13 职场文书
协会周年庆活动方案
2014/08/26 职场文书
党员示范岗材料
2014/12/19 职场文书
小学生暑假生活总结
2015/07/13 职场文书