SpringBoot + OAuth2 资源服务器 + Scope 控制:精细化 API 访问权限管理

引言

在微服务架构日益普及的今天,API权限管理成了每个开发者都必须面对的挑战。你有没有遇到过这种情况:不同的客户端应用需要访问同一个API,但权限要求却截然不同?或者用户在不同场景下应该看到不同的数据?这些问题的核心就是缺乏精细化的API访问控制机制。

今天就来聊聊如何用SpringBoot结合OAuth2实现基于Scope的精细化API权限管理,让你的系统能够灵活控制不同客户端和用户的访问权限,实现真正的"按需授权"。

为什么需要精细化API权限管理?

传统权限控制的局限

让我们先看看传统的权限控制方式存在什么问题:

权限粒度太粗

  • 简单的用户角色控制(管理员、普通用户)
  • 无法满足复杂的业务场景需求
  • 不同应用间权限难以区分

扩展性差

  • 新增权限需要修改代码
  • 权限逻辑与业务逻辑耦合
  • 难以支持第三方应用接入

安全性不足

  • 权限检查逻辑分散在各处
  • 缺乏统一的权限管理中心
  • 审计和监控困难

维护成本高

  • 权限配置复杂繁琐
  • 不同环境配置不一致
  • 权限变更影响范围难以控制

OAuth2 Scope控制的优势

精细化控制

  • 基于Scope的细粒度权限控制
  • 支持动态权限分配
  • 灵活适应不同业务场景

标准化协议

  • 遵循OAuth2行业标准
  • 支持第三方应用集成
  • 业界广泛认可和使用

安全性强

  • 集中化的权限管理
  • 标准化的Token验证流程
  • 完善的安全审计机制

易于维护

  • 配置化权限管理
  • 支持运行时权限调整
  • 清晰的权限边界划分

核心架构设计

我们的OAuth2资源服务器Scope控制架构:

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   客户端应用    │───▶│   授权服务器     │───▶│   资源服务器    │
│  (Client App)   │    │(Authorization)   │    │(Resource Server)│
└─────────────────┘    └──────────────────┘    └─────────────────┘
        │                        │                       │
        │ 请求授权               │                       │
        │───────────────────────▶│                       │
        │                        │ 用户认证              │
        │                        │──────────────────────▶│
        │                        │                       │
        │                        │ 返回包含Scope的Token  │
        │                        │──────────────────────▶│
        │                        │                       │
        │ 使用Token访问API       │                       │
        │────────────────────────────────────────────────▶│
        │                        │                       │
        │                        │ 验证Token和Scope      │
        │                        │──────────────────────▶│
        │                        │                       │
        │                        │ 检查API访问权限       │
        │                        │──────────────────────▶│
        │                        │                       │
        │ 返回数据或拒绝访问     │                       │
        │◀────────────────────────────────────────────────│
        │                        │                       │

核心设计要点

1. Scope权限模型设计

// Scope权限实体类
@Data
@TableName("oauth2_scope")
public class OAuth2Scope {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private String scopeCode;         // Scope编码
    private String scopeName;         // Scope名称
    private String description;       // 描述
    private String resourceType;      // 资源类型(API/数据/功能)
    private String resourcePath;      // 资源路径
    private String httpMethod;        // HTTP方法
    private Integer priority;         // 优先级
    private Integer status;           // 状态(0-禁用 1-启用)
    private LocalDateTime createTime; // 创建时间
    private LocalDateTime updateTime; // 更新时间
}

// 客户端Scope关联
@Data
@TableName("oauth2_client_scope")
public class OAuth2ClientScope {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private String clientId;          // 客户端ID
    private Long scopeId;             // Scope ID
    private LocalDateTime createTime; // 创建时间
}

// 用户Scope授权
@Data
@TableName("oauth2_user_scope")
public class OAuth2UserScope {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private Long userId;              // 用户ID
    private Long scopeId;             // Scope ID
    private LocalDateTime grantedTime; // 授权时间
    private LocalDateTime expireTime;  // 过期时间
    private String grantedBy;         // 授权人
}

