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 评论