mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1060 字
3 分钟
为 Web 应用构建可扩展的访问控制能力
2022-08-27

引言#

在 Web 应用开发中,访问控制(Access Control)是保障数据安全和功能权限的核心机制。然而,随着业务复杂度的增加,很多开发者会遇到以下问题:

  • 权限混乱:角色和资源的关系难以维护,代码中遍布 if-else 判断。
  • 扩展困难:新增功能或用户类型需要大量重构。
  • 安全漏洞:因权限逻辑分散,容易遗漏关键检查点。

本文将深入探讨如何设计一套可扩展、易维护、高安全的访问控制系统,涵盖核心模型、实现策略和实战代码示例。

一、访问控制的核心模型#

1.1 RBAC(基于角色的访问控制)#

  • 角色(Role):权限的集合(如 Admin、Editor)。
  • 权限(Permission):对资源的操作(如 article、user)。
  • 用户(User):通过分配角色继承权限。

适用场景:角色层级固定、权限变动较少的系统(如企业内部系统)。

1.2 ABAC(基于属性的访问控制)#

  • 动态决策:根据用户属性(部门)、资源属性(所有者)、环境属性(时间、IP)等动态授权。
  • 示例规则:允许用户编辑文章,当且仅当用户是文章所有者且文章状态为草稿。

适用场景:需要细粒度、动态权限的场景(如云平台、多租户系统)。

1.3 混合模型#

结合 RBAC 和 ABAC,例如:通过角色分配基础权限,再通过属性动态调整。

二、可扩展访问控制的设计原则#

2.1 最小权限原则#

  • 用户仅拥有完成工作所需的最低权限。
  • 实现方法:角色初始化时仅包含必要权限,按需扩展。

2.2 中心化策略管理#

  • 将权限规则从代码中抽离,存储到数据库或配置文件。
  • 优势:修改权限无需重新部署代码。

2.3 分层设计#

  • 策略层:定义权限规则(如 JSON 或 DSL)。
  • 执行层:在 API 网关、中间件或服务内部校验权限。
  • 审计层:记录访问日志,定期分析异常行为。

2.4 支持动态角色和权限#

允许通过管理界面动态创建角色、分配权限。

三、实战代码实现#

本节将基于前文的理论模型,展示如何用代码实现一个可扩展的访问控制系统。将使用 TypeScript 构建权限中间件和动态策略引擎。

3.1 权限中间件实现#

以下是基于 Express.js 的权限中间件实现,支持 RBAC 和 ABAC 混合模式:

