mcpField
O
oauth2-resource-server

oauth2-resource-server

github.com/rrezartprebreza安全32897 次安装6 天前更新
代码仓库

关于 oauth2-resource-server

面向 Spring 开发者的 OAuth2 资源服务器配置指南,涵盖 JWT 验证以及基于作用域或角色的授权,并指出常见身份提供商的声明差异和配置问题。

主要功能

  1. 01Spring Boot 资源服务器配置
  2. 02提取 JWT 声明
  3. 03基于作用域和角色授权
  4. 04身份提供商特定的 JWT 指引
  5. 05常见安全配置问题

适用场景

  1. 01使用 JWT 保护 Spring API 端点
  2. 02应用基于作用域的方法级安全
  3. 03处理 Auth0、Okta 或 Keycloak 声明

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rrezartprebreza/spring-boot-skills --skill oauth2-resource-server

安装后在 Claude Code 中输入/oauth2-resource-server确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
openai.yaml
examples/
bad-resource-server-config.java
good-resource-server-config.java
templates/
JwtClaimExtractor.java
ResourceServerConfig.java

详细介绍

OAuth2 资源服务

依赖项

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

安全配置

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers("/api/v1/admin/**").hasAuthority("SCOPE_admin")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter()))
            )
            .build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthConverter() {
        // Keep the defaults: the "scope" OR "scp" claim (Okta/Entra use scp) -> SCOPE_*.
        // Calling setAuthoritiesClaimName(...) replaces them — then hasAuthority("SCOPE_admin")
        // can never pass. Extend the defaults with Keycloak realm roles instead.
        var scopes = new JwtGrantedAuthoritiesConverter();
        var converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(jwt -> {
            Collection<GrantedAuthority> authorities = new ArrayList<>(scopes.convert(jwt));
            // Keycloak nests realm roles under realm_access.roles -> ROLE_* (for hasRole)
            Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
            if (realmAccess != null && realmAccess.get("roles") instanceof Collection<?> roles) {
                roles.forEach(role -> authorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
            }
            return authorities;
        });
        return converter;
    }
}

application.yml — 常见提供方

# Keycloak
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://keycloak.example.com/realms/my-realm
          jwk-set-uri: https://keycloak.example.com/realms/my-realm/protocol/openid-connect/certs
          audiences: my-api   # without it, tokens minted for ANY client in the realm are accepted

# Auth0
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://your-domain.auth0.com/
          audiences: https://your-api.example.com  # custom claim validation

自定义声明提取

@Component
public class JwtClaimExtractor {

    // Provider subjects are opaque strings ("auth0|64f1c2", Okta "00u1abcd..."), not UUIDs.
    public String getSubject(JwtAuthenticationToken token) {
        return token.getToken().getSubject();
    }

    public String getEmail(JwtAuthenticationToken token) {
        return token.getToken().getClaimAsString("email");
    }

    public List<String> getRoles(JwtAuthenticationToken token) {
        // Keycloak nests roles under realm_access.roles
        Map<String, Object> realmAccess = token.getToken().getClaimAsMap("realm_access");
        if (realmAccess == null) return List.of();
        return (List<String>) realmAccess.getOrDefault("roles", List.of());
    }
}

控制器 — 访问当前用户

@RestController
@RequiredArgsConstructor
public class OrderController {

    @GetMapping("/api/v1/orders/my")
    public ApiResponse<List<OrderResponse>> myOrders(
        @AuthenticationPrincipal Jwt jwt  // inject JWT directly
    ) {
        String subject = jwt.getSubject(); // opaque provider id — map it to your user, don't parse it
        return ApiResponse.ok(orderService.findByOwnerSubject(subject));
    }

    // Or with JwtAuthenticationToken for full principal
    @GetMapping("/api/v1/profile")
    public ApiResponse<ProfileResponse> profile(JwtAuthenticationToken token) {
        return ApiResponse.ok(userService.findByEmail(
            token.getToken().getClaimAsString("email")
        ));
    }
}

使用作用域进行方法安全控制

@PreAuthorize("hasAuthority('SCOPE_orders:read')")
public List<Order> findAll() { ... }

@PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#orderId, authentication)")
public Order findById(UUID orderId) { ... }

// Custom security bean
@Component("orderSecurity")
public class OrderSecurityService {
    public boolean isOwner(UUID orderId, Authentication auth) {
        Jwt jwt = (Jwt) auth.getPrincipal();
        return orderRepository.existsByIdAndOwnerSubject(orderId, jwt.getSubject());
    }
}

注意事项

  • Agent 在 JwtGrantedAuthoritiesConverter 上调用 setAuthoritiesClaimName(...) —— 这会替换默认的 scope/scp 查找,因此 SCOPE_* 权限会消失(Okta/Entra 的 scp 作用域也会被丢弃);应扩展默认配置,而不是替换它
  • Agent 使用 UUID.fromString 解析 jwt.getSubject() —— Auth0(auth0|...)和 Okta(00u...)的主体不是 UUID;请求会失败并返回 500
  • Agent 省略 audiences —— 这样一来,资源服务会接受签发者为任意客户端签发的所有令牌
  • Agent 使用 hasRole("ADMIN") 检查作用域 —— 作用域应使用 hasAuthority("SCOPE_admin")
  • Agent 忘记配置 issuer-uri 验证 —— 务必配置,以防令牌伪造
  • Agent 为 Keycloak 错误映射角色 —— 角色嵌套在 realm_access.roles 下
  • Agent 直接使用 getPrincipal() —— 应转换为 Jwt,或使用 @AuthenticationPrincipal Jwt
  • Agent 添加 userDetailsService bean —— 资源服务不需要它(无状态 JWT)

评论

支持 Markdown