2. OAuth2资源服务器配置

// OAuth2资源服务器配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class OAuth2ResourceServerConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                // 公开接口
                .requestMatchers("/public/**").permitAll()
                // 需要认证的接口
                .requestMatchers("/api/**").authenticated()
                // 管理接口需要特殊Scope
                .requestMatchers("/admin/**").hasAuthority("SCOPE_admin")
                // 其他请求需要认证
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            );
            
        return http.build();
    }

    /**
     * JWT认证转换器
     * 将JWT中的scope信息转换为Spring Security的权限
     */
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
        authoritiesConverter.setAuthorityPrefix("SCOPE_");
        authoritiesConverter.setAuthoritiesClaimName("scope");

        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
        return converter;
    }

    /**
     * JWT解码器配置
     */
    @Bean
    public JwtDecoder jwtDecoder() {
        // 这里应该配置正确的JWK Set URI
        // 实际项目中应该从授权服务器获取公钥
        return NimbusJwtDecoder.withJwkSetUri("http://auth-server/.well-known/jwks.json")
            .build();
    }
}

3. Scope权限服务

// Scope权限服务
@Service
@Transactional
@Slf4j
public class ScopePermissionService {
    
    @Autowired
    private OAuth2ScopeMapper scopeMapper;
    
    @Autowired
    private OAuth2ClientScopeMapper clientScopeMapper;
    
    @Autowired
    private OAuth2UserScopeMapper userScopeMapper;
    
    /**
     * 验证客户端是否有指定Scope权限
     */
    public boolean hasClientScope(String clientId, String scopeCode) {
        try {
            // 检查客户端是否被授权该Scope
            QueryWrapper<OAuth2ClientScope> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("client_id", clientId)
                       .eq("scope_id", getScopeIdByCode(scopeCode));
            
            return clientScopeMapper.selectCount(queryWrapper) > 0;
            
        } catch (Exception e) {
            log.error("检查客户端Scope权限失败: clientId={}, scope={}", clientId, scopeCode, e);
            return false;
        }
    }
    
    /**
     * 验证用户是否有指定Scope权限
     */
    public boolean hasUserScope(Long userId, String scopeCode) {
        try {
            // 检查用户是否被授权该Scope
            QueryWrapper<OAuth2UserScope> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("user_id", userId)
                       .eq("scope_id", getScopeIdByCode(scopeCode))
                       .ge("expire_time", LocalDateTime.now());
            
            return userScopeMapper.selectCount(queryWrapper) > 0;
            
        } catch (Exception e) {
            log.error("检查用户Scope权限失败: userId={}, scope={}", userId, scopeCode, e);
            return false;
        }
    }
    
    /**
     * 获取用户所有有效的Scope
     */
    public Set<String> getUserScopes(Long userId) {
        try {
            List<OAuth2UserScope> userScopes = userScopeMapper.selectByUserId(userId);
            return userScopes.stream()
                .filter(scope -> scope.getExpireTime().isAfter(LocalDateTime.now()))
                .map(this::getScopeCodeById)
                .collect(Collectors.toSet());
                
        } catch (Exception e) {
            log.error("获取用户Scope列表失败: userId={}", userId, e);
            return Collections.emptySet();
        }
    }
    
    /**
     * 获取客户端所有Scope
     */
    public Set<String> getClientScopes(String clientId) {
        try {
            List<OAuth2ClientScope> clientScopes = clientScopeMapper.selectByClientId(clientId);
            return clientScopes.stream()
                .map(this::getScopeCodeById)
                .collect(Collectors.toSet());
                
        } catch (Exception e) {
            log.error("获取客户端Scope列表失败: clientId={}", clientId, e);
            return Collections.emptySet();
        }
    }
    
