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() }
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
稳了!魔兽国服回归的3条重磅消息!官宣时间再确认!
昨天有一位朋友在大神群里分享,自己亚服账号被封号之后居然弹出了国服的封号信息对话框。
这里面让他访问的是一个国服的战网网址,com.cn和后面的zh都非常明白地表明这就是国服战网。
而他在复制这个网址并且进行登录之后,确实是网易的网址,也就是我们熟悉的停服之后国服发布的暴雪游戏产品运营到期开放退款的说明。这是一件比较奇怪的事情,因为以前都没有出现这样的情况,现在突然提示跳转到国服战网的网址,是不是说明了简体中文客户端已经开始进行更新了呢?
更新日志
- 凤飞飞《我们的主题曲》飞跃制作[正版原抓WAV+CUE]
- 刘嘉亮《亮情歌2》[WAV+CUE][1G]
- 红馆40·谭咏麟《歌者恋歌浓情30年演唱会》3CD[低速原抓WAV+CUE][1.8G]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[320K/MP3][193.25MB]
- 【轻音乐】曼托凡尼乐团《精选辑》2CD.1998[FLAC+CUE整轨]
- 邝美云《心中有爱》1989年香港DMIJP版1MTO东芝首版[WAV+CUE]
- 群星《情叹-发烧女声DSD》天籁女声发烧碟[WAV+CUE]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[FLAC/分轨][748.03MB]
- 理想混蛋《Origin Sessions》[320K/MP3][37.47MB]
- 公馆青少年《我其实一点都不酷》[320K/MP3][78.78MB]
- 群星《情叹-发烧男声DSD》最值得珍藏的完美男声[WAV+CUE]
- 群星《国韵飘香·贵妃醉酒HQCD黑胶王》2CD[WAV]
- 卫兰《DAUGHTER》【低速原抓WAV+CUE】
- 公馆青少年《我其实一点都不酷》[FLAC/分轨][398.22MB]
- ZWEI《迟暮的花 (Explicit)》[320K/MP3][57.16MB]