跳转至

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)

用户未认证时抛出。

throw new UnauthorizedException("User not authenticated");

ForbiddenException (403)

用户已认证但权限不足时抛出。

throw new ForbiddenException("Insufficient permissions");

全局异常处理

建议在全局异常处理器中捕获:

@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+)。