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居然支持中文(unicode)编程!
Apr 12 Javascript
javascript 异常处理使用总结
Jun 21 Javascript
javascript 快速排序函数代码
May 30 Javascript
Jquery为单选框checkbox绑定单击click事件
Dec 18 Javascript
JSCode all of Brower 全局屏蔽网页右键功能 具体实现
Jun 05 Javascript
jquery垂直公告滚动实现代码
Dec 08 Javascript
JQuery中节点遍历方法实例
May 18 Javascript
基于JavaScript怎么实现让歌词滚动播放
Nov 03 Javascript
angular route中使用resolve在uglify压缩后问题解决
Sep 21 Javascript
详解Vue中状态管理Vuex
May 11 Javascript
jquery插件canvaspercent.js实现百分比圆饼效果
Jul 18 jQuery
Angular实现响应式表单
Aug 04 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/08/18 国漫
PHP数据集构建JSON格式及新数组的方法
2012/11/07 PHP
php抓取并保存网站图片的实现代码
2015/10/28 PHP
基于jquery的无缝循环新闻列表插件
2011/03/07 Javascript
node.js chat程序如何实现Ajax long-polling长链接刷新模式
2012/03/13 Javascript
JS实现网站菜单拖拽移位效果的方法
2015/09/24 Javascript
JS面试题---关于算法台阶的问题
2016/07/26 Javascript
js绘制购物车抛物线动画
2020/11/18 Javascript
js判断请求的url是否可访问,支持跨域判断的实现方法
2016/09/17 Javascript
AngularJS中如何使用echart插件示例详解
2016/10/26 Javascript
BootStrap Validator对于隐藏域验证和程序赋值即时验证的问题浅析
2016/12/01 Javascript
原生Aajax 和jQuery Ajax 写法个人总结
2017/03/24 jQuery
vue调用高德地图实例代码
2017/04/28 Javascript
javaScript字符串工具类StringUtils详解
2017/12/08 Javascript
js隐式转换的知识实例讲解
2018/09/28 Javascript
微信小程序日历组件使用方法详解
2018/12/29 Javascript
setTimeout与setInterval的区别浅析
2019/03/23 Javascript
js实现点击图片在屏幕中间弹出放大效果
2019/09/11 Javascript
关于vue项目中搜索节流的实现代码
2019/09/17 Javascript
JavaScript中继承原理与用法实例入门
2020/05/09 Javascript
python实现的二叉树定义与遍历算法实例
2017/06/30 Python
windows10下python3.5 pip3安装图文教程
2018/04/02 Python
新年快乐! python实现绚烂的烟花绽放效果
2019/01/30 Python
python3 pygame实现接小球游戏
2019/05/14 Python
详解如何从TensorFlow的mnist数据集导出手写体数字图片
2019/08/05 Python
Tensorflow与Keras自适应使用显存方式
2020/06/22 Python
中国高端家电购物商城:顺电
2018/03/04 全球购物
英国最大的专业户外零售商:Mountain Warehouse
2018/06/06 全球购物
机关单位人员学雷锋心得体会
2014/03/10 职场文书
《雕塑之美》教学反思
2014/04/24 职场文书
人大代表选举标语
2014/10/07 职场文书
黄山导游词
2015/01/31 职场文书
2015年员工试用期工作总结
2015/05/28 职场文书
golang import自定义包方式
2021/04/29 Golang
OpenCV实现反阈值二值化
2021/11/17 Java/Android
通过Python把学姐照片做成拼图游戏
2022/02/15 Python