Spring Security 6: OAuth2 Resource Server einrichten

Praktischer Leitfaden zur Konfiguration eines OAuth2 Resource Servers mit Spring Security 6. JWT-Validierung, Issuer-Konfiguration, Scope-Verwaltung und Keycloak-Integration.

OAuth2 Resource Server Konfiguration mit Spring Security 6

Ein OAuth2 Resource Server schützt APIs durch die Validierung von JWT-Tokens, die von einem externen Authorization Server wie Keycloak, Auth0 oder Okta ausgestellt werden. Im Gegensatz zur benutzerdefinierten JWT-Authentifizierung, bei der die Anwendung ihre eigenen Tokens generiert, delegiert der Resource Server die Identitätsverwaltung vollständig an einen dedizierten Identity Provider (IdP).

Resource Server vs. benutzerdefiniertes JWT

Ein Resource Server generiert niemals Tokens. Er validiert sie ausschließlich. Diese Trennung der Zuständigkeiten stärkt die Sicherheit und vereinfacht die Architektur durch zentralisierte Identitätsverwaltung.

OAuth2 Resource Server Architektur

Der OAuth2-Flow umfasst drei Hauptakteure. Der Client (Frontend- oder Mobilanwendung) erhält ein Access Token vom Authorization Server. Anschließend bindet er dieses Token in jede Anfrage an den Resource Server ein. Der Resource Server validiert das Token, indem er dessen Signatur mit dem öffentlichen Schlüssel des Authorization Servers überprüft.

Diese Architektur bietet mehrere Vorteile. Der Resource Server bleibt zustandslos, da alle erforderlichen Informationen im JWT-Token enthalten sind. Die Validierung erfolgt dank asymmetrischer Signaturprüfung ohne Netzwerkaufrufe an den IdP. Mehrere APIs können denselben Authorization Server gemeinsam nutzen, was die Benutzerverwaltung vereinfacht.

text
┌──────────────┐     1. Login      ┌─────────────────────┐
│   Client     │ ───────────────►  │  Authorization      │
│   (SPA/App)  │                   │  Server (Keycloak)  │
│              │ ◄─────────────────│                     │
└──────────────┘   2. JWT Token    └─────────────────────┘
       │                                    │
       │ 3. Request + Bearer Token          │
       ▼                                    ▼
┌──────────────────────┐           ┌─────────────────────┐
│   Resource Server    │ ◄─────── │   JWKS Endpoint     │
│   (Spring Boot API)  │  4. Public Keys (cached)       │
└──────────────────────┘           └─────────────────────┘

Der Resource Server lädt die öffentlichen Schlüssel beim Start herunter und cacht sie, wodurch Netzwerkaufrufe für jede Anfrage vermieden werden.

Maven-Abhängigkeiten für OAuth2 Resource Server

Die Konfiguration eines Resource Servers erfordert zwei spezifische Abhängigkeiten. Der Starter oauth2-resource-server stellt die Basisinfrastruktur bereit, während oauth2-jose die Klassen zum Dekodieren und Validieren von JWTs enthält.

xml
<!-- pom.xml -->
<dependencies>
    <!-- OAuth2 Resource Server support -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>

    <!-- Spring Web for REST endpoints -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- DTO validation -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
</dependencies>

Zusätzliche Abhängigkeiten wie JJWT sind nicht erforderlich. Spring Security verwendet die in spring-security-oauth2-jose enthaltene Nimbus JOSE+JWT-Bibliothek für die Token-Verarbeitung.

Grundkonfiguration mit issuer-uri

Die minimale Resource-Server-Konfiguration erfordert lediglich zwei Zeilen. Spring Security ermittelt die IdP-Endpunkte automatisch über das OpenID Connect Discovery-Protokoll.

yaml
# application.yml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          # URI des Authorization Servers
          # Spring ruft automatisch die öffentlichen Schlüssel über /.well-known/openid-configuration ab
          issuer-uri: https://keycloak.example.com/realms/myrealm

Ausgehend von der issuer-uri sendet Spring Security eine Anfrage an {issuer-uri}/.well-known/openid-configuration, um die Metadaten des Authorization Servers abzurufen. Anschließend extrahiert es die JWKS-URL (JSON Web Key Set), die die öffentlichen Schlüssel zur Validierung der Token-Signaturen enthält.

SecurityConfig.javajava
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            // Disable CSRF for stateless APIs
            .csrf(csrf -> csrf.disable())

            // Authorization rules
            .authorizeHttpRequests(auth -> auth
                // Public endpoints
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/actuator/health").permitAll()
                // All other requests require a valid token
                .anyRequest().authenticated()
            )

            // Enable OAuth2 JWT validation
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults())
            )

            // Stateless mode required for Resource Servers
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )

            .build();
    }
}