    /**
     * 验证请求是否具有必要权限
     */
    public boolean validateRequestPermission(String clientId, Long userId, String resourcePath, String httpMethod) {
        try {
            // 根据资源路径和方法获取需要的Scope
            String requiredScope = getRequiredScope(resourcePath, httpMethod);
            if (StringUtils.isEmpty(requiredScope)) {
                return true; // 不需要特定权限
            }
            
            // 验证客户端和用户权限
            boolean clientHasPermission = hasClientScope(clientId, requiredScope);
            boolean userHasPermission = hasUserScope(userId, requiredScope);
            
            return clientHasPermission && userHasPermission;
            
        } catch (Exception e) {
            log.error("验证请求权限失败", e);
            return false;
        }
    }
    
    private Long getScopeIdByCode(String scopeCode) {
        OAuth2Scope scope = scopeMapper.selectByCode(scopeCode);
        return scope != null ? scope.getId() : null;
    }
    
    private String getScopeCodeById(Long scopeId) {
        OAuth2Scope scope = scopeMapper.selectById(scopeId);
        return scope != null ? scope.getScopeCode() : null;
    }
    
    private String getRequiredScope(String resourcePath, String httpMethod) {
        // 根据资源路径和HTTP方法确定需要的Scope
        // 这里可以实现复杂的权限映射逻辑
        if (resourcePath.startsWith("/admin")) {
            return "admin";
        } else if (resourcePath.startsWith("/api/users") && "GET".equals(httpMethod)) {
            return "user.read";
        } else if (resourcePath.startsWith("/api/users") && "POST".equals(httpMethod)) {
            return "user.write";
        }
        return null;
    }
}

4. 注解驱动权限控制

// Scope权限注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequireScope {
    
    /**
     * 需要的Scope列表
     * 支持AND和OR逻辑
     */
    String[] value() default {};
    
    /**
     * Scope逻辑关系
     * ALL: 需要所有Scope
     * ANY: 满足任一Scope即可
     */
    Logic logic() default Logic.ALL;
    
    /**
     * 是否允许匿名访问
     */
    boolean allowAnonymous() default false;
    
    enum Logic {
        ALL, ANY
    }
}

// Scope权限检查切面
@Aspect
@Component
@Slf4j
public class ScopePermissionAspect {
    
    @Autowired
    private ScopePermissionService scopePermissionService;
    
    @Around("@annotation(requireScope)")
    public Object checkScopePermission(ProceedingJoinPoint joinPoint, RequireScope requireScope) throws Throwable {
        try {
            // 获取当前认证信息
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication == null) {
                if (requireScope.allowAnonymous()) {
                    return joinPoint.proceed();
                }
                throw new AccessDeniedException("未认证的访问请求");
            }
            
            // 获取客户端ID和用户ID
            String clientId = getClientId(authentication);
            Long userId = getUserId(authentication);
            
            // 检查Scope权限
            boolean hasPermission = checkScopePermission(clientId, userId, requireScope);
            
            if (!hasPermission) {
                log.warn("Scope权限检查失败: clientId={}, userId={}, requiredScopes={}", 
                    clientId, userId, Arrays.toString(requireScope.value()));
                throw new AccessDeniedException("权限不足,无法访问该资源");
            }
            
            log.debug("Scope权限检查通过: clientId={}, userId={}", clientId, userId);
            return joinPoint.proceed();
            
        } catch (AccessDeniedException e) {
            throw e;
        } catch (Exception e) {
            log.error("Scope权限检查异常", e);
            throw new AccessDeniedException("权限检查失败");
        }
    }
    
    private boolean checkScopePermission(String clientId, Long userId, RequireScope requireScope) {
        String[] scopes = requireScope.value();
        RequireScope.Logic logic = requireScope.logic();
        
        if (scopes.length == 0) {
            return true;
        }
        
        if (logic == RequireScope.Logic.ALL) {
            // 需要所有Scope
            return Arrays.stream(scopes)
                .allMatch(scope -> scopePermissionService.hasClientScope(clientId, scope) 
                                && scopePermissionService.hasUserScope(userId, scope));
        } else {
            // 满足任一Scope即可
            return Arrays.stream(scopes)
                .anyMatch(scope -> scopePermissionService.hasClientScope(clientId, scope) 
                                && scopePermissionService.hasUserScope(userId, scope));
        }
    }
    
    private String getClientId(Authentication authentication) {
        // 从认证信息中提取客户端ID
        if (authentication instanceof JwtAuthenticationToken) {
            JwtAuthenticationToken jwtToken = (JwtAuthenticationToken) authentication;
            return jwtToken.getToken().getClaim("client_id");
        }
        return null;
    }
    
    private Long getUserId(Authentication authentication) {
        // 从认证信息中提取用户ID
        if (authentication instanceof JwtAuthenticationToken) {
            JwtAuthenticationToken jwtToken = (JwtAuthenticationToken) authentication;
            String userId = jwtToken.getToken().getClaim("user_id");
            return userId != null ? Long.valueOf(userId) : null;
        }
        return null;
    }
}

