@lark-apaas/nestjs-authzpaas
v0.1.12
Published
FullStack Nestjs authzpaas
Readme
nestjs-authzpaas
nestjs-authzpaas 是一个基于 NestJS 的权限管理模块,提供了完整的 RBAC(基于角色的访问控制)和 ABAC(基于属性的访问控制)解决方案。它基于 CASL 实现,支持角色检查、权限检查、环境检查等多种鉴权方式。
目录
核心概念
权限模型
本模块采用以下权限模型:
- 角色 (Role): 用户所属的角色,如
admin、editor、viewer
核心组件
| 组件 | 类型 | 说明 |
|------|------|------|
| AuthZPaasGuard | 全局守卫 | 自动拦截所有请求,执行鉴权检查 |
| RolesMiddleware | 中间件 | 自动获取并注入用户角色信息到请求上下文 |
| CanRole | 装饰器 | 声明式角色检查 |
快速开始
1. 安装依赖
npm install @lark-apaas/nestjs-authzpaas
# 或
yarn add @lark-apaas/nestjs-authzpaas2. 配置模块
在 app.module.ts 中导入并配置 AuthZPaasModule:
import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
import { AuthZPaasModule, RolesMiddleware } from '@lark-apaas/nestjs-authzpaas';
import { UserContextMiddleware } from '@lark-apaas/fullstack-nestjs-core';
@Module({
imports: [
// 配置 AuthZPaas 模块
AuthZPaasModule.forRoot({
permissionApi: {
timeout: 5000, // 权限 API 超时时间(毫秒), 默认 5000
},
}),
],
controllers: [/* ... */],
providers: [/* ... */],
})3. 异步配置(可选)
如果需要从 ConfigService 获取配置:
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot(),
AuthZPaasModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
permissionApi: {
timeout: 5000,
},
}),
}),
],
})
export class AppModule {}配置选项详解
核心配置
| 配置项 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| permissionApi | PermissionApiConfig | - | 权限 API 配置 |
装饰器使用
💡 装饰器是最常用的权限控制方式,推荐优先使用装饰器来进行权限检查,简单、直观、声明式。
1. CanRole - 角色检查
使用 @CanRole() 装饰器检查用户角色。
示例 1: 单个角色
import { Controller, Get } from '@nestjs/common';
import { CanRole } from '@lark-apaas/nestjs-authzpaas';
@Controller('demo')
export class DemoController {
// 需要 admin 角色
@CanRole(['admin'])
@Get('admin-only')
adminOnly() {
return {
message: '✅ 这是管理员专属接口',
timestamp: new Date(),
};
}
}示例 2: 多个角色(OR 逻辑)
@Controller('content')
export class ContentController {
// 拥有 admin 或 editor 任一角色即可访问
@CanRole(['admin', 'editor'])
@Get('manage')
manage() {
return '管理内容';
}
}核心组件
📚 核心组件是装饰器的底层实现,了解核心组件有助于深入理解权限系统的工作原理。
AuthZPaasGuard
AuthZPaasGuard 是一个全局守卫,会自动拦截所有请求并执行以下操作:
- 提取用户ID: 从
req.userContext.userId中提取用户ID - 角色检查: 如果使用了
@CanRole(),检查用户角色 - 注入权限数据: 将权限数据附加到
req.userPermissions
工作流程:
请求
↓
AuthZPaasGuard
↓
提取 userId
↓
检查角色/权限
↓
注入权限数据
↓
Controller配置: AuthZPaasGuard 会在模块导入时自动注册为全局守卫,无需手动配置。
PermissionService
PermissionService 提供权限管理的核心功能。
主要方法
import { Injectable } from '@nestjs/common';
import { PermissionService } from '@lark-apaas/nestjs-authzpaas';
@Injectable()
export class YourService {
constructor(private readonly permissionService: PermissionService) {}
async checkUserPermission(userId: string) {
// 1. 获取用户权限数据(带缓存)
const permissionData = await this.permissionService.getUserPermissions(userId);
console.log('用户角色:', permissionData.roles);
console.log('用户权限:', permissionData.permissions);
// 2. 获取用户的 CASL Ability 实例
const ability = await this.permissionService.getAbility(userId);
// 使用 Ability 检查权限
const canReadUser = ability.can('read', 'User');
const canCreateTask = ability.can('create', 'Task');
const canManageAll = ability.can('manage', 'all');
// 3. 检查权限要求
const hasPermission = await this.permissionService.checkPermissions(
[{ actions: ['read'], subject: 'User' }],
userId
);
// 4. 清除用户缓存(权限变更时)
this.permissionService.clearUserCache(userId);
// 5. 清除所有缓存
this.permissionService.clearAllCache();
// 6. 获取缓存统计
const stats = this.permissionService.getCacheStats();
console.log('缓存统计:', stats);
}
}工作原理
- 从
req.userContext.userId中提取用户ID - 调用
PermissionService.getUserPermissions()获取权限数据 - 将角色列表注入到
req.userContext.userRoles - 如果获取失败,记录警告但不阻塞请求(由 Guard 处理)
高级用法
1. 手动权限检查
在某些场景下,你可能需要在业务逻辑中手动检查权限,而不是使用装饰器:
import { Injectable } from '@nestjs/common';
import { PermissionService } from '@lark-apaas/nestjs-authzpaas';
@Injectable()
export class TasksService {
constructor(private readonly permissionService: PermissionService) {}
async processTask(userId: string, taskId: string) {
// 获取用户权限数据
const permissionData = await this.permissionService.getUserPermissions(userId);
// 检查权限
const canUpdate = await this.permissionService.checkPermissions(
[{ actions: ['update'], subject: 'Task' }],
userId
);
if (!canUpdate) {
throw new Error('没有权限');
}
// 执行业务逻辑
return '任务处理成功';
}
}2. 使用 CASL Ability 进行细粒度控制
CASL Ability 提供了更灵活的权限检查方式:
import { Injectable } from '@nestjs/common';
import { PermissionService, ROLE_SUBJECT } from '@lark-apaas/nestjs-authzpaas';
@Injectable()
export class ArticleService {
constructor(private readonly permissionService: PermissionService) {}
async canUserEditArticle(userId: string, articleId: string) {
const ability = await this.permissionService.getAbility(userId);
// 检查是否可以更新文章
if (ability.can('update', 'Article')) {
return true;
}
// 或者检查是否有 manage all 权限
if (ability.can('manage', 'all')) {
return true;
}
// 检查角色
if (ability.can('admin', ROLE_SUBJECT)) {
return true;
}
return false;
}
}权限 API 规范
请求格式
权限 API 的 endpoint 配置支持动态参数替换:
AuthZPaasModule.forRoot({
permissionApi: {
timeout: 5000,
},
})响应格式
您的权限 API 必须返回以下格式的 JSON 数据:
{
"roles": ["role_admin", "role_editor"],
"fetchedAt": "2024-01-01T00:00:00.000Z"
}字段说明
userId: 用户ID(必需)roles: 角色名称列表(必需)- 字符串数组
['role_admin', 'role_editor']
- 字符串数组
fetchedAt: 获取时间(可选,SDK 会自动添加)
错误处理
1. 常见异常类型
| 异常 | 说明 | HTTP 状态码 |
|------|------|------------|
| PermissionDeniedException.unauthenticated() | 未认证(无 userId) | 403 |
| PermissionDeniedException.roleRequired() | 缺少必需的角色 | 403 |
| PermissionDeniedException.permissionDenied() | 缺少必需的权限 | 403 |
| PermissionDeniedException.environmentRestricted() | 环境限制(如 IP 白名单) | 403 |
最佳实践
1. 使用 TypeScript 类型提示
充分利用 TypeScript 的类型系统:
import {
CanPermission,
CanRole,
PermissionService,
ROLE_SUBJECT
} from '@lark-apaas/nestjs-authzpaas';
// TypeScript 会自动提供代码提示和类型检查2. 测试建议
编写单元测试时,可以 mock PermissionService:
describe('TasksController', () => {
let controller: TasksController;
let permissionService: PermissionService;
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [TasksController],
providers: [
{
provide: PermissionService,
useValue: {
getUserPermissions: jest.fn(),
getAbility: jest.fn(),
checkPermissions: jest.fn(),
},
},
],
}).compile();
controller = module.get<TasksController>(TasksController);
permissionService = module.get<PermissionService>(PermissionService);
});
it('should check permissions', async () => {
jest.spyOn(permissionService, 'checkPermissions').mockResolvedValue(true);
// 测试逻辑...
});
});运行时权限 SDK(AuthorizationSDK)
AuthorizationSDK 提供对平台运行态权限 API 的封装,包括角色管理、成员管理和混合搜索三个子模块。SDK 自动从请求上下文获取 appId 和 userId,无需手动传递。
注入方式
import { Injectable } from '@nestjs/common';
import { AuthorizationSDK } from '@lark-apaas/nestjs-authzpaas';
@Injectable()
export class YourService {
constructor(private readonly authzSDK: AuthorizationSDK) {}
}AuthorizationSDK 暴露三个子对象:
| 属性 | 类型 | 说明 |
|------|------|------|
| authzSDK.roles | RolesAPI | 角色 CRUD |
| authzSDK.members | MembersAPI | 角色成员管理 |
| authzSDK.search | SearchAPI | 混合搜索(用户/部门/群组) |
roles — 角色管理
roles.list(params?)
获取角色列表。
const roles = await authzSDK.roles.list();
// 带参数
const roles = await authzSDK.roles.list({ needMember: true });| 参数 | 类型 | 说明 |
|------|------|------|
| needMember | boolean | 是否返回角色成员信息 |
| userID | string \| number | 可选,覆盖上下文中的 userId |
返回: ForceRoleDTO[]
roles.get(bizID)
获取单个角色详情。
const role = await authzSDK.roles.get('admin');返回: ForceRoleDTO
roles.create(params)
创建角色。
const result = await authzSDK.roles.create({
role: {
bizID: 'editor',
name: '编辑者',
description: '可编辑内容',
},
});
// result: { bizID: 'editor', apiID: '...' }| 参数 | 类型 | 说明 |
|------|------|------|
| role.bizID | string | 业务 ID(唯一标识) |
| role.name | string | 角色名称 |
| role.description | string? | 角色描述 |
返回: CreateRoleResponse({ bizID, apiID })
roles.update(bizID, params)
更新角色。
await authzSDK.roles.update('editor', {
role: { name: '高级编辑者', description: '更新后的描述' },
});roles.delete(bizID)
删除角色。
await authzSDK.roles.delete('editor');members — 成员管理
所有成员操作以角色的 bizID 为入口。成员通过 RoleMemberDTO 按类型分组:
interface RoleMemberDTO {
userList?: UserSimpleDTO[]; // 用户列表
departmentList?: DepartmentDTO[]; // 部门列表
groupChatList?: ChatSimpleDTO[]; // 群组列表
}members.list(bizID, params?)
获取角色下的成员列表。不传 type 时返回所有类型的成员。
// 获取所有类型成员
const res = await authzSDK.members.list('admin');
// 仅获取用户类型
const res = await authzSDK.members.list('admin', { type: 'User' });
// 分页
const res = await authzSDK.members.list('admin', { page: 1, page_size: 20 });| 参数 | 类型 | 说明 |
|------|------|------|
| type | MemberType? | 成员类型过滤:'User' | 'Department' | 'GroupChat' |
| page | number? | 页码 |
| page_size | number? | 每页数量 |
返回: ListMembersResponse
interface ListMembersResponse {
members: RoleMemberDTO;
total: number;
hasMore: boolean;
}members.add(bizID, params)
向角色添加成员。支持同时添加用户、部门、群组。
// 添加用户
await authzSDK.members.add('admin', {
members: { userList: [{ userID: '1826968659245100' }] },
});
// 添加部门
await authzSDK.members.add('admin', {
members: { departmentList: [{ id: '7579138586559286811' }] },
});
// 添加群组
await authzSDK.members.add('admin', {
members: { groupChatList: [{ chatID: '123456' }] },
});members.remove(bizID, params)
从角色移除成员。SDK 会自动补充 allEmployees、public、presetGroup 等默认参数。
await authzSDK.members.remove('admin', {
members: { userList: [{ userID: '1826968659245100' }] },
});members.clear(bizID)
清空角色下的所有成员。
await authzSDK.members.clear('admin');search — 混合搜索
search.search(params)
搜索用户、部门、群组。不传 filters 时自动使用默认参数(每种类型 pageSize=20)。
// 最简用法
const res = await authzSDK.search.search({ query: '张三' });
// 自定义过滤
const res = await authzSDK.search.search({
query: '张三',
filters: {
userParam: { commonParam: { searchable: true, pageSize: 10, offset: 0 } },
departmentParam: { commonParam: { searchable: true, pageSize: 5, offset: 0 } },
chatParam: { commonParam: { searchable: true, pageSize: 5, offset: 0 } },
},
includeExternalUser: false,
includeExternalGroup: true,
});| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| query | string | — | 搜索关键词(必填) |
| filters | FilterParams? | 三种类型各 pageSize=20 | 按类型配置搜索参数 |
| includeExternalUser | boolean? | false | 是否包含外部用户 |
| includeExternalGroup | boolean? | true | 是否包含外部群组 |
返回: SearchResponse
interface SearchResponse {
result: SearchResult;
}
interface SearchResult {
userResult?: { total?: number; items?: SearchUserEntity[] };
departmentResult?: { total?: number; items?: DepartmentEntity[] };
chatResult?: { total?: number; items?: SearchChatEntity[] };
}完整使用示例
import { Controller, Get, Post, Delete, Param, Body, Query } from '@nestjs/common';
import { AuthorizationSDK } from '@lark-apaas/nestjs-authzpaas';
@Controller('api/authorization')
export class AuthorizationController {
constructor(private readonly authzSDK: AuthorizationSDK) {}
@Get('roles')
listRoles() {
return this.authzSDK.roles.list();
}
@Post('roles')
createRole(@Body() dto: { role: { bizID: string; name: string; description?: string } }) {
return this.authzSDK.roles.create(dto);
}
@Get('roles/:bizID/members')
listMembers(@Param('bizID') bizID: string, @Query('type') type?: string) {
return this.authzSDK.members.list(bizID, { type: type as any });
}
@Post('roles/:bizID/members')
addMembers(@Param('bizID') bizID: string, @Body() dto: { members: any }) {
return this.authzSDK.members.add(bizID, dto);
}
@Post('search')
search(@Body() dto: { query: string }) {
return this.authzSDK.search.search(dto);
}
}完整示例
查看 examples/node-demo 目录获取完整的工作示例,包括:
- ✅ 模块配置和中间件设置
- ✅ Mock 权限 API 实现
- ✅ 角色和权限装饰器使用
- ✅ 手动权限检查
- ✅ CASL Ability 使用
- ✅ 测试脚本
运行示例:
cd examples/node-demo
yarn install
yarn start
# 测试不同用户的权限
curl -H "x-user-id: user-admin" http://localhost:3000/demo/admin-only
curl -H "x-user-id: user-normal" http://localhost:3000/demo/reader-onlyLicense
MIT
