工程实践 架构心得

鉴权与授权:银行系统的权限管控体系

从 RBAC 到 ABAC,从 JWT Claims 到行级权限,详解银行系统中完整的权限管控体系设计。

发布于 2026/03/20 更新于 2026/03/20 2 分钟

“权限设计错了,比没有权限更危险——它给了错误的信任。“

前言

鉴权(Authentication)和授权(Authorization)是两个概念:

  • 鉴权:验证”你是谁”(Who are you?)→ JWT、Session、OAuth2 Token
  • 授权:判断”你能做什么”(What can you do?)→ RBAC、ABAC、行级权限

银行系统的权限设计有三个硬约束:

  1. 最小权限原则:每个用户/系统只能访问必要的资源
  2. 职责分离:敏感操作需要多人授权(如大额转账需要主管审批)
  3. 可审计:所有权限变更和访问行为必须记录

1. RBAC:基于角色的访问控制

RBAC 是银行系统权限管理的基础模型。

1.1 RBAC 模型

用户 ──属于──→ 角色 ──拥有──→ 权限

                    ├── 角色A: 读取账户余额
                    ├── 角色A: 查看交易历史
                    └── 角色A: 无(转账)→ 不能执行

用户A(柜员)    → 角色: TELLER     → 权限: [查账、限额内转账]
用户B(主管)    → 角色: SUPERVISOR → 权限: [查账、任意额转账、审批]
用户C(审计员)  → 角色: AUDITOR    → 权限: [只读所有账户]

1.2 数据库模型

-- 权限表
CREATE TABLE permission (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    code        VARCHAR(64) UNIQUE NOT NULL,  -- 权限编码:PAYMENT:TRANSFER:ANY
    name        VARCHAR(128) NOT NULL,          -- 权限名称
    resource    VARCHAR(64),                   -- 资源:PAYMENT
    action      VARCHAR(32),                   -- 操作:TRANSFER
    scope       VARCHAR(32),                   -- 范围:ANY、SELF、Branch
    description VARCHAR(256),
    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 角色表
CREATE TABLE role (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    code        VARCHAR(64) UNIQUE NOT NULL,
    name        VARCHAR(128) NOT NULL,
    description VARCHAR(256),
    is_active   BOOLEAN DEFAULT TRUE,
    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 角色-权限关联表
CREATE TABLE role_permission (
    role_id       BIGINT NOT NULL,
    permission_id BIGINT NOT NULL,
    PRIMARY KEY (role_id, permission_id)
);

-- 用户-角色关联表
CREATE TABLE user_role (
    user_id  BIGINT NOT NULL,
    role_id  BIGINT NOT NULL,
    branch_id BIGINT,         -- 银行特有:按分行授权
    valid_from DATETIME,
    valid_to   DATETIME,
    PRIMARY KEY (user_id, role_id)
);

1.3 Spring Security 集成

@Component
@RequiredArgsConstructor
public class BankPermissionEvaluator implements PermissionEvaluator {

    private final RoleDao roleDao;
    private final PermissionDao permissionDao;

    @Override
    public boolean hasPermission(Authentication auth,
                                  Object targetDomainObject,
                                  Object permission) {
        UserDetails user = (UserDetails) auth.getPrincipal();
        String permissionCode = (String) permission;

        // 1. 检查用户是否持有该权限
        boolean hasDirect = user.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().equals(permissionCode));

        if (hasDirect) return true;

        // 2. 银行特殊:检查范围权限(如:只能操作本分行账户)
        if (requiresBranchScope(permissionCode) && targetDomainObject instanceof Account account) {
            return hasBranchAccess(user, account.getBranchId());
        }

        // 3. 检查金额范围权限
        if (targetDomainObject instanceof TransferRequest transfer) {
            return withinAmountLimit(user, transfer.getAmount());
        }

        return false;
    }

    private boolean hasBranchAccess(UserDetails user, Long targetBranchId) {
        // 用户持有角色的 branch_id 必须覆盖目标账户的分行
        // 例如:柜员 TELLER_BRANCH_001 只能操作 BRANCH_001 的账户
        return user.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().startsWith("BRANCH:") &&
                           a.getAuthority().contains(String.valueOf(targetBranchId)));
    }

    private boolean withinAmountLimit(UserDetails user, BigDecimal amount) {
        // 大额转账权限检查
        if (amount.compareTo(new BigDecimal("50000")) > 0) {
            return user.getAuthorities().stream()
                .anyMatch(a -> a.getAuthority().equals("PERM:TRANSFER:HIGH_VALUE"));
        }
        return true;
    }
}