关键实现细节

1. 权限配置管理

# application.yml
oauth2:
  # 资源服务器配置
  resource-server:
    # JWT验证配置
    jwt:
      issuer-uri: http://localhost:8080/auth/realms/myapp
      jwk-set-uri: http://localhost:8080/auth/realms/myapp/protocol/openid-connect/certs
    
    # Scope配置
    scopes:
      # 用户管理相关Scope
      user:
        read: "user.read"
        write: "user.write"
        delete: "user.delete"
      
      # 订单管理相关Scope
      order:
        read: "order.read"
        write: "order.write"
        manage: "order.manage"
      
      # 系统管理相关Scope
      admin:
        full: "admin"
        config: "admin.config"
        monitor: "admin.monitor"
    
    # 资源路径与Scope映射
    resource-mappings:
      - path: "/api/users/**"
        method: "GET"
        required-scope: "user.read"
      - path: "/api/users/**"
        method: "POST"
        required-scope: "user.write"
      - path: "/api/orders/**"
        method: "GET"
        required-scope: "order.read"
      - path: "/api/orders/**"
        method: "POST"
        required-scope: "order.write"
      - path: "/admin/**"
        method: "*"
        required-scope: "admin"
// OAuth2配置属性类
@Configuration
@ConfigurationProperties(prefix = "oauth2.resource-server")
@Data
public class OAuth2ResourceProperties {
    
    private JwtConfig jwt = new JwtConfig();
    private ScopeConfig scopes = new ScopeConfig();
    private List<ResourceMapping> resourceMappings = new ArrayList<>();
    
    @Data
    public static class JwtConfig {
        private String issuerUri;
        private String jwkSetUri;
    }
    
    @Data
    public static class ScopeConfig {
        private Map<String, Map<String, String>> user = new HashMap<>();
        private Map<String, Map<String, String>> order = new HashMap<>();
        private Map<String, Map<String, String>> admin = new HashMap<>();
    }
    
    @Data
    public static class ResourceMapping {
        private String path;
        private String method;
        private String requiredScope;
    }
}

2. 权限检查拦截器

// OAuth2权限检查拦截器
@Component
@Order(100)
@Slf4j
public class OAuth2ScopeInterceptor implements HandlerInterceptor {
    
    @Autowired
    private ScopePermissionService scopePermissionService;
    
    @Autowired
    private OAuth2ResourceProperties properties;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 只处理Controller方法
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        String requestPath = request.getRequestURI();
        String httpMethod = request.getMethod();
        
