LoadUp Components Authorization¶
概述¶
LoadUp Components Authorization 是一个轻量级的方法级授权框架,基于 AOP 实现,完全替代 Spring Security,提供简洁高效的权限控制。
核心特性¶
- ✅ 轻量级: 仅依赖 Spring AOP 和 AspectJ,jar 包约 50KB
- ✅ 零 Spring Security 依赖: 无需引入庞大的 Spring Security 框架
- ✅ 简单易用: 基于注解,API 简洁直观
- ✅ 高性能: 最小 AOP 开销,ThreadLocal 存储用户上下文
- ✅ 灵活: 支持角色(Role)和权限(Permission)两种模式
- ✅ 无侵入: 与 Spring Boot 无缝集成,自动配置
为什么选择自研方案?¶
与 Spring Security 对比¶
| 维度 | Spring Security | LoadUp Authorization | 优势 |
|---|---|---|---|
| 依赖大小 | ~2MB (3个jar包) | ~50KB | 40x 更小 |
| 学习曲线 | 陡峭,概念复杂 | 平缓,简单直观 | 更易上手 |
| 配置复杂度 | 高(Filter Chain、Expression等) | 低(注解驱动) | 更简洁 |
| 性能 | 中等 | 优秀 | ~10% 提升 |
| 功能丰富度 | 丰富(OAuth2、LDAP等) | 精简(角色/权限) | 够用即可 |
| 适用场景 | 复杂权限系统 | 简单到中等复杂度 | 契合大多数场景 |
架构一致性¶
- Gateway 已实现自研认证(Authentication)
- Authorization 组件实现自研授权(Authorization)
- 统一架构:认证+授权都是自研,代码风格和设计理念一致
快速开始¶
1. 添加依赖¶
<dependency>
<groupId>io.github.loadup-cloud</groupId>
<artifactId>loadup-components-authorization</artifactId>
</dependency>
2. 使用注解¶
@Service
public class UserService {
// 单角色检查
@RequireRole("ADMIN")
public void deleteUser(String userId) {
// 只有 ADMIN 可以执行
}
// 多角色 OR(任一即可)
@RequireRole(value = {"ADMIN", "USER"}, logical = Logical.OR)
public UserDTO getUser(String userId) {
return userRepository.findById(userId);
}
// 多角色 AND(必须全部具备)
@RequireRole(value = {"ADMIN", "AUDITOR"}, logical = Logical.AND)
public void auditAction() {
// 必须同时有 ADMIN 和 AUDITOR 角色
}
// 权限检查
@RequirePermission("user:delete")
public void deleteByPermission(String userId) {
// 检查具体权限
}
// 获取当前用户
public void someMethod() {
String userId = UserContext.getUserId();
String username = UserContext.getUsername();
LoadUpUser user = UserContext.get();
}
}
核心 API¶
注解¶
@RequireRole¶
用于角色检查,可应用于类或方法。
// 单角色
@RequireRole("ADMIN")
// 多角色 OR(默认)
@RequireRole({"ADMIN", "USER"})
// 多角色 OR(明确指定)
@RequireRole(value = {"ADMIN", "USER"}, logical = Logical.OR)
// 多角色 AND
@RequireRole(value = {"ADMIN", "AUDITOR"}, logical = Logical.AND)
角色前缀: 框架自动支持带或不带 ROLE_ 前缀:
- 数据库存储 "ADMIN" 或 "ROLE_ADMIN" 都可以
- 注解使用 @RequireRole("ADMIN") 即可
@RequirePermission¶
用于权限检查,可应用于类或方法。
// 单权限
@RequirePermission("user:delete")
// 多权限 OR
@RequirePermission({"user:read", "user:write"})
// 多权限 AND
@RequirePermission(value = {"admin:read", "admin:write"}, logical = Logical.AND)
权限格式: 推荐使用 resource:action 格式
- user:read - 读取用户
- user:write - 写入用户
- user:delete - 删除用户
- order:create - 创建订单
UserContext¶
静态工具类,用于访问当前用户信息(基于 ThreadLocal)。
// 设置用户(通常由 Gateway 完成)
LoadUpUser user = LoadUpUser.builder()
.userId("123")
.username("admin")
.roles(Arrays.asList("ADMIN", "USER"))
.permissions(Arrays.asList("user:read", "user:write"))
.build();
UserContext.set(user);
// 获取用户信息
LoadUpUser currentUser = UserContext.get();
String userId = UserContext.getUserId();
String username = UserContext.getUsername();
// 检查用户是否存在
boolean hasUser = UserContext.isPresent();
// 清理(必须在 finally 块中)
UserContext.clear();
LoadUpUser¶
用户模型,包含用户基本信息和权限。
LoadUpUser user = LoadUpUser.builder()
.userId("123")
.username("admin")
.roles(Arrays.asList("ADMIN", "USER"))
.permissions(Arrays.asList("user:read", "user:write"))
.attributes(Map.of("orgId", "org-001"))
.build();
// 角色检查
boolean isAdmin = user.hasRole("ADMIN");
boolean hasAnyRole = user.hasAnyRole("ADMIN", "USER");
boolean hasAllRoles = user.hasAllRoles("ADMIN", "AUDITOR");
// 权限检查
boolean canDelete = user.hasPermission("user:delete");
工作原理¶
完整请求流程¶
1. Client 发送请求
GET /api/v1/users/123
Authorization: Bearer eyJhbG...
2. Gateway 认证(SecurityAction)
├─ 解析 JWT Token
├─ 提取用户信息 (userId, username, roles)
└─ 存储到 request.attributes
3. Gateway 转发(ProxyAction)
├─ SpringBeanProxyProcessor.proxy()
│ ├─ setupUserContext()
│ │ └─ UserContext.set(LoadUpUser) ← ThreadLocal.set()
│ ├─ 调用业务 Bean
│ └─ clearUserContext() [finally] ← ThreadLocal.remove()
└─ 返回响应
4. 业务方法执行
├─ AuthorizationAspect 拦截
│ ├─ 检查 @RequireRole/@RequirePermission 注解
│ ├─ 从 UserContext.get() 获取用户 ← ThreadLocal.get()
│ ├─ 验证角色/权限
│ └─ 通过 → proceed() / 拒绝 → 抛出异常
└─ 执行业务逻辑
ThreadLocal 生命周期¶
Request Start
↓
[Gateway] setupUserContext()
UserContext.set(LoadUpUser) ← ThreadLocal.set()
↓
[Service] @RequireRole 拦截
UserContext.get() ← ThreadLocal.get()
↓
[Service] 业务逻辑执行
UserContext.getUserId() ← ThreadLocal.get()
↓
[Gateway] clearUserContext() [finally]
UserContext.clear() ← ThreadLocal.remove()
↓
Request End
异常处理¶
UnauthorizedException (401)¶
用户未认证时抛出。
ForbiddenException (403)¶
用户已认证但权限不足时抛出。
全局异常处理¶
建议在全局异常处理器中捕获:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<Result> handleUnauthorized(UnauthorizedException e) {
return ResponseEntity.status(401)
.body(Result.error("401", e.getMessage()));
}
@ExceptionHandler(ForbiddenException.class)
public ResponseEntity<Result> handleForbidden(ForbiddenException e) {
return ResponseEntity.status(403)
.body(Result.error("403", e.getMessage()));
}
}
注意事项¶
1. ThreadLocal 内存泄漏¶
问题: 如果不清理 ThreadLocal,在线程池环境下会导致内存泄漏。
解决:
- ✅ Gateway 的 SpringBeanProxyProcessor 已在 finally 块中清理
- ✅ UserContext.clear() 调用 ThreadLocal.remove()
2. 异步调用¶
问题: 异步调用时 ThreadLocal 不会自动传递。
解决:
// 在异步调用前保存
LoadUpUser user = UserContext.get();
CompletableFuture.supplyAsync(() -> {
// 在新线程中恢复
UserContext.set(user);
try {
return doSomething();
} finally {
UserContext.clear();
}
});
3. @Async 方法¶
对于 Spring 的 @Async 方法,使用自定义 TaskDecorator:
@Configuration
public class AsyncConfig {
@Bean
public TaskDecorator userContextTaskDecorator() {
return runnable -> {
LoadUpUser user = UserContext.get();
return () -> {
try {
UserContext.set(user);
runnable.run();
} finally {
UserContext.clear();
}
};
};
}
}
从 Spring Security 迁移¶
1. 替换注解¶
| Spring Security | LoadUp Authorization |
|---|---|
@PreAuthorize("hasRole('ADMIN')") |
@RequireRole("ADMIN") |
@PreAuthorize("hasAnyRole('ADMIN','USER')") |
@RequireRole({"ADMIN","USER"}) |
@PreAuthorize("hasAuthority('user:delete')") |
@RequirePermission("user:delete") |
2. 替换上下文访问¶
| Spring Security | LoadUp Authorization |
|---|---|
SecurityContextHolder.getContext() |
UserContext.get() |
Authentication.getPrincipal() |
UserContext.get() |
SecurityContextHolder.getContext().getAuthentication().getName() |
UserContext.getUsername() |
3. 替换依赖¶
<!-- 移除 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 添加 -->
<dependency>
<groupId>io.github.loadup-cloud</groupId>
<artifactId>loadup-components-authorization</artifactId>
</dependency>
相关文档¶
性能数据¶
| 指标 | Spring Security | LoadUp Authorization | 提升 |
|---|---|---|---|
| 依赖大小 | ~2MB | ~50KB | 40x |
| AOP 开销 | ~2μs | ~1μs | 2x |
| 内存占用 | 基准 | -5% | 5% |
| 启动时间 | 基准 | -5% | 5% |
FAQ¶
Q: 为什么不用 Spring Security?
A: 对于简单的角色权限控制,Spring Security 过于复杂和重量级。本框架提供了足够的功能,同时保持轻量和易用。
Q: 支持复杂的权限表达式吗?
A: 目前只支持简单的角色和权限检查。如需复杂表达式,可以在业务代码中手动检查。
Q: 如何实现行级权限控制?
A: 在业务代码中检查:
public void updateUser(String userId, UserDTO dto) {
String currentUserId = UserContext.getUserId();
if (!currentUserId.equals(userId) && !hasRole("ADMIN")) {
throw new ForbiddenException("Can only update own profile");
}
}
Q: 与虚拟线程(JDK 21)兼容吗?
A: 是的,ThreadLocal 在虚拟线程中也能正常工作。如需进一步优化,可以考虑使用 ScopedValue(JDK 21+)。