2. ABAC:基于属性的动态授权

RBAC 适合粗粒度权限,但银行有些规则是动态的、基于上下文的,这时需要 ABAC:

@Component
public class AbacAuthorizationService {

    /**
     * ABAC 策略引擎
     * 权限 = f(主体属性, 资源属性, 环境属性)
     */
    public boolean evaluate(AuthorizationContext context) {
        Subject subject = context.getSubject();
        Resource resource = context.getResource();
        Environment env = context.getEnvironment();

        // 策略1:工作时间限制
        if (hasTimeRestriction(subject) && !isWithinWorkingHours(env)) {
            log.warn("用户 {} 在非工作时间访问 {}", subject.getUserId(), resource.getType());
            return false;
        }

        // 策略2:IP 白名单(银行特有:只能从内网访问核心系统)
        if (resource.isInternalOnly() && !isFromInternalNetwork(env)) {
            return false;
        }

        // 策略3:交易金额动态审批
        if (resource.getType().equals("TRANSFER")) {
            return evaluateTransferPolicy(context);
        }

        // 策略4:敏感数据访问需要 MFA
        if (resource.isSensitive() && !context.isMfaVerified()) {
            return false;
        }

        return true;
    }

    private boolean evaluateTransferPolicy(AuthorizationContext context) {
        TransferResource transfer = (TransferResource) context.getResource();
        BigDecimal amount = transfer.getAmount();
        Subject subject = context.getSubject();

        // 分级审批规则
        if (amount.compareTo(new BigDecimal("500000")) >= 0) {
            // 50万以上:需要分行主管 + 区域主管双人审批
            return hasDualApproval(context);
        } else if (amount.compareTo(new BigDecimal("50000")) >= 0) {
            // 5万-50万:需要本分行主管审批
            return hasSingleApproval(context, ApprovalLevel.BRANCH);
        } else {
            // 5万以下:柜员直接处理
            return subject.hasRole("TELLER");
        }
    }
}

public record AuthorizationContext(
    Subject subject,
    Resource resource,
    Environment environment,
    boolean isMfaVerified
) {}

3. 银行特有的权限问题

3.1 委托代理:柜员替客户操作

银行柜员需要以客户身份操作账户,而不是以自己身份:

@Service
public class DelegationService {

    private final SecurityContext securityContext;

    /**
     * 执行委托操作
     * 柜员张三(以客户李四身份)查询账户余额
     */
    public <T> T executeOnBehalfOf(String customerId,
                                     String tellerId,
                                     Supplier<T> action) {
        // 记录审计日志:谁(teller)以谁(customer)身份做了什么
        String auditTraceId = UUID.randomUUID().toString();

        try {
            // 1. 验证柜员的委托权限
            validateDelegationPermission(tellerId, customerId);

            // 2. 切换安全上下文(以客户身份执行)
            SecurityContext original = SecurityContextHolder.getContext();
            SecurityContext delegate = new SecurityContextHolder.createEmptyContext();
            delegate.setAuthentication(buildCustomerAuth(customerId, tellerId));
            SecurityContextHolder.setContext(delegate);

            try {
                T result = action.get();

                // 3. 记录委托操作审计日志
                auditLogService.log(new AuditEntry(
                    traceId = auditTraceId,
                    actor = tellerId,         // 实际执行者
                    principal = customerId,   // 被代理客户
                    action = "ACCOUNT_QUERY",
                    result = "SUCCESS"
                ));

                return result;
            } finally {
                SecurityContextHolder.setContext(original);
            }
        } catch (Exception e) {
            auditLogService.log(new AuditEntry(
                traceId = auditTraceId,
                actor = tellerId,
                principal = customerId,
                action = "ACCOUNT_QUERY",
                result = "FAILED: " + e.getMessage()
            ));
            throw e;
        }
    }
}