        try {
            // 检查是否需要特殊权限
            String requiredScope = findRequiredScope(requestPath, httpMethod);
            if (StringUtils.isEmpty(requiredScope)) {
                return true; // 不需要特殊权限
            }
            
            // 获取认证信息
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication == null) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return false;
            }
            
            // 获取客户端和用户信息
            String clientId = extractClientId(authentication);
            Long userId = extractUserId(authentication);
            
            if (StringUtils.isEmpty(clientId) || userId == null) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return false;
            }
            
            // 检查权限
            boolean hasPermission = scopePermissionService.validateRequestPermission(
                clientId, userId, requestPath, httpMethod);
                
            if (!hasPermission) {
                log.warn("API访问权限不足: clientId={}, userId={}, path={}, method={}", 
                    clientId, userId, requestPath, httpMethod);
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                return false;
            }
            
            log.debug("API访问权限验证通过: clientId={}, userId={}, path={}", 
                clientId, userId, requestPath);
            return true;
            
        } catch (Exception e) {
            log.error("API权限检查异常: path={}, method={}", requestPath, httpMethod, e);
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            return false;
        }
    }
    
    private String findRequiredScope(String requestPath, String httpMethod) {
        return properties.getResourceMappings().stream()
            .filter(mapping -> pathMatches(mapping.getPath(), requestPath))
            .filter(mapping -> methodMatches(mapping.getMethod(), httpMethod))
            .map(OAuth2ResourceProperties.ResourceMapping::getRequiredScope)
            .findFirst()
            .orElse(null);
    }
    
    private boolean pathMatches(String pattern, String path) {
        AntPathMatcher matcher = new AntPathMatcher();
        return matcher.match(pattern, path);
    }
    
    private boolean methodMatches(String pattern, String method) {
        return "*".equals(pattern) || pattern.equalsIgnoreCase(method);
    }
    
    private String extractClientId(Authentication authentication) {
        if (authentication instanceof JwtAuthenticationToken) {
            JwtAuthenticationToken jwt = (JwtAuthenticationToken) authentication;
            return jwt.getToken().getClaim("client_id");
        }
        return null;
    }
    
    private Long extractUserId(Authentication authentication) {
        if (authentication instanceof JwtAuthenticationToken) {
            JwtAuthenticationToken jwt = (JwtAuthenticationToken) authentication;
            String userId = jwt.getToken().getClaim("user_id");
            return userId != null ? Long.valueOf(userId) : null;
        }
        return null;
    }
}

// 配置拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    private OAuth2ScopeInterceptor oauth2ScopeInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(oauth2ScopeInterceptor)
            .addPathPatterns("/api/**")
            .excludePathPatterns("/public/**");
    }
}

3. 监控和审计

// OAuth2权限审计服务
@Service
@Slf4j
public class OAuth2AuditService {
    
    @Autowired
    private MeterRegistry meterRegistry;
    
    private final Counter successfulAccessCounter;
    private final Counter deniedAccessCounter;
    private final Counter tokenInvalidCounter;
    
    public OAuth2AuditService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.successfulAccessCounter = Counter.builder("oauth2_access_requests")
            .description("OAuth2访问请求成功次数")
            .tag("result", "success")
            .register(meterRegistry);
        this.deniedAccessCounter = Counter.builder("oauth2_access_requests")
            .description("OAuth2访问请求拒绝次数")
            .tag("result", "denied")
            .register(meterRegistry);
        this.tokenInvalidCounter = Counter.builder("oauth2_access_requests")
            .description("OAuth2 Token无效次数")
            .tag("result", "invalid_token")
            .register(meterRegistry);
    }
    
    /**
     * 记录访问日志
     */
    public void recordAccess(String clientId, Long userId, String resourcePath, 
                           String httpMethod, boolean granted) {
        log.info("API访问记录: client={}, user={}, path={}, method={}, granted={}", 
            clientId, userId, resourcePath, httpMethod, granted);
            
        if (granted) {
            successfulAccessCounter.increment();
        } else {
            deniedAccessCounter.increment();
        }
    }
    
    /**
     * 记录Token验证失败
     */
    public void recordInvalidToken(String clientId, String resourcePath) {
        log.warn("Token验证失败: client={}, path={}", clientId, resourcePath);
        tokenInvalidCounter.increment();
    }
    
    /**
     * 获取权限统计信息
     */
    public OAuth2AccessStats getAccessStats() {
        return OAuth2AccessStats.builder()
            .totalSuccess((long) successfulAccessCounter.count())
            .totalDenied((long) deniedAccessCounter.count())
            .totalInvalid((long) tokenInvalidCounter.count())
            .accessRate(calculateAccessRate())
            .build();
    }
    
    private double calculateAccessRate() {
        double total = successfulAccessCounter.count() + deniedAccessCounter.count();
        return total > 0 ? successfulAccessCounter.count() / total * 100 : 0;
    }
}

