第 3 篇:授权(AuthZ)—— 从 if(user.id) 到策略引擎的演进路径
在前两篇文章中,我们探讨了多租户上下文和认证系统。现在,让我们关注另一个关键的横切关注点:授权(Authorization)。
授权决定了经过认证的用户能够访问哪些资源和执行哪些操作。许多团队在实现授权时,往往使用简单的条件判断(如 if(user.id === resource.ownerId)),这种方式在初期看似简单,但随着业务复杂度增加,会变得难以维护和扩展。
为什么 RBAC 不够?何时需要 ABAC/ReBAC?
RBAC(基于角色的访问控制)的局限性
RBAC 是最常见的授权模型,它将权限分配给角色,再将角色分配给用户:
typescript
// 简单的 RBAC 实现
export enum Role {
ADMIN = 'admin',
MANAGER = 'manager',
USER = 'user',
}
export enum Permission {
CREATE_ORDER = 'create_order',
VIEW_ORDER = 'view_order',
UPDATE_ORDER = 'update_order',
DELETE_ORDER = 'delete_order',
}
// 用户实体
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
@Column({
type: 'enum',
enum: Role,
array: true,
})
roles: Role[];
}
// 简单的 RBAC 检查
@Injectable()
export class OrderService {
async canViewOrder(user: User, order: Order): Promise<boolean> {
// 管理员可以查看所有订单
if (user.roles.includes(Role.ADMIN)) {
return true;
}
// 普通用户只能查看自己的订单
if (user.roles.includes(Role.USER) && order.userId === user.id) {
return true;
}
return false;
}
}RBAC 的局限性在于:
- 静态性:权限与角色紧密耦合,难以处理复杂的业务规则
- 扩展性差:当需要基于更多属性(如时间、地理位置等)进行授权时,RBAC 显得力不从心
- 维护困难:随着角色和权限增加,代码中会出现大量条件判断
ABAC(基于属性的访问控制)的优势
ABAC 通过评估用户、资源和环境的属性来做出授权决策:
typescript
// ABAC 策略示例
export interface AuthorizationContext {
user: {
id: string;
roles: string[];
department?: string;
level?: number;
};
resource: {
id: string;
type: string;
ownerId: string;
department?: string;
sensitivity?: 'public' | 'confidential' | 'secret';
};
action: string;
environment?: {
time?: Date;
ip?: string;
userAgent?: string;
};
}
// ABAC 策略引擎
@Injectable()
export class PolicyEngine {
private policies: ((context: AuthorizationContext) => boolean)[] = [];
registerPolicy(policy: (context: AuthorizationContext) => boolean) {
this.policies.push(policy);
}
async authorize(context: AuthorizationContext): Promise<boolean> {
// 按顺序评估所有策略
for (const policy of this.policies) {
if (!policy(context)) {
return false;
}
}
return true;
}
}
// 具体策略实现
const adminPolicy = (context: AuthorizationContext): boolean => {
return context.user.roles.includes('admin');
};
const ownerPolicy = (context: AuthorizationContext): boolean => {
return context.resource.ownerId === context.user.id;
};
const departmentPolicy = (context: AuthorizationContext): boolean => {
// 同部门用户可以查看非机密资源
return (
context.user.department === context.resource.department &&
context.resource.sensitivity !== 'secret'
);
};
const timeBasedPolicy = (context: AuthorizationContext): boolean => {
// 工作时间才能访问
const now = context.environment?.time || new Date();
const hour = now.getHours();
return hour >= 9 && hour < 18;
};
// 注册策略
const policyEngine = new PolicyEngine();
policyEngine.registerPolicy(adminPolicy);
policyEngine.registerPolicy(ownerPolicy);
policyEngine.registerPolicy(departmentPolicy);
policyEngine.registerPolicy(timeBasedPolicy);ReBAC(基于关系的访问控制)的适用场景
ReBAC 通过用户与资源之间的关系来决定访问权限,适用于社交网络、协作平台等场景:
typescript
// 关系实体
@Entity()
export class Relationship {
@PrimaryGeneratedColumn()
id: number;
@Column()
subjectId: string; // 用户ID
@Column()
objectId: string; // 资源ID
@Column()
relation: string; // 关系类型:owner, editor, viewer, commenter
@Column()
tenantId: string;
}
// ReBAC 授权服务
@Injectable()
export class RebacAuthorizationService {
constructor(
@InjectRepository(Relationship)
private readonly relationshipRepository: Repository<Relationship>,
) {}
async checkPermission(
userId: string,
resourceId: string,
requiredRelation: string,
tenantId: string,
): Promise<boolean> {
// 检查直接关系
const directRelation = await this.relationshipRepository.findOne({
where: {
subjectId: userId,
objectId: resourceId,
relation: requiredRelation,
tenantId,
},
});
if (directRelation) {
return true;
}
// 检查传递关系(如 editor 也具有 viewer 权限)
const relationHierarchy = {
owner: ['editor', 'viewer', 'commenter'],
editor: ['viewer', 'commenter'],
commenter: ['viewer'],
viewer: [],
};
const allowedRelations = [requiredRelation, ...(relationHierarchy[requiredRelation] || [])];
const relation = await this.relationshipRepository.findOne({
where: {
subjectId: userId,
objectId: resourceId,
relation: In(allowedRelations),
tenantId,
},
});
return !!relation;
}
}权限检查函数的抽象:can(user, action, resource)
为了简化授权检查,我们可以抽象出一个通用的权限检查函数:
typescript
// 权限检查接口
export interface PermissionChecker {
can(user: any, action: string, resource: any): Promise<boolean>;
}
// 统一的授权服务
@Injectable()
export class AuthorizationService implements PermissionChecker {
constructor(
private readonly policyEngine: PolicyEngine,
private readonly rebacService: RebacAuthorizationService,
) {}
async can(user: any, action: string, resource: any): Promise<boolean> {
// 构建授权上下文
const context: AuthorizationContext = {
user: {
id: user.id,
roles: user.roles,
department: user.department,
level: user.level,
},
resource: {
id: resource.id,
type: resource.constructor.name,
ownerId: resource.ownerId,
department: resource.department,
sensitivity: resource.sensitivity,
},
action: action,
environment: {
time: new Date(),
ip: undefined, // 可从请求中获取
userAgent: undefined, // 可从请求中获取
},
};
// 使用策略引擎进行授权
return this.policyEngine.authorize(context);
}
// 针对特定资源类型的便捷方法
async canViewOrder(user: any, order: any): Promise<boolean> {
return this.can(user, 'view', order);
}
async canUpdateOrder(user: any, order: any): Promise<boolean> {
return this.can(user, 'update', order);
}
async canDeleteOrder(user: any, order: any): Promise<boolean> {
return this.can(user, 'delete', order);
}
}
// 在控制器中使用
@Controller('orders')
export class OrderController {
constructor(
private readonly orderService: OrderService,
private readonly authService: AuthorizationService,
@Inject(REQUEST) private readonly request: Request,
) {}
@Get(':id')
async getOrder(@Param('id') id: string) {
const order = await this.orderService.findById(id);
const user = (this.request as any).user;
if (!await this.authService.canViewOrder(user, order)) {
throw new ForbiddenException('You do not have permission to view this order');
}
return order;
}
@Put(':id')
async updateOrder(@Param('id') id: string, @Body() updateOrderDto: UpdateOrderDto) {
const order = await this.orderService.findById(id);
const user = (this.request as any).user;
if (!await this.authService.canUpdateOrder(user, order)) {
throw new ForbiddenException('You do not have permission to update this order');
}
return this.orderService.update(id, updateOrderDto);
}
}开源方案对比:Casbin vs SpiceDB vs 自研 ACL 表
Casbin
Casbin 是一个强大的授权库,支持多种访问控制模型:
typescript
// Casbin 策略示例
// policy.csv
// p, admin, order, read
// p, admin, order, write
// p, user, order, read
// g, alice, admin
import { newEnforcer } from 'casbin';
@Injectable()
export class CasbinAuthorizationService {
private enforcer: any;
async onModuleInit() {
// 初始化 Casbin 执行器
this.enforcer = await newEnforcer('model.conf', 'policy.csv');
}
async checkPermission(user: string, resource: string, action: string): Promise<boolean> {
return this.enforcer.enforce(user, resource, action);
}
}SpiceDB
SpiceDB 是 Google Zanzibar 的开源实现,专为大规模授权设计:
typescript
// SpiceDB 客户端示例
import { v1 } from '@authzed/authzed-node';
@Injectable()
export class SpiceDBAuthorizationService {
private client: v1.ZedClient;
constructor() {
this.client = v1.NewClient('spicedb:50051');
}
async checkPermission(
subjectId: string,
resourceId: string,
permission: string,
): Promise<boolean> {
const req = v1.CheckPermissionRequest.create({
subject: {
object: {
objectType: 'user',
objectId: subjectId,
},
},
resource: {
objectType: 'order',
objectId: resourceId,
},
permission,
});
const response = await this.client.checkPermission(req);
return response.permissionship === v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION;
}
}自研 ACL 表
对于简单场景,可以自研轻量级 ACL 系统:
typescript
// ACL 实体
@Entity()
export class AccessControlList {
@PrimaryGeneratedColumn()
id: number;
@Column()
userId: string;
@Column()
resourceId: string;
@Column()
resourceType: string;
@Column({
type: 'enum',
enum: ['read', 'write', 'delete', 'admin'],
})
permission: string;
@Column()
grantedAt: Date;
@Column({ default: true })
isActive: boolean;
}
// ACL 服务
@Injectable()
export class AclService {
constructor(
@InjectRepository(AccessControlList)
private readonly aclRepository: Repository<AccessControlList>,
) {}
async grantPermission(
userId: string,
resourceId: string,
resourceType: string,
permission: string,
): Promise<void> {
const acl = new AccessControlList();
acl.userId = userId;
acl.resourceId = resourceId;
acl.resourceType = resourceType;
acl.permission = permission;
acl.grantedAt = new Date();
acl.isActive = true;
await this.aclRepository.save(acl);
}
async checkPermission(
userId: string,
resourceId: string,
resourceType: string,
requiredPermission: string,
): Promise<boolean> {
const permissionHierarchy = {
read: [],
write: ['read'],
delete: ['read'],
admin: ['read', 'write', 'delete'],
};
const requiredPermissions = [
requiredPermission,
...(permissionHierarchy[requiredPermission] || []),
];
const acl = await this.aclRepository.findOne({
where: {
userId,
resourceId,
resourceType,
permission: In(requiredPermissions),
isActive: true,
},
});
return !!acl;
}
}创业初期建议:先做"资源归属字段 + 简单策略",预留接口
对于创业初期的团队,建议采用渐进式的方法实现授权系统:
typescript
// 第一阶段:资源归属字段 + 简单策略
@Entity()
export class Order {
@PrimaryGeneratedColumn()
id: number;
@Column()
tenantId: string;
@Column()
ownerId: string; // 资源归属字段
@Column()
amount: number;
@Column()
status: string;
}
@Injectable()
export class SimpleAuthorizationService {
// 简单的所有权检查
async checkOwnership(userId: string, resource: { ownerId: string }): Promise<boolean> {
return userId === resource.ownerId;
}
// 管理员检查
async checkAdmin(user: { roles: string[] }): Promise<boolean> {
return user.roles.includes('admin');
}
// 组合检查
async authorize(
user: { id: string; roles: string[] },
action: string,
resource: { ownerId: string },
): Promise<boolean> {
// 管理员有所有权限
if (await this.checkAdmin(user)) {
return true;
}
// 根据操作类型检查权限
switch (action) {
case 'view':
case 'update':
case 'delete':
return await this.checkOwnership(user.id, resource);
default:
return false;
}
}
}
// 预留扩展接口
export abstract class AuthorizationStrategy {
abstract authorize(user: any, action: string, resource: any): Promise<boolean>;
}
// 可以在未来替换为更复杂的实现
@Injectable()
export class FutureAuthorizationService extends AuthorizationStrategy {
async authorize(user: any, action: string, resource: any): Promise<boolean> {
// 未来可以集成 Casbin、SpiceDB 或自定义策略引擎
return true;
}
}创业团队行动清单
立即行动:
- 在所有资源实体中添加
ownerId字段 - 实现简单的所有权检查机制
- 创建统一的授权服务接口
- 在所有资源实体中添加
一周内完成:
- 实现管理员权限检查
- 在关键接口中集成授权检查
- 建立权限不足的错误处理机制
一月内完善:
- 根据业务需求选择合适的授权模型
- 实现更复杂的策略引擎
- 添加权限审计日志
总结
授权系统是保护系统资源安全的关键,正确的实现需要:
- 选择合适的模型:根据业务复杂度选择 RBAC、ABAC 或 ReBAC
- 抽象权限检查:提供统一的
can(user, action, resource)接口 - 渐进式实现:初期实现简单所有权检查,预留扩展接口
- 考虑开源方案:对于复杂场景,可以考虑 Casbin 或 SpiceDB
在下一篇文章中,我们将探讨日志与审计系统,这是满足合规要求和问题排查的重要工具。