types/permission.ts
interface User {
id: string;
roles: string[];
attributes: Record<string, unknown>;
}
interface Resource {
id: string;
type: string;
ownerId?: string;
attributes: Record<string, unknown>;
}
interface Permission {
action: string; // 如 'read', 'write', 'delete'
resource: string; // 如 'article', 'user'
conditions?: PolicyCondition[];
}
interface PolicyCondition {
type: "user_attr" | "resource_attr" | "environment";
key: string;
operator: "eq" | "neq" | "in" | "gt" | "lt" | "contains";
value: unknown;
}
middleware/permission.ts
import { Request, Response, NextFunction } from "express";
// 权限缓存(生产环境应使用 Redis)
const permissionCache = new Map<string, Set<string>>();
class PermissionMiddleware {
/**
* RBAC 基础权限检查中间件
* 检查用户角色是否拥有所需权限
*/
static requirePermission(permission: string) {
return async (req: Request, res: Response, next: NextFunction) => {
const user = req.user as User | undefined;
if (!user) {
return res.status(401).json({ error: "未授权访问" });
}
const hasPermission = await this.checkUserPermission(user, permission);
if (!hasPermission) {
return res.status(403).json({ error: "权限不足" });
}
next();
};
}
/**
* ABAC 动态权限检查中间件
* 根据属性条件动态判断权限
*/
static requireResourceAccess(action: string, resourceType: string) {
return async (req: Request, res: Response, next: NextFunction) => {
const user = req.user as User | undefined;
const resourceId = req.params.id;
if (!user) {
return res.status(401).json({ error: "未授权访问" });
}
// 获取资源信息(实际项目中从数据库获取)
const resource = await this.getResource(resourceId, resourceType);
if (!resource) {
return res.status(404).json({ error: "资源不存在" });
}
const hasAccess = await this.evaluateABAC(user, resource, action);
if (!hasAccess) {
return res.status(403).json({ error: "无权访问此资源" });
}
// 将资源附加到请求对象,供后续处理使用
req.resource = resource;
next();
};
}
/**
* 检查用户是否拥有指定权限(RBAC)
*/
private static async checkUserPermission(
user: User,
permission: string
): Promise<boolean> {
// 从缓存获取用户权限集
const cacheKey = `user:${user.id}:permissions`;
let permissions = permissionCache.get(cacheKey);
if (!permissions) {
// 从数据库加载角色权限
permissions = await this.loadUserPermissions(user.roles);
permissionCache.set(cacheKey, permissions);
}
return permissions.has(permission);
}
/**
* ABAC 条件评估
*/
private static async evaluateABAC(
user: User,
resource: Resource,
action: string
): Promise<boolean> {
// 获取适用于此资源和操作的策略
const policies = await this.getApplicablePolicies(action, resource.type);
for (const policy of policies) {
const result = this.evaluateConditions(policy.conditions, user, resource);
if (result) {
return policy.effect === "allow";
}
}
// 默认拒绝
return false;
}
/**
* 评估条件列表
*/
private static evaluateConditions(
conditions: PolicyCondition[],
user: User,
resource: Resource
): boolean {
return conditions.every(condition => {
let actualValue: unknown;
switch (condition.type) {
case "user_attr":
actualValue = user.attributes[condition.key];
break;
case "resource_attr":
actualValue = resource.attributes[condition.key];
break;
case "environment":
actualValue = this.getEnvironmentValue(condition.key);
break;
}
return this.compareValues(
actualValue,
condition.operator,
condition.value
);
});
}
/**
* 值比较操作
*/
private static compareValues(
actual: unknown,
operator: string,
expected: unknown
): boolean {
switch (operator) {
case "eq":
return actual === expected;
case "neq":
return actual !== expected;
case "in":
return Array.isArray(expected) && expected.includes(actual);
case "gt":
return Number(actual) > Number(expected);
case "lt":
return Number(actual) < Number(expected);
case "contains":
return String(actual).includes(String(expected));
default:
return false;
}
}
// 占位方法(实际项目中实现数据库交互)
private static async loadUserPermissions(
roles: string[]
): Promise<Set<string>> {
// 模拟:从数据库加载角色对应的权限
const rolePermissions: Record<string, string[]> = {
admin: ["article:read", "article:write", "article:delete", "user:manage"],
editor: ["article:read", "article:write"],
viewer: ["article:read"],
};
const permissions = new Set<string>();
roles.forEach(role => {
rolePermissions[role]?.forEach(p => permissions.add(p));
});
return permissions;
}
private static async getResource(
id: string,
type: string
): Promise<Resource | null> {
// 实际项目中从数据库获取
return { id, type, attributes: {} };
}
private static async getApplicablePolicies(
action: string,
resourceType: string
) {
// 实际项目中从策略存储加载
return [];
}
private static getEnvironmentValue(key: string): unknown {
// 返回环境属性,如当前时间、IP 等
const envValues: Record<string, unknown> = {
currentTime: new Date(),
ip: "127.0.0.1",
};
return envValues[key];
}
}
export { PermissionMiddleware };

3.2 使用示例#

app.ts
import express from "express";
import { PermissionMiddleware } from "./middleware/permission";
const app = express();
// RBAC 示例:检查用户是否有文章编辑权限
app.post(
"/articles",
PermissionMiddleware.requirePermission("article:write"),
(req, res) => {
// 创建文章逻辑
res.json({ message: "文章创建成功" });
}
);
// ABAC 示例:动态检查用户对特定资源的访问权限
app.put(
"/articles/:id",
PermissionMiddleware.requireResourceAccess("write", "article"),
(req, res) => {
// 更新文章逻辑
res.json({ message: "文章更新成功" });
}
);

3.3 动态策略引擎#

策略引擎允许在运行时配置权限规则,无需修改代码:

engine/policy-engine.ts
interface Policy {
id: string;
name: string;
effect: "allow" | "deny";
resource: string;
action: string | string[];
conditions: PolicyCondition[];
priority: number; // 优先级,数字越小优先级越高
}
interface PolicyStore {
getPolicies(): Promise<Policy[]>;
addPolicy(policy: Policy): Promise<void>;
updatePolicy(id: string, policy: Partial<Policy>): Promise<void>;
deletePolicy(id: string): Promise<void>;
}
class PolicyEngine {
private policies: Policy[] = [];
private policyStore: PolicyStore;
constructor(store: PolicyStore) {
this.policyStore = store;
}
/**
* 初始化:加载所有策略
*/
async initialize(): Promise<void> {
this.policies = await this.policyStore.getPolicies();
// 按优先级排序
this.policies.sort((a, b) => a.priority - b.priority);
}
/**
* 刷新策略缓存
*/
async refresh(): Promise<void> {
await this.initialize();
}
/**
* 添加新策略
*/
async addPolicy(policy: Policy): Promise<void> {
await this.policyStore.addPolicy(policy);
this.policies.push(policy);
this.policies.sort((a, b) => a.priority - b.priority);
}
/**
* 核心决策方法
*/
async decide(
user: User,
resource: Resource,
action: string
): Promise<{ allowed: boolean; reason: string }> {
for (const policy of this.policies) {
// 检查策略是否适用于此资源和操作
if (!this.matchesPolicy(policy, resource.type, action)) {
continue;
}
// 评估条件
const conditionsMet = this.evaluateConditions(
policy.conditions,
user,
resource
);
if (conditionsMet) {
return {
allowed: policy.effect === "allow",
reason: `策略 [${policy.name}] 生效`,
};
}
}
// 默认拒绝
return {
allowed: false,
reason: "未匹配任何允许策略,默认拒绝",
};
}
/**
* 检查策略是否匹配资源和操作
*/
private matchesPolicy(
policy: Policy,
resourceType: string,
action: string
): boolean {
const resourceMatch =
policy.resource === "*" || policy.resource === resourceType;
const actionMatch = Array.isArray(policy.action)
? policy.action.includes(action) || policy.action.includes("*")
: policy.action === action || policy.action === "*";
return resourceMatch && actionMatch;
}
/**
* 评估策略条件
*/
private evaluateConditions(
conditions: PolicyCondition[],
user: User,
resource: Resource
): boolean {
if (!conditions || conditions.length === 0) {
return true; // 无条件策略直接通过
}
return conditions.every(condition => {
return this.evaluateSingleCondition(condition, user, resource);
});
}
/**
* 评估单个条件
*/
private evaluateSingleCondition(
condition: PolicyCondition,
user: User,
resource: Resource
): boolean {
let actualValue: unknown;
switch (condition.type) {
case "user_attr":
// 支持嵌套属性访问,如 'department.name'
actualValue = this.getNestedValue(user.attributes, condition.key);
break;
case "resource_attr":
actualValue = this.getNestedValue(resource.attributes, condition.key);
// 特殊处理:资源所有者检查
if (condition.key === "ownerId" && condition.operator === "eq") {
return resource.ownerId === user.id;
}
break;
case "environment":
actualValue = this.getEnvironmentContext(condition.key);
break;
default:
return false;
}
return this.compare(actualValue, condition.operator, condition.value);
}
/**
* 获取嵌套属性值
*/
private getNestedValue(obj: Record<string, unknown>, path: string): unknown {
return path.split(".").reduce((current, key) => {
return current && typeof current === "object"
? (current as Record<string, unknown>)[key]
: undefined;
}, obj as unknown);
}
/**
* 获取环境上下文
*/
private getEnvironmentContext(key: string): unknown {
const context: Record<string, unknown> = {
currentTime: new Date(),
currentHour: new Date().getHours(),
isWorkingHour: this.isWorkingHour(),
currentDayOfWeek: new Date().getDay(),
};
return context[key];
}
private isWorkingHour(): boolean {
const hour = new Date().getHours();
return hour >= 9 && hour <= 18;
}
/**
* 比较操作
*/
private compare(
actual: unknown,
operator: string,
expected: unknown
): boolean {
switch (operator) {
case "eq":
return actual === expected;
case "neq":
return actual !== expected;
case "in":
return Array.isArray(expected) && expected.includes(actual);
case "not_in":
return Array.isArray(expected) && !expected.includes(actual);
case "gt":
return Number(actual) > Number(expected);
case "gte":
return Number(actual) >= Number(expected);
case "lt":
return Number(actual) < Number(expected);
case "lte":
return Number(actual) <= Number(expected);
case "contains":
return String(actual).includes(String(expected));
case "starts_with":
return String(actual).startsWith(String(expected));
case "ends_with":
return String(actual).endsWith(String(expected));
case "regex":
return new RegExp(String(expected)).test(String(actual));
default:
return false;
}
}
}
export { PolicyEngine, Policy };