Diese Konfiguration reicht aus, um eine API zu schützen. Spring Security validiert automatisch die Token-Signatur, prüft die Zeitstempel (exp, nbf, iat) und stellt sicher, dass der iss-Claim mit der konfigurierten issuer-uri übereinstimmt.

Start ohne IdP

Standardmäßig startet die Anwendung nicht, wenn der Authorization Server nicht erreichbar ist. Um einen unabhängigen Start zu ermöglichen, sollte jwk-set-uri explizit anstelle von issuer-uri konfiguriert werden.

Erweiterte Konfiguration mit jwk-set-uri

Wenn der Authorization Server kein OpenID Connect Discovery unterstützt oder die Anwendung ohne Netzwerkabhängigkeit zum IdP starten muss, ist eine explizite JWKS-Konfiguration vorzuziehen.

yaml
# application.yml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          # Direct URL to JWKS (public keys)
          jwk-set-uri: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/certs
          # Expected value of the "iss" claim in tokens
          issuer-uri: https://keycloak.example.com/realms/myrealm
          # Allowed audiences ("aud" claim)
          audiences:
            - my-api
            - account

Dieser Ansatz bietet mehr Kontrolle. Die Audience-Validierung verhindert die Verwendung von Tokens, die für andere Dienste bestimmt sind. Ein für die Anwendung frontend-app ausgestelltes Token wird abgelehnt, wenn die API nur die Audiences my-api und account akzeptiert.

SecurityConfig.javajava
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
    private String issuerUri;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())

            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )

            // Custom JWT configuration
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            )

            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )

            .build();
    }

    // Custom converter to extract authorities from the token
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
            new JwtGrantedAuthoritiesConverter();
        // SCOPE_ prefix for authorities (e.g., SCOPE_read, SCOPE_write)
        grantedAuthoritiesConverter.setAuthorityPrefix("SCOPE_");
        // Claim containing scopes (OAuth2 standard)
        grantedAuthoritiesConverter.setAuthoritiesClaimName("scope");

        JwtAuthenticationConverter jwtAuthenticationConverter =
            new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(
            grantedAuthoritiesConverter
        );

        return jwtAuthenticationConverter;
    }
}

Der JwtAuthenticationConverter wandelt die Token-Claims in GrantedAuthority-Objekte um, die in Sicherheitsausdrücken verwendet werden können.

Bereit für deine Spring Boot-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Extraktion von Keycloak-Rollen

Keycloak strukturiert Rollen anders als der OAuth2-Standard. Die Rollen befinden sich in realm_access.roles (Realm-Rollen) oder resource_access.{client}.roles (clientspezifische Rollen). Ein benutzerdefinierter Converter extrahiert diese Informationen.

KeycloakJwtAuthenticationConverter.javajava
@Component
public class KeycloakJwtAuthenticationConverter
    implements Converter<Jwt, AbstractAuthenticationToken> {

    private static final String REALM_ACCESS_CLAIM = "realm_access";
    private static final String RESOURCE_ACCESS_CLAIM = "resource_access";
    private static final String ROLES_CLAIM = "roles";

    @Value("${keycloak.client-id}")
    private String clientId;

    @Override
    public AbstractAuthenticationToken convert(Jwt jwt) {
        // Combine realm and client roles
        Collection<GrantedAuthority> authorities = Stream.concat(
            extractRealmRoles(jwt).stream(),
            extractClientRoles(jwt).stream()
        ).collect(Collectors.toSet());

        // Return authentication token with extracted authorities
        return new JwtAuthenticationToken(jwt, authorities, extractUsername(jwt));
    }

    // Extract realm-level roles
    private Collection<GrantedAuthority> extractRealmRoles(Jwt jwt) {
        Map<String, Object> realmAccess = jwt.getClaim(REALM_ACCESS_CLAIM);
        if (realmAccess == null) {
            return Collections.emptyList();
        }

        @SuppressWarnings("unchecked")
        List<String> roles = (List<String>) realmAccess.get(ROLES_CLAIM);
        if (roles == null) {
            return Collections.emptyList();
        }

        // ROLE_ prefix for compatibility with hasRole()
        return roles.stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
            .collect(Collectors.toList());
    }

    // Extract client-specific roles
    private Collection<GrantedAuthority> extractClientRoles(Jwt jwt) {
        Map<String, Object> resourceAccess = jwt.getClaim(RESOURCE_ACCESS_CLAIM);
        if (resourceAccess == null) {
            return Collections.emptyList();
        }

        @SuppressWarnings("unchecked")
        Map<String, Object> clientAccess =
            (Map<String, Object>) resourceAccess.get(clientId);
        if (clientAccess == null) {
            return Collections.emptyList();
        }

        @SuppressWarnings("unchecked")
        List<String> roles = (List<String>) clientAccess.get(ROLES_CLAIM);
        if (roles == null) {
            return Collections.emptyList();
        }

        return roles.stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
            .collect(Collectors.toList());
    }

    // Extract username from token
    private String extractUsername(Jwt jwt) {
        // Keycloak uses "preferred_username" by default
        String username = jwt.getClaimAsString("preferred_username");
        if (username != null) {
            return username;
        }
        // Fallback to subject (usually the user UUID)
        return jwt.getSubject();
    }
}