// 访问统计信息
@Data
@Builder
public class OAuth2AccessStats {
    private Long totalSuccess;
    private Long totalDenied;
    private Long totalInvalid;
    private Double accessRate;
    
    // 可以添加更多统计信息
    private Map<String, Long> accessByClient;
    private Map<String, Long> accessByResource;
}

业务场景应用

1. 用户服务权限控制

@RestController
@RequestMapping("/api/users")
@Slf4j
public class UserController {
    
    @Autowired
    private UserService userService;
    
    /**
     * 查看用户列表 - 需要user.read权限
     */
    @GetMapping
    @RequireScope("user.read")
    public ResponseEntity<List<User>> getUsers() {
        List<User> users = userService.getAllUsers();
        return ResponseEntity.ok(users);
    }
    
    /**
     * 创建用户 - 需要user.write权限
     */
    @PostMapping
    @RequireScope("user.write")
    public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) {
        User user = userService.createUser(request);
        return ResponseEntity.ok(user);
    }
    
    /**
     * 删除用户 - 需要user.delete权限
     */
    @DeleteMapping("/{id}")
    @RequireScope("user.delete")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
    
    /**
     * 获取当前用户信息 - 允许匿名访问,但有权限时返回更多信息
     */
    @GetMapping("/me")
    @RequireScope(value = "user.profile", allowAnonymous = true)
    public ResponseEntity<UserProfile> getCurrentUser() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth == null || !auth.isAuthenticated()) {
            // 匿名用户,返回基本信息
            return ResponseEntity.ok(getAnonymousProfile());
        }
        
        // 认证用户,返回完整信息
        UserProfile profile = userService.getCurrentUserProfile();
        return ResponseEntity.ok(profile);
    }
}

2. 订单服务权限控制

@RestController
@RequestMapping("/api/orders")
@Slf4j
public class OrderController {
    
    @Autowired
    private OrderService orderService;
    
    /**
     * 查看订单列表 - 需要order.read权限
     * 管理员可以看到所有订单,普通用户只能看到自己的订单
     */
    @GetMapping
    @RequireScope("order.read")
    public ResponseEntity<List<Order>> getOrders(
            @RequestParam(required = false) Long userId,
            @RequestParam(required = false) String status) {
        
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        boolean isAdmin = hasAdminScope(auth);
        
        List<Order> orders;
        if (isAdmin && userId == null) {
            // 管理员查看所有订单
            orders = orderService.getAllOrders(status);
        } else {
            // 普通用户查看自己的订单
            Long currentUserId = extractUserId(auth);
            orders = orderService.getUserOrders(currentUserId, status);
        }
        
        return ResponseEntity.ok(orders);
    }
    
    /**
     * 创建订单 - 需要order.write权限
     */
    @PostMapping
    @RequireScope("order.write")
    public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        Long userId = extractUserId(auth);
        request.setUserId(userId);
        
        Order order = orderService.createOrder(request);
        return ResponseEntity.ok(order);
    }
    
    /**
     * 订单管理 - 需要order.manage权限
     */
    @PutMapping("/{id}/status")
    @RequireScope("order.manage")
    public ResponseEntity<Order> updateOrderStatus(
            @PathVariable Long id, 
            @RequestBody UpdateOrderStatusRequest request) {
        Order order = orderService.updateOrderStatus(id, request.getStatus());
        return ResponseEntity.ok(order);
    }
    
    private boolean hasAdminScope(Authentication auth) {
        if (auth instanceof JwtAuthenticationToken) {
            Collection<GrantedAuthority> authorities = auth.getAuthorities();
            return authorities.stream()
                .anyMatch(auth -> "SCOPE_admin".equals(auth.getAuthority()));
        }
        return false;
    }
    
    private Long extractUserId(Authentication auth) {
        if (auth instanceof JwtAuthenticationToken) {
            JwtAuthenticationToken jwt = (JwtAuthenticationToken) auth;
            String userId = jwt.getToken().getClaim("user_id");
            return userId != null ? Long.valueOf(userId) : null;
        }
        return null;
    }
}