3.4 策略配置示例#

以下是一个完整的策略配置示例,展示如何通过 JSON 配置实现细粒度的权限控制:

policies/article-policies.json
[
{
"id": "owner-full-access",
"name": "资源所有者完全访问",
"effect": "allow",
"resource": "article",
"action": ["read", "write", "delete"],
"conditions": [
{
"type": "resource_attr",
"key": "ownerId",
"operator": "eq",
"value": "${user.id}"
}
],
"priority": 10
},
{
"id": "admin-full-access",
"name": "管理员完全访问",
"effect": "allow",
"resource": "*",
"action": "*",
"conditions": [
{
"type": "user_attr",
"key": "role",
"operator": "eq",
"value": "admin"
}
],
"priority": 5
},
{
"id": "working-hours-only",
"name": "工作时间限制",
"effect": "deny",
"resource": "article",
"action": "delete",
"conditions": [
{
"type": "environment",
"key": "isWorkingHour",
"operator": "eq",
"value": false
}
],
"priority": 15
},
{
"id": "editor-write-access",
"name": "编辑者写入权限",
"effect": "allow",
"resource": "article",
"action": ["read", "write"],
"conditions": [
{
"type": "user_attr",
"key": "role",
"operator": "in",
"value": ["editor", "admin"]
}
],
"priority": 20
}
]

3.5 完整集成示例#

app-integrated.ts
import express from "express";
import { PermissionMiddleware } from "./middleware/permission";
import { PolicyEngine } from "./engine/policy-engine";
// 策略存储实现(示例使用内存存储)
class MemoryPolicyStore implements PolicyStore {
private policies: Map<string, Policy> = new Map();
async getPolicies(): Promise<Policy[]> {
return Array.from(this.policies.values());
}
async addPolicy(policy: Policy): Promise<void> {
this.policies.set(policy.id, policy);
}
async updatePolicy(id: string, policy: Partial<Policy>): Promise<void> {
const existing = this.policies.get(id);
if (existing) {
this.policies.set(id, { ...existing, ...policy });
}
}
async deletePolicy(id: string): Promise<void> {
this.policies.delete(id);
}
}
// 初始化应用
async function createApp() {
const app = express();
app.use(express.json());
// 初始化策略引擎
const policyStore = new MemoryPolicyStore();
const policyEngine = new PolicyEngine(policyStore);
// 加载初始策略
await policyEngine.initialize();
// 将策略引擎注入到中间件
app.locals.policyEngine = policyEngine;
// 受保护的路由
app.delete(
"/articles/:id",
async (req, res, next) => {
const user = req.user as User;
const resource = await getResourceById(req.params.id);
const decision = await policyEngine.decide(user, resource, "delete");
if (!decision.allowed) {
return res.status(403).json({
error: "权限不足",
reason: decision.reason,
});
}
next();
},
(req, res) => {
// 删除文章逻辑
res.json({ message: "文章已删除" });
}
);
// 管理接口:动态添加策略
app.post("/admin/policies", async (req, res) => {
const policy: Policy = req.body;
await policyEngine.addPolicy(policy);
res.json({ message: "策略添加成功", policy });
});
return app;
}

3.6 最佳实践建议#

  1. 分层缓存:权限数据应采用多级缓存策略

    • 第一层:内存缓存(进程内)
    • 第二层:分布式缓存(Redis)
    • 第三层:数据库
  2. 策略版本管理:保存策略变更历史,支持快速回滚

  3. 审计日志:记录所有权限决策,便于事后追溯

// 审计日志记录
interface AuditLog {
timestamp: Date;
userId: string;
action: string;
resource: string;
resourceId: string;
decision: boolean;
reason: string;
ipAddress: string;
}
async function logAuditDecision(log: AuditLog): Promise<void> {
// 写入审计日志系统
console.log("[AUDIT]", JSON.stringify(log));
}
  1. 性能优化:对于高频权限检查,可采用预计算策略
    • 预计算用户权限矩阵
    • 使用位图存储权限
    • 批量权限检查接口

通过以上实现,您可以构建一套灵活、可扩展的访问控制系统,既支持 RBAC 的角色管理便捷性,又具备 ABAC 的细粒度控制能力。

参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

为 Web 应用构建可扩展的访问控制能力
https://blog.souloss.com/posts/web/web-access-control/
作者
Souloss
发布于
2022-08-27
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时