3.2 系统间认证:mTLS + JWT

银行内部服务间调用也需要认证,用 mTLS + JWT 双重保障:

@Configuration
public class ServiceMeshAuthConfig {

    @Bean
    public ClientHttpRequestInterceptor serviceAuthInterceptor() {
        return (request, body, execution) -> {
            // 1. mTLS(在网络层已处理,这里验证)

            // 2. 注入服务身份 JWT
            String serviceToken = generateServiceToken();
            request.getHeaders().add("X-Service-Token", serviceToken);

            // 3. 传递调用者身份(用于审计)
            String callerContext = extractCallerContext();
            request.getHeaders().add("X-Caller-Context", callerContext);

            return execution.execute(request, body);
        };
    }

    private String generateServiceToken() {
        return Jwts.builder()
            .subject("payment-service")
            .claim("service_name", "payment-service")
            .claim("namespace", "payment-prod")
            .claim("audience", "account-service")
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + 60000))
            .signWith(serviceKey)
            .compact();
    }
}

4. JWT 权限设计

4.1 JWT Claims 结构

{
  "sub": "user-12345",
  "name": "Zhang San",
  "roles": ["TELLER", "BRANCH_001"],
  "permissions": [
    "PAYMENT:TRANSFER:SELF",
    "PAYMENT:TRANSFER:LOW_VALUE",
    "ACCOUNT:VIEW:SELF"
  ],
  "branch_id": "BRANCH_001",
  "region": "APAC",
  "mfa_verified": true,
  "iat": 1672531200,
  "exp": 1672534800,
  "jti": "token-unique-id"
}

4.2 权限校验:网关层 vs 服务层

网关层(粗粒度)         服务层(细粒度)
────────────────        ────────────────
验证 Token 有效性       检查具体权限
验证 Token 未吊销       检查金额范围
检查基本角色             检查分行归属
提取 X-User-Id          检查委托权限
提取 X-User-Roles       ABAC 动态评估

网关层快速拦截无效请求,服务层做精确的银行级权限判断。不在网关做所有判断,也不在服务层重复做 Token 验证

5. 权限变更的审计与合规

银行监管要求所有权限变更必须可追溯:

@Service
@Slf4j
public class PrivilegeAuditService {

    @Transactional
    public void grantRole(Long userId, String roleCode, String grantedBy) {
        // 1. 检查授权人权限(只有 HR 或主管可以授权)
        validateGranterPermission(grantedBy, roleCode);

        // 2. 执行授权
        userRoleDao.insert(userId, roleCode);

        // 3. 强制审计日志(银行合规要求:不能失败就回滚,必须保证审计)
        try {
            auditLogService.logAccessChange(new PrivilegeChangeEvent(
                userId, roleCode, grantedBy,
                LocalDateTime.now(), "GRANT"
            ));
        } catch (Exception e) {
            // 审计失败不影响业务,但必须告警
            alertService.alert("PRIVILEGE_AUDIT_FAILED",
                Map.of("userId", userId, "role", roleCode));
        }
    }
}

6. 总结:银行权限体系设计原则

原则实现
最小权限RBAC 角色精确分配,定期清理孤儿账号
职责分离大额交易分级审批(单人→双人→三人)
委托受控柜员代理操作全程审计,可回溯
系统隔离服务间 mTLS+JWT 双认证,不可伪造
及时撤销Token 黑名单 + 权限变更实时生效
可审计所有权限变更和访问操作写审计日志

相关阅读:Spring Security 与 OAuth2 银行级安全实战 · Spring Cloud Gateway 银行网关实战 · HashiCorp Vault 银行密钥管理实战