3. 管理服务权限控制

@RestController
@RequestMapping("/admin")
@Slf4j
public class AdminController {
    
    @Autowired
    private AdminService adminService;
    
    /**
     * 系统配置管理 - 需要admin.config权限
     */
    @GetMapping("/config")
    @RequireScope("admin.config")
    public ResponseEntity<SystemConfig> getSystemConfig() {
        SystemConfig config = adminService.getSystemConfig();
        return ResponseEntity.ok(config);
    }
    
    @PutMapping("/config")
    @RequireScope("admin.config")
    public ResponseEntity<SystemConfig> updateSystemConfig(@RequestBody SystemConfig config) {
        SystemConfig updatedConfig = adminService.updateSystemConfig(config);
        return ResponseEntity.ok(updatedConfig);
    }
    
    /**
     * 系统监控 - 需要admin.monitor权限
     */
    @GetMapping("/monitor/metrics")
    @RequireScope("admin.monitor")
    public ResponseEntity<SystemMetrics> getSystemMetrics() {
        SystemMetrics metrics = adminService.getSystemMetrics();
        return ResponseEntity.ok(metrics);
    }
    
    @GetMapping("/monitor/health")
    @RequireScope("admin.monitor")
    public ResponseEntity<HealthStatus> getHealthStatus() {
        HealthStatus health = adminService.getHealthStatus();
        return ResponseEntity.ok(health);
    }
    
    /**
     * 用户管理 - 需要admin权限
     */
    @GetMapping("/users")
    @RequireScope("admin")
    public ResponseEntity<List<User>> getAllUsers() {
        List<User> users = adminService.getAllUsers();
        return ResponseEntity.ok(users);
    }
    
    @DeleteMapping("/users/{id}")
    @RequireScope("admin")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        adminService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
}

最佳实践建议

1. Scope设计规范

// Scope命名规范工具类
public class ScopeNamingUtils {
    
    /**
     * 生成标准Scope名称
     * 格式: {resource}.{action}
     */
    public static String generateScope(String resource, String action) {
        return resource.toLowerCase() + "." + action.toLowerCase();
    }
    
    /**
     * 生成管理类Scope
     */
    public static String generateAdminScope(String module) {
        return "admin." + module.toLowerCase();
    }
    
    /**
     * 验证Scope格式是否正确
     */
    public static boolean isValidScope(String scope) {
        if (StringUtils.isEmpty(scope)) {
            return false;
        }
        // Scope应该包含点号,且不以点号开头或结尾
        return scope.contains(".") && 
               !scope.startsWith(".") && 
               !scope.endsWith(".");
    }
    
    /**
     * Scope层级结构定义
     */
    public static class Scopes {
        // 用户相关
        public static final String USER_READ = "user.read";
        public static final String USER_WRITE = "user.write";
        public static final String USER_DELETE = "user.delete";
        public static final String USER_PROFILE = "user.profile";
        
        // 订单相关
        public static final String ORDER_READ = "order.read";
        public static final String ORDER_WRITE = "order.write";
        public static final String ORDER_MANAGE = "order.manage";
        
        // 系统管理
        public static final String ADMIN = "admin";
        public static final String ADMIN_CONFIG = "admin.config";
        public static final String ADMIN_MONITOR = "admin.monitor";
        
        // 公共权限
        public static final String PUBLIC_READ = "public.read";
    }
}

2. 异常处理

// OAuth2权限异常处理器
@RestControllerAdvice
@Slf4j
public class OAuth2ExceptionHandler {
    
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException ex) {
        log.warn("访问被拒绝: {}", ex.getMessage());
        
        ErrorResponse error = ErrorResponse.builder()
            .code("ACCESS_DENIED")
            .message("权限不足,无法访问该资源")
            .timestamp(System.currentTimeMillis())
            .build();
            
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
    }
    
    @ExceptionHandler(InvalidBearerTokenException.class)
    public ResponseEntity<ErrorResponse> handleInvalidToken(InvalidBearerTokenException ex) {
        log.warn("无效的访问令牌: {}", ex.getMessage());
        
        ErrorResponse error = ErrorResponse.builder()
            .code("INVALID_TOKEN")
            .message("访问令牌无效或已过期")
            .timestamp(System.currentTimeMillis())
            .build();
            
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
    }
    
    @ExceptionHandler(OAuth2AuthenticationException.class)
    public ResponseEntity<ErrorResponse> handleOAuth2Auth(OAuth2AuthenticationException ex) {
        log.warn("OAuth2认证失败: {}", ex.getMessage());
        
        ErrorResponse error = ErrorResponse.builder()
            .code("AUTHENTICATION_FAILED")
            .message("认证失败,请重新登录")
            .timestamp(System.currentTimeMillis())
            .build();
            
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
        log.error("系统异常", ex);
        
        ErrorResponse error = ErrorResponse.builder()
            .code("INTERNAL_ERROR")
            .message("系统内部错误")
            .timestamp(System.currentTimeMillis())
            .build();
            
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

3. 测试支持

// OAuth2权限测试工具
@Component
public class OAuth2TestHelper {
    
    /**
     * 创建测试用的JWT Token
     */
    public String createTestToken(String clientId, Long userId, String... scopes) {
        // 创建JWT Claims
        Map<String, Object> claims = new HashMap<>();
        claims.put("client_id", clientId);
        claims.put("user_id", userId != null ? userId.toString() : null);
        claims.put("scope", String.join(" ", scopes));
        claims.put("exp", System.currentTimeMillis() + 3600000); // 1小时后过期
        
        // 这里应该使用实际的JWT签名逻辑
        // 简化处理,实际项目中应该使用JWT库生成
        return "test-jwt-token";
    }
    
    /**
     * 模拟认证上下文
     */
    public void mockAuthentication(String clientId, Long userId, String... scopes) {
        Collection<GrantedAuthority> authorities = Arrays.stream(scopes)
            .map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope))
            .collect(Collectors.toList());
            
        Authentication auth = new UsernamePasswordAuthenticationToken(
            userId != null ? userId.toString() : "anonymous",
            "N/A",
            authorities
        );
        
        SecurityContextHolder.getContext().setAuthentication(auth);
    }
    
    /**
     * 清理测试上下文
     */
    public void clearContext() {
        SecurityContextHolder.clearContext();
    }
    
    /**
     * 验证Scope权限
     */
    public boolean verifyScopePermission(String token, String requiredScope) {
        // 验证Token中的Scope是否包含所需权限
        // 这里简化处理,实际应该解析JWT并验证
        return token.contains(requiredScope);
    }
}

预期效果

通过这套OAuth2资源服务器Scope控制方案,我们可以实现:

  • 精细化控制:基于Scope的细粒度权限管理
  • 标准化集成:遵循OAuth2行业标准,便于第三方集成
  • 安全可靠:集中化的权限管理和验证机制
  • 易于维护:配置化的权限管理,支持动态调整
  • 监控完善:全面的权限访问监控和审计

这套方案让API权限管理从"简单粗暴"变成了"精细可控",大大提升了系统的安全性和灵活性。


欢迎关注公众号"服务端技术精选",获取更多技术干货!
欢迎加群交流


标题:SpringBoot + OAuth2 资源服务器 + Scope 控制:精细化 API 访问权限管理
作者:jiangyi
地址:http://jiangyi.space/articles/2026/02/15/1770964570467.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消