Skip to content

第 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 的局限性在于:

  1. 静态性:权限与角色紧密耦合,难以处理复杂的业务规则
  2. 扩展性差:当需要基于更多属性(如时间、地理位置等)进行授权时,RBAC 显得力不从心
  3. 维护困难:随着角色和权限增加,代码中会出现大量条件判断

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;
  }
}

创业团队行动清单

  1. 立即行动

    • 在所有资源实体中添加 ownerId 字段
    • 实现简单的所有权检查机制
    • 创建统一的授权服务接口
  2. 一周内完成

    • 实现管理员权限检查
    • 在关键接口中集成授权检查
    • 建立权限不足的错误处理机制
  3. 一月内完善

    • 根据业务需求选择合适的授权模型
    • 实现更复杂的策略引擎
    • 添加权限审计日志

总结

授权系统是保护系统资源安全的关键,正确的实现需要:

  1. 选择合适的模型:根据业务复杂度选择 RBAC、ABAC 或 ReBAC
  2. 抽象权限检查:提供统一的 can(user, action, resource) 接口
  3. 渐进式实现:初期实现简单所有权检查,预留扩展接口
  4. 考虑开源方案:对于复杂场景,可以考虑 Casbin 或 SpiceDB

在下一篇文章中,我们将探讨日志与审计系统,这是满足合规要求和问题排查的重要工具。