Die Integration in die Sicherheitskonfiguration erfolgt über den Parameter jwtAuthenticationConverter.

SecurityConfig.javajava
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final KeycloakJwtAuthenticationConverter keycloakConverter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())

            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                // Protection by Keycloak role
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )

            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    // Use Keycloak converter
                    .jwtAuthenticationConverter(keycloakConverter)
                )
            )

            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )

            .build();
    }
}

Die extrahierten Rollen sind nun mit @PreAuthorize("hasRole('ADMIN')")-Annotationen oder in requestMatchers().hasRole()-Ausdrücken verwendbar.

Benutzerdefinierte Token-Validierung

Je nach geschäftlichen Anforderungen können zusätzliche Validierungen erforderlich sein: Überprüfung benutzerdefinierter Claims, Audience-Validierung oder Kontrolle der maximalen Lebensdauer.

CustomJwtValidator.javajava
@Component
public class CustomJwtValidator implements OAuth2TokenValidator<Jwt> {

    private static final OAuth2Error AUDIENCE_ERROR =
        new OAuth2Error("invalid_token", "Token audience is not valid", null);

    private static final OAuth2Error CUSTOM_CLAIM_ERROR =
        new OAuth2Error("invalid_token", "Required custom claim missing", null);

    @Value("${app.jwt.required-audience}")
    private String requiredAudience;

    @Value("${app.jwt.required-tenant-claim:tenant_id}")
    private String tenantClaimName;

    @Override
    public OAuth2TokenValidatorResult validate(Jwt jwt) {
        List<OAuth2Error> errors = new ArrayList<>();

        // Audience validation
        if (!validateAudience(jwt)) {
            errors.add(AUDIENCE_ERROR);
        }

        // Custom business claim validation
        if (!validateTenantClaim(jwt)) {
            errors.add(CUSTOM_CLAIM_ERROR);
        }

        if (!errors.isEmpty()) {
            return OAuth2TokenValidatorResult.failure(errors);
        }

        return OAuth2TokenValidatorResult.success();
    }

    private boolean validateAudience(Jwt jwt) {
        List<String> audiences = jwt.getAudience();
        return audiences != null && audiences.contains(requiredAudience);
    }

    private boolean validateTenantClaim(Jwt jwt) {
        // Check for required business claim
        String tenantId = jwt.getClaimAsString(tenantClaimName);
        return tenantId != null && !tenantId.isBlank();
    }
}

Der benutzerdefinierte Validator wird in die JwtDecoder-Konfiguration integriert.

JwtConfig.javajava
@Configuration
@RequiredArgsConstructor
public class JwtConfig {

    private final CustomJwtValidator customValidator;

    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
    private String issuerUri;

    @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
    private String jwkSetUri;

    @Bean
    public JwtDecoder jwtDecoder() {
        // Create decoder with JWKS URL
        NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder
            .withJwkSetUri(jwkSetUri)
            .build();

        // Combine default validators with custom validator
        OAuth2TokenValidator<Jwt> defaultValidators =
            JwtValidators.createDefaultWithIssuer(issuerUri);

        OAuth2TokenValidator<Jwt> combinedValidator =
            new DelegatingOAuth2TokenValidator<>(defaultValidators, customValidator);

        jwtDecoder.setJwtValidator(combinedValidator);

        return jwtDecoder;
    }
}
Validierungsleistung

Synchrone Validierungen blockieren den Verarbeitungs-Thread. Für Validierungen, die externe Aufrufe erfordern (Datenbank, Drittanbieter-Dienste), ist ein asynchroner Filter oder eine nachgelagerte Überprüfung vorzuziehen.

Zugriff auf Token-Informationen in Controllern

Spring Security stellt mehrere Methoden bereit, um in Controllern auf JWT-Token-Informationen zuzugreifen.

UserController.javajava
@RestController
@RequestMapping("/api/users")
public class UserController {

    // Direct JWT injection via @AuthenticationPrincipal
    @GetMapping("/me")
    public ResponseEntity<UserInfoResponse> getCurrentUser(
        @AuthenticationPrincipal Jwt jwt
    ) {
        String userId = jwt.getSubject();
        String email = jwt.getClaimAsString("email");
        String username = jwt.getClaimAsString("preferred_username");
        List<String> roles = extractRoles(jwt);

        return ResponseEntity.ok(new UserInfoResponse(
            userId, email, username, roles
        ));
    }

    // Alternative with JwtAuthenticationToken to access authorities
    @GetMapping("/profile")
    public ResponseEntity<ProfileResponse> getProfile(
        JwtAuthenticationToken authentication
    ) {
        Jwt jwt = authentication.getToken();
        Collection<String> authorities = authentication.getAuthorities()
            .stream()
            .map(GrantedAuthority::getAuthority)
            .toList();

        return ResponseEntity.ok(new ProfileResponse(
            jwt.getSubject(),
            jwt.getClaimAsString("name"),
            authorities
        ));
    }

    // Access scopes for conditional business logic
    @GetMapping("/data")
    @PreAuthorize("hasAuthority('SCOPE_read')")
    public ResponseEntity<DataResponse> getData(
        @AuthenticationPrincipal Jwt jwt
    ) {
        boolean canWrite = hasScope(jwt, "write");

        // Business logic adapted according to permissions
        DataResponse response = buildDataResponse(jwt.getSubject(), canWrite);
        return ResponseEntity.ok(response);
    }

    private List<String> extractRoles(Jwt jwt) {
        Map<String, Object> realmAccess = jwt.getClaim("realm_access");
        if (realmAccess == null) {
            return Collections.emptyList();
        }
        @SuppressWarnings("unchecked")
        List<String> roles = (List<String>) realmAccess.get("roles");
        return roles != null ? roles : Collections.emptyList();
    }

    private boolean hasScope(Jwt jwt, String scope) {
        String scopes = jwt.getClaimAsString("scope");
        return scopes != null && scopes.contains(scope);
    }
}
UserInfoResponse.javajava
public record UserInfoResponse(
    String userId,
    String email,
    String username,
    List<String> roles
) {}

// ProfileResponse.java
public record ProfileResponse(
    String userId,
    String name,
    Collection<String> authorities
) {}

Die Annotation @AuthenticationPrincipal vermeidet das manuelle Abrufen der Authentifizierung aus dem SecurityContextHolder.

Behandlung von OAuth2-Authentifizierungsfehlern

Eine spezifische Fehlerbehandlung verbessert die Entwicklererfahrung für API-Konsumenten.

OAuth2SecurityExceptionHandler.javajava
@RestControllerAdvice
public class OAuth2SecurityExceptionHandler {

    private static final Logger log =
        LoggerFactory.getLogger(OAuth2SecurityExceptionHandler.class);

    // Missing token or invalid format
    @ExceptionHandler(AuthenticationException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ErrorResponse handleAuthenticationException(AuthenticationException ex) {
        log.warn("Authentication failed: {}", ex.getMessage());

        return new ErrorResponse(
            "UNAUTHORIZED",
            "Authentication required. Provide a valid Bearer token.",
            Map.of("error", ex.getMessage())
        );
    }

    // Access denied despite valid token
    @ExceptionHandler(AccessDeniedException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public ErrorResponse handleAccessDeniedException(AccessDeniedException ex) {
        log.warn("Access denied: {}", ex.getMessage());

        return new ErrorResponse(
            "FORBIDDEN",
            "Insufficient permissions for this resource",
            null
        );
    }

    // OAuth2-specific error (expired token, invalid signature, etc.)
    @ExceptionHandler(OAuth2AuthenticationException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ErrorResponse handleOAuth2Exception(OAuth2AuthenticationException ex) {
        OAuth2Error error = ex.getError();
        log.warn("OAuth2 authentication error: {} - {}",
            error.getErrorCode(), error.getDescription());

        String message = switch (error.getErrorCode()) {
            case "invalid_token" -> "Token is invalid or expired";
            case "insufficient_scope" -> "Token does not have required scopes";
            default -> "Authentication failed";
        };

        return new ErrorResponse(
            error.getErrorCode().toUpperCase(),
            message,
            Map.of("details", error.getDescription())
        );
    }
}

// ErrorResponse.java
public record ErrorResponse(
    String code,
    String message,
    Map<String, String> details
) {}

Standardisierte OAuth2-Fehlercodes (invalid_token, insufficient_scope) erleichtern die clientseitige Verarbeitung.

Multi-Umgebungs-Konfiguration

Die Konfiguration variiert je nach Umgebung. In der Entwicklung kann ein lokaler Keycloak-Server verwendet werden, während in der Produktion ein verwalteter IdP wie Auth0 die Aufgabe übernimmt.

yaml
# application.yml (common configuration)
app:
  jwt:
    required-audience: my-api

---
# application-dev.yml
spring:
  config:
    activate:
      on-profile: dev
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8180/realms/dev-realm

keycloak:
  client-id: my-api-dev

logging:
  level:
    org.springframework.security: DEBUG

---
# application-prod.yml
spring:
  config:
    activate:
      on-profile: prod
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: ${OAUTH2_ISSUER_URI}
          audiences: ${OAUTH2_AUDIENCES:my-api}

keycloak:
  client-id: ${KEYCLOAK_CLIENT_ID}

logging:
  level:
    org.springframework.security: WARN

In der Produktion stammen sensible Werte aus Umgebungsvariablen oder einem Secrets-Manager.

Integrationstests mit gemockten JWTs

Integrationstests verwenden die Spring Security Test-Utilities, um gültige JWT-Tokens zu simulieren.

ResourceServerIntegrationTest.javajava
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class ResourceServerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void shouldRejectRequestWithoutToken() throws Exception {
        mockMvc.perform(get("/api/users/me"))
            .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockJwtAuth(
        claims = @OpenIdClaims(sub = "user-123", preferredUsername = "john.doe"),
        authorities = {"ROLE_USER"}
    )
    void shouldAllowAuthenticatedUser() throws Exception {
        mockMvc.perform(get("/api/users/me"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.userId").value("user-123"))
            .andExpect(jsonPath("$.username").value("john.doe"));
    }

    @Test
    void shouldAllowAccessWithValidJwt() throws Exception {
        mockMvc.perform(get("/api/users/me")
                .with(jwt()
                    .jwt(jwt -> jwt
                        .subject("user-456")
                        .claim("preferred_username", "jane.doe")
                        .claim("email", "jane@example.com")
                    )
                    .authorities(new SimpleGrantedAuthority("ROLE_USER"))
                ))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.email").value("jane@example.com"));
    }

    @Test
    void shouldDenyAccessWithoutAdminRole() throws Exception {
        mockMvc.perform(get("/api/admin/users")
                .with(jwt()
                    .authorities(new SimpleGrantedAuthority("ROLE_USER"))
                ))
            .andExpect(status().isForbidden());
    }

    @Test
    void shouldAllowAdminAccess() throws Exception {
        mockMvc.perform(get("/api/admin/users")
                .with(jwt()
                    .authorities(new SimpleGrantedAuthority("ROLE_ADMIN"))
                ))
            .andExpect(status().isOk());
    }
}

Das jwt()-Utility aus spring-security-test erstellt gemockte Tokens, ohne dass ein Authorization Server erforderlich ist.

SecurityTestConfig.javajava
@TestConfiguration
public class SecurityTestConfig {

    // Mocked JwtDecoder for tests
    @Bean
    @Primary
    public JwtDecoder jwtDecoder() {
        return token -> {
            // Return a mocked JWT for tests
            return Jwt.withTokenValue(token)
                .header("alg", "RS256")
                .subject("test-user")
                .claim("scope", "read write")
                .build();
        };
    }
}

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Fazit

Die Konfiguration eines OAuth2 Resource Servers mit Spring Security 6 zentralisiert die Token-Validierung und vereinfacht die API-Sicherheit. Der Authorization Server übernimmt die Authentifizierung und Token-Ausstellung, während sich die API auf die Validierung und Extraktion von Identitätsinformationen konzentriert.

Deployment-Checkliste:

  • issuer-uri oder jwk-set-uri je nach Anforderungen konfigurieren
  • ✅ Audience validieren, um Missbrauch von Tokens anderer Dienste zu verhindern
  • ✅ Benutzerdefinierter Converter zur Extraktion von Keycloak-Rollen
  • ✅ OAuth2-Fehlerbehandlung mit standardisierten Codes
  • ✅ Integrationstests mit gemockten Tokens
  • ✅ HTTPS zwischen Client und Resource Server obligatorisch
  • ✅ Umgebungsspezifische Konfiguration
  • ✅ Logging fehlgeschlagener Authentifizierungsversuche

Diese Architektur fügt sich natürlich in ein Microservices-Ökosystem ein, in dem mehrere APIs denselben Identity Provider nutzen.

Tags

#spring security
#oauth2
#resource server
#jwt
#spring boot

Teilen

Verwandte Artikel