Nest.js 授權(quán)驗(yàn)證的方法示例
0x0 前言
系統(tǒng)授權(quán)指的是登錄用戶(hù)執(zhí)行操作過(guò)程,比如管理員可以對(duì)系統(tǒng)進(jìn)行用戶(hù)操作、網(wǎng)站帖子管理操作,非管理員可以進(jìn)行授權(quán)閱讀帖子等操作,所以實(shí)現(xiàn)需要對(duì)系統(tǒng)的授權(quán)需要身份驗(yàn)證機(jī)制,下面來(lái)實(shí)現(xiàn)最基本的基于角色的訪問(wèn)控制系統(tǒng)。
0x1 RBAC 實(shí)現(xiàn)
基于角色的訪問(wèn)控制(RBAC)是圍繞角色的特權(quán)和定義的策略無(wú)關(guān)的訪問(wèn)控制機(jī)制,首先創(chuàng)建個(gè)代表系統(tǒng)角色枚舉信息 role.enum.ts:
export enum Role { User = 'user', Admin = 'admin' }
如果是更復(fù)雜的系統(tǒng),推薦角色信息存儲(chǔ)到數(shù)據(jù)庫(kù)更好管理。
然后創(chuàng)建裝飾器和使用 @Roles() 來(lái)運(yùn)行指定訪問(wèn)所需要的資源角色,創(chuàng)建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)
上述創(chuàng)建一個(gè)名叫 @Roles() 的裝飾器,可以使用他來(lái)裝飾任何一個(gè)路由控制器,比如用戶(hù)創(chuàng)建:
@Post() @Roles(Role.Admin) create(@Body() createUserDto: CreateUserDto): Promise<UserEntity> { return this.userService.create(createUserDto) }
最后創(chuàng)建一個(gè) RolesGuard 類(lèi),它會(huì)實(shí)現(xiàn)將分配給當(dāng)前用戶(hù)角色和當(dāng)前路由控制器所需要角色進(jìn)行比較,為了訪問(wèn)路由角色(自定義元數(shù)據(jù)),將使用 Reflector 工具類(lèi),新建 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)) } }
假設(shè) request.user 包含 roles 屬性:
class User { // ...other properties roles: Role[] }
然后 RolesGuard 在控制器全局注冊(cè):
providers: [ { provide: APP_GUARD, useClass: RolesGuard } ]
當(dāng)某個(gè)用戶(hù)訪問(wèn)超出角色范疇內(nèi)的請(qǐng)求出現(xiàn):
{ "statusCode": 403, "message": "Forbidden resource", "error": "Forbidden" }
0x2 基于聲明的授權(quán)
創(chuàng)建身份后,系統(tǒng)可以給身份分配一個(gè)或者多個(gè)聲明權(quán)限,表示告訴當(dāng)前用戶(hù)可以做什么,而不是當(dāng)前用戶(hù)是什么,在 Nest 系統(tǒng)里實(shí)現(xiàn)基于聲明授權(quán),步驟和上面 RBAC 差不多,但有個(gè)區(qū)別是,需要比較權(quán)限,而不是判斷特定角色,每個(gè)用戶(hù)分配一組權(quán)限,比如定一個(gè) @RequirePermissions() 裝飾器,然后訪問(wèn)所需的權(quán)限屬性:
@Post() @RequirePermissions(Permission.CREATE_USER) create(@Body() createUserDto: CreateUserDto): Promise<UserEntity> { return this.userService.create(createUserDto) }
Permission 表示類(lèi)似 PRAC 中的 Role 枚舉,包含其中系統(tǒng)可訪問(wèn)的權(quán)限組:
export enum Role { CREATE_USER = ['add', 'read', 'update', 'delete'], READ_USER = ['read'] }
0x3 集成 CASL
CASL 是一個(gè)同構(gòu)授權(quán)庫(kù),可以限制客戶(hù)端訪問(wèn)的路由控制器資源,安裝依賴(lài):
yarn add @casl/ability
下面使用最簡(jiǎn)單的例子來(lái)實(shí)現(xiàn) CASL 的機(jī)制,創(chuàng)建 User 和 Article 倆個(gè)實(shí)體類(lèi):
class User { id: number isAdmin: boolean }
User 實(shí)體類(lèi)倆個(gè)屬性,分別是用戶(hù)編號(hào)和是否具有管理員權(quán)限。
class Article { id: number isPublished: boolean authorId: string }
Article 實(shí)體類(lèi)有三個(gè)屬性,分別是文章編號(hào)和文章?tīng)顟B(tài)(是否已經(jīng)發(fā)布)以及撰寫(xiě)文章的作者編號(hào)。
根據(jù)上面?zhèn)z個(gè)最簡(jiǎn)單的例子組成最簡(jiǎn)單的功能:
- 具有管理員權(quán)限的用戶(hù)可以管理所有實(shí)體(創(chuàng)建、讀取、更新和刪除)
- 用戶(hù)對(duì)所有內(nèi)容只有只讀訪問(wèn)權(quán)限
- 用戶(hù)可以更新自己撰寫(xiě)的文章 authorId === userId
- 已發(fā)布的文章無(wú)法刪除 article.isPublished === true
針對(duì)上面功能可以創(chuàng)建 Action 枚舉,來(lái)表示用戶(hù)對(duì)實(shí)體的操作:
export enum Action { Manage = 'manage', Create = 'create', Read = 'read', Update = 'update', Delete = 'delete', }
manage 是 CASL 中的特殊關(guān)鍵字,表示可以進(jìn)行任何操作。
實(shí)現(xiàn)功能需要二次封裝 CASL 庫(kù),執(zhí)行 nest-cli 進(jìn)行創(chuàng)建需要業(yè)務(wù):
nest g module casl nest g class casl/casl-ability.factory
定義 CaslAbilityFactory 的 createForUser() 方法,來(lái)未用戶(hù)創(chuàng)建對(duì)象:
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') // 允許任何讀寫(xiě)操作 } else { can(Action.Read, 'all') // 只讀操作 } can(Action.Update, Article, { authorId: user.id }) cannot(Action.Delete, Article, { isPublished: true }) return build({ // 詳細(xì):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 {}
然后在任何業(yè)務(wù)引入 CaslModule 然后在構(gòu)造函數(shù)注入就可以使用了:
constructor(private caslAbilityFactory: CaslAbilityFactory) {} const ability = this.caslAbilityFactory.createForUser(user) if (ability.can(Action.Read, 'all')) { // "user" 對(duì)所有內(nèi)容可以讀寫(xiě) }
如果當(dāng)前用戶(hù)是普通權(quán)限非管理員用戶(hù),可以閱讀文章,但不能創(chuàng)建新的文章和刪除現(xiàn)有文章:
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
這樣顯然有問(wèn)題,當(dāng)前用戶(hù)如果是文章的作者,應(yīng)該可以對(duì)此進(jìn)行操作:
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
上述簡(jiǎn)單的實(shí)現(xiàn),但在復(fù)雜的系統(tǒng)中還是不滿(mǎn)足更復(fù)雜的需求,所以配合上一篇的身份驗(yàn)證文章來(lái)進(jìn)行擴(kuò)展類(lèi)級(jí)別授權(quán)策略模式,在原有的 CaslAbilityFactory 類(lèi)進(jìn)行擴(kuò)展:
import { AppAbility } from '../casl/casl-ability.factory' interface IPolicyHandler { handle(ability: AppAbility): boolean } type PolicyHandlerCallback = (ability: AppAbility) => boolean export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback
提供支持對(duì)象和函數(shù)對(duì)每個(gè)路由控制器進(jìn)行策略檢查:IPolicyHandler 和 PolicyHandlerCallback。
然后創(chuàng)建一個(gè) @CheckPolicies() 裝飾器來(lái)運(yùn)行指定訪問(wèn)特定資源策略:
export const CHECK_POLICIES_KEY = 'check_policy' export const CheckPolicies = (...handlers: PolicyHandler[]) => SetMetadata(CHECK_POLICIES_KEY, handlers)
創(chuàng)建 PoliciesGuard 類(lèi)來(lái)提取并且執(zhí)行綁定路由控制器所有策略:
@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) } }
假設(shè) request.user 包含用戶(hù)實(shí)例,policyHandlers 是通過(guò)裝飾器 @CheckPolicies() 分配,使用 aslAbilityFactory#create 構(gòu)造 Ability 對(duì)象方法,來(lái)驗(yàn)證用戶(hù)是否具有足夠的權(quán)限來(lái)執(zhí)行特定的操作,然后將此對(duì)象傳遞給策略處理方法,該方法可以實(shí)現(xiàn)函數(shù)或者是類(lèi)的實(shí)例 IPolicyHandler,并且公開(kāi) handle() 方法返回布爾值。
@Get() @UseGuards(PoliciesGuard) @CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Article)) findAll() { return this.articlesService.findAll() }
同樣可以定義 IPolicyHandler 接口類(lèi):
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() }
到此這篇關(guān)于Nest.js 授權(quán)驗(yàn)證的方法示例的文章就介紹到這了,更多相關(guān)Nest.js 授權(quán)驗(yàn)證內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Node.js+express+socket實(shí)現(xiàn)在線實(shí)時(shí)多人聊天室
這篇文章主要為大家詳細(xì)介紹了Node.js+express+socket實(shí)現(xiàn)在線實(shí)時(shí)多人聊天室,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07NVM管理Node.js實(shí)現(xiàn)不同版本Angular環(huán)境切換
Node Version Manager(NVM)是一個(gè)用于管理多個(gè)Node.js版本的工具,它允許用戶(hù)在同一臺(tái)機(jī)器上安裝和使用多個(gè)Node.js版本,本文將給大家介紹NVM管理Node.js實(shí)現(xiàn)不同版本Angular環(huán)境切換的流程步驟,需要的朋友可以參考下2024-05-05使用travis-ci如何持續(xù)部署node.js應(yīng)用詳解
最近在學(xué)習(xí)使用 travis-ci 對(duì)項(xiàng)目進(jìn)行持續(xù)集成測(cè)試,所以下面這篇文章主要給大家介紹了關(guān)于使用travis-ci如何持續(xù)部署node.js應(yīng)用的相關(guān)資料,文中介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-07-07nodejs 使用nodejs-websocket模塊實(shí)現(xiàn)點(diǎn)對(duì)點(diǎn)實(shí)時(shí)通訊
這篇文章主要介紹了nodejs 使用nodejs-websocket模塊實(shí)現(xiàn)點(diǎn)對(duì)點(diǎn)實(shí)時(shí)通訊的實(shí)例代碼,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-11-11node.js使用免費(fèi)的阿里云ip查詢(xún)獲取ip所在地【推薦】
這篇文章主要介紹了node.js使用免費(fèi)的阿里云ip查詢(xún)獲取ip所在地的相關(guān)知識(shí),非常不錯(cuò),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2018-09-09詳解nodejs微信公眾號(hào)開(kāi)發(fā)——4.自動(dòng)回復(fù)各種消息
這篇文章主要介紹了詳解nodejs微信公眾號(hào)開(kāi)發(fā)——4.自動(dòng)回復(fù)各種消息,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-04-04node.js如何自定義實(shí)現(xiàn)一個(gè)EventEmitter
我們了解到,Node采用了事件驅(qū)動(dòng)機(jī)制,而EventEmitter就是Node實(shí)現(xiàn)事件驅(qū)動(dòng)的基礎(chǔ),本文主要介紹了node.js自定義實(shí)現(xiàn)EventEmitter,感興趣的可以了解一下2021-07-07window10系統(tǒng)下nvm詳細(xì)安裝步驟以及使用
nvm可以管理不同版本的node和npm,可以簡(jiǎn)單操作node版本的切換、安裝、查看等,下面這篇文章主要給大家介紹了關(guān)于window10系統(tǒng)下nvm詳細(xì)安裝步驟以及使用的相關(guān)資料,需要的朋友可以參考下2022-07-07解決npm?install版本不匹配問(wèn)題:?npm?ERR!?code?ETARGET?npm?ERR!?
這篇文章主要介紹了如何解決npm?install版本不匹配問(wèn)題:?npm?ERR!?code?ETARGET?npm?ERR!?notarget?No?matching?version?found?for,文中給出了詳細(xì)的解決方法,需要的朋友可以參考下2024-02-02