跳到正文
cccskills
免费在 GitHub 公开

oauth2-resource-server

Use when configuring Spring Boot as an OAuth2 resource server, validating JWTs from an external auth provider (Keycloak, Auth0, Okta, Cognito), extracting claims, or implementing scope-based authorization.

中文简介正在准备中。当前显示原文说明。

查看安装方法

包含的文件(6)

  • SKILL.md5.8 KB
  • agents/openai.yaml207 B
  • examples/bad-resource-server-config.java1.7 KB
  • examples/good-resource-server-config.java1.7 KB
  • templates/JwtClaimExtractor.java1.8 KB
  • templates/ResourceServerConfig.java3.3 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

OAuth2 Resource Server

Dependency

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

Security Configuration

@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 — Common Providers

# 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

Custom Claim Extraction

@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());
    }
}

Controller — Accessing Current User

@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")
        ));
    }
}

Method Security with Scopes

@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());
    }
}

Gotchas

  • Agent calls setAuthoritiesClaimName(...) on JwtGrantedAuthoritiesConverter — it replaces the default scope/scp lookup, so SCOPE_* authorities vanish (and Okta/Entra scp scopes are dropped); extend the defaults instead
  • Agent parses jwt.getSubject() with UUID.fromString — Auth0 (auth0|...) and Okta (00u...) subjects aren't UUIDs; the request fails with 500
  • Agent omits audiences — the resource server then accepts any token the issuer minted for any client
  • Agent uses hasRole("ADMIN") for scope check — scopes use hasAuthority("SCOPE_admin")
  • Agent forgets issuer-uri validation — always configure to prevent token forgery
  • Agent maps roles wrong for Keycloak — roles are nested under realm_access.roles
  • Agent uses getPrincipal() directly — cast to Jwt or use @AuthenticationPrincipal Jwt
  • Agent adds userDetailsService bean — not needed for resource servers (stateless JWT)

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

Use when adding Spring AI-specific model observations, token usage, latency, externally configured cost attribution, advisor telemetry, or protected prompt and completion logging. Use production-observability for general service metrics, health, logs, and OTLP setup.

中文简介正在准备中。当前显示原文说明。

rrezartprebreza/spring-boot-skills3192026年10月17日 更新

Use when versioning Spring MVC or WebFlux APIs in Spring Boot 4 / Spring Framework 7. Covers built-in mapping versions, request version resolution, defaults, supported versions, and deprecation headers.

中文简介正在准备中。当前显示原文说明。

rrezartprebreza/spring-boot-skills3192026年10月17日 更新

Use when versioning Spring MVC or WebFlux APIs in Spring Boot 3 / Spring Framework 6. Covers explicit URL, header, and media-type strategies, compatibility rules, and deprecation handling.

中文简介正在准备中。当前显示原文说明。

rrezartprebreza/spring-boot-skills3192026年10月17日 更新

Use when introducing or correcting grouped Spring Boot configuration, typed property binding, validation, profiles or secret injection. Do not rewrite unrelated single-value configuration.

中文简介正在准备中。当前显示原文说明。

rrezartprebreza/spring-boot-skills3192026年10月17日 更新

Use when packaging a Spring Boot 3 application as an OCI image or GraalVM native executable. Covers buildpacks, layered images, JVM containers, AOT hints, probes, security, and verification.

中文简介正在准备中。当前显示原文说明。

rrezartprebreza/spring-boot-skills3192026年10月17日 更新

Use when packaging Spring Boot 4 as an OCI image, JVM container, AOT application, or GraalVM native executable. Covers buildpacks, layers, runtime hints, probes, security, and verification.

中文简介正在准备中。当前显示原文说明。

rrezartprebreza/spring-boot-skills3192026年10月17日 更新

查看 rrezartprebreza 的全部技能

报告此技能的问题