“权限设计错了,比没有权限更危险——它给了错误的信任。“
前言
鉴权(Authentication)和授权(Authorization)是两个概念:
- 鉴权:验证”你是谁”(Who are you?)→ JWT、Session、OAuth2 Token
- 授权:判断”你能做什么”(What can you do?)→ RBAC、ABAC、行级权限
银行系统的权限设计有三个硬约束:
- 最小权限原则:每个用户/系统只能访问必要的资源
- 职责分离:敏感操作需要多人授权(如大额转账需要主管审批)
- 可审计:所有权限变更和访问行为必须记录
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 银行密钥管理实战