快活林资源网 Design By www.csstdc.com

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"htmlcode">
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()
}
快活林资源网 Design By www.csstdc.com
广告合作:本站广告合作请联系QQ:858582 申请时备注:广告合作(否则不回)
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
快活林资源网 Design By www.csstdc.com

《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线

暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。

艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。

《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。