markdown复制## 1. 项目背景与问题定位
去年在一次常规安全审计中,我们的电商后台系统被发现存在严重的数据越权漏洞。攻击者只需修改请求参数中的公司ID,就能非法获取其他商户的销售数据。这种水平权限漏洞(Horizontal Privilege Escalation)在Web应用中极为常见,但危害性极高。
问题的本质在于:系统在Controller层只做了基础的登录认证,却没有对数据访问权限进行细粒度控制。随着业务发展,系统已有超过300个接口,手动为每个接口添加权限校验既不现实也难以维护。
## 2. 解决方案设计思路
### 2.1 核心需求拆解
理想的解决方案需要满足:
- **无侵入性**:不改动现有业务逻辑代码
- **声明式配置**:通过注解即可完成权限控制
- **灵活适配**:支持各种参数传递方式:
- 基础类型参数:`/api?companyId=123`
- 对象属性:`request.getCompanyId()`
- 集合元素:`List<User>.getCompanyId()`
- 嵌套属性:`request.getCompany().getId()`
### 2.2 技术选型对比
| 方案 | 优点 | 缺点 |
|---------------------|-----------------------|--------------------------|
| Filter拦截器 | 执行时机早 | 难以获取方法参数信息 |
| Interceptor拦截器 | 可获取HandlerMethod | 无法直接修改参数值 |
| AOP切面 | 完整方法上下文 | 需处理代理问题 |
| 自定义参数解析器 | 精准控制参数 | 只能处理特定参数类型 |
最终选择AOP方案,因其能:
1. 获取完整的方法签名和参数值
2. 支持灵活的条件判断
3. 与Spring生态无缝集成
## 3. 核心实现细节
### 3.1 注解定义与解析
定义`@DataPermission`注解包含以下关键属性:
```java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataPermission {
/**
* 权限资源类型
*/
ResourceType resource() default ResourceType.COMPANY;
/**
* 参数提取策略
*/
ParamStrategy strategy() default ParamStrategy.DIRECT;
/**
* 目标参数名/路径
* 如"companyId"或"order.company.id"
*/
String target();
/**
* 参数索引位置
*/
int position() default 0;
}
通过AnnotationUtils工具类实现注解继承逻辑:
java复制// 获取方法上的注解(包括父类/接口上的注解)
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
DataPermission ann = AnnotationUtils.findAnnotation(method, DataPermission.class);
// 支持类级别默认配置
if (ann == null) {
ann = AnnotationUtils.findAnnotation(
pjp.getTarget().getClass(),
DataPermission.class
);
}
3.2 参数提取引擎
设计参数提取器工厂支持多种场景:
java复制public interface ParamExtractor {
Object extract(ProceedingJoinPoint pjp, DataPermission ann);
}
// 注册各场景处理器
private static final Map<ParamStrategy, ParamExtractor> EXTRACTORS =
ImmutableMap.<ParamStrategy, ParamExtractor>builder()
.put(DIRECT, new DirectParamExtractor())
.put(OBJECT_FIELD, new FieldAccessExtractor())
.put(COLLECTION_ELEMENT, new CollectionElementExtractor())
.build();
以嵌套属性提取为例:
java复制public class NestedFieldExtractor implements ParamExtractor {
@Override
public Object extract(ProceedingJoinPoint pjp, DataPermission ann) {
Object root = pjp.getArgs()[ann.position()];
String[] paths = ann.target().split("\\.");
Object current = root;
for (String field : paths) {
current = BeanUtils.getProperty(current, field);
if (current == null) break;
}
return current;
}
}
3.3 权限校验服务
采用策略模式对接不同权限系统:
java复制public interface PermissionService {
boolean checkPermission(String userId, String resourceType, Object resourceId);
}
// 默认实现(可替换为RBAC/ABAC等)
@Service
@RequiredArgsConstructor
public class DefaultPermissionService implements PermissionService {
private final UserCompanyMapper userCompanyMapper;
@Override
public boolean checkPermission(String userId, String resourceType, Object resourceId) {
if (ResourceType.COMPANY.equals(resourceType)) {
Long companyId = convertToLong(resourceId);
return userCompanyMapper.existsByUserAndCompany(userId, companyId);
}
// 其他资源类型校验...
}
}
4. 完整工作流程
- 请求拦截:AOP切面捕获Controller方法调用
- 注解检测:检查方法/类上的
@DataPermission注解 - 参数提取:根据策略从请求参数中提取目标资源ID
- 权限校验:调用权限服务验证当前用户与资源的关系
- 异常处理:无权限时抛出
AccessDeniedException
mermaid复制graph TD
A[HTTP请求] --> B{AOP拦截}
B -->|有注解| C[提取目标资源ID]
B -->|无注解| D[放行]
C --> E[权限校验]
E -->|通过| F[执行业务方法]
E -->|拒绝| G[抛出403异常]
5. 高级应用场景
5.1 批量操作校验
处理集合参数时自动遍历校验:
java复制@PostMapping("/batch/delete")
@DataPermission(
resource = ResourceType.COMPANY,
strategy = ParamStrategy.COLLECTION_ELEMENT,
target = "companyId"
)
public Result<Void> batchDelete(@RequestBody List<OrderDTO> orders) {
// 自动校验每个order.companyId的权限
orderService.batchDelete(orders);
return Result.success();
}
5.2 多级嵌套属性
支持无限级属性路径解析:
java复制@Data
public class ComplexDTO {
private OrderWrapper wrapper;
@Data
public static class OrderWrapper {
private Order order;
}
@Data
public static class Order {
private Company company;
}
}
@PostMapping("/complex")
@DataPermission(
strategy = ParamStrategy.NESTED_FIELD,
target = "wrapper.order.company.id"
)
public Result<?> complexOp(@RequestBody ComplexDTO dto) {
// 自动解析dto.wrapper.order.company.id
}
6. 性能优化实践
6.1 权限缓存设计
java复制@Cacheable(cacheNames = "user_permission",
key = "#userId + ':' + #resourceType + ':' + #resourceId")
public boolean checkPermissionWithCache(String userId, String resourceType, Object resourceId) {
return permissionService.checkPermission(userId, resourceType, resourceId);
}
6.2 批量查询优化
java复制@Cacheable(cacheNames = "user_companies", key = "#userId")
public Set<Long> getUserCompanyIds(String userId) {
return userCompanyMapper.findCompanyIdsByUser(userId);
}
// 校验时转为本地判断
Set<Long> userCompanies = getUserCompanyIds(userId);
boolean hasPermission = userCompanies.containsAll(requestCompanyIds);
6.3 反射性能提升
使用CGLIB生成快速访问类:
java复制private static final Map<String, FastClass> FAST_CLASS_CACHE = new ConcurrentHashMap<>();
public Object fastGetField(Object obj, String fieldName) {
FastClass fc = FAST_CLASS_CACHE.computeIfAbsent(
obj.getClass().getName(),
k -> FastClass.create(obj.getClass())
);
return fc.invoke("get" + StringUtils.capitalize(fieldName),
new Class[0], obj, new Object[0]);
}
7. 生产环境踩坑记录
7.1 事务失效问题
现象:权限校验抛出异常时,已执行的数据库操作未回滚
原因:AOP切面顺序导致事务切面在外层
解决:调整切面顺序
java复制@Aspect
@Order(Ordered.LOWEST_PRECEDENCE - 100) // 确保在事务切面前执行
public class DataPermissionAspect {
// ...
}
7.2 循环依赖陷阱
现象:启动时报Bean创建循环依赖错误
解决方案:
java复制// 方案1:使用setter注入
private PermissionService permissionService;
@Autowired
public void setPermissionService(PermissionService permissionService) {
this.permissionService = permissionService;
}
// 方案2:使用@Lazy
@Lazy
@Autowired
private PermissionService permissionService;
7.3 参数名丢失问题
现象:编译后参数名变为arg0、arg1
解决方案:
- 编译参数添加
-parameters - 使用Spring的
DefaultParameterNameDiscoverer作为后备方案
java复制ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
String[] paramNames = nameDiscoverer.getParameterNames(method);
8. 扩展设计思路
8.1 多维度权限控制
扩展注解支持复合条件:
java复制@DataPermissions({
@DataPermission(resource=ResourceType.COMPANY, target="companyId"),
@DataPermission(resource=ResourceType.DEPARTMENT, target="deptId")
})
public Result<?> multiCheck() {
// 需同时满足公司和部门权限
}
8.2 动态权限注入
结合SpEL实现动态规则:
java复制@DataPermission(
resource = "#resourceType",
target = "#root.args[0].targetId"
)
public Result<?> dynamicCheck(RequestDTO dto, String resourceType) {
// 运行时确定校验规则
}
8.3 前端权限联动
自动生成权限元数据:
java复制@GetMapping("/permission-metadata")
public Map<String, Set<String>> getPermissionMetadata() {
return permissionService.getAllUserPermissions(SecurityUtils.getUserId());
}
9. 实施效果评估
| 指标 | 改进前 | 改进后 |
|---|---|---|
| 漏洞修复周期 | 2-3天/个 | 即时生效 |
| 代码侵入度 | 高 | 零侵入 |
| 权限变更成本 | 需要发版 | 热配置 |
| 平均校验耗时 | 15-20ms | <5ms(缓存后) |
在日均千万级调用的生产环境中:
- 权限校验成功率:99.99%
- 99线耗时:8ms
- CPU负载增加:<2%
10. 最佳实践建议
-
渐进式实施:
- 先从读接口开始实施
- 核心写接口添加操作日志
- 逐步覆盖全量接口
-
监控设计:
java复制@Around("dataPermissionPointcut()") public Object monitorAndCheck(ProceedingJoinPoint pjp) { long start = System.currentTimeMillis(); try { return pjp.proceed(); } finally { Metrics.timer("permission.check") .record(System.currentTimeMillis() - start, MILLISECONDS); } } -
测试策略:
- 单元测试:验证各ParamExtractor实现
- 集成测试:模拟不同用户权限场景
- 压力测试:验证缓存效果
这套方案已在多个业务线落地,累计拦截非法请求超过12万次。实施过程中最重要的经验是:权限系统要做到对业务透明,开发者只需要关注"做什么",而不需要操心"能不能做"的问题。
code复制