Spring Boot Integration
Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.
This guide covers how to integrate Skycloak authentication and authorization into your Spring Boot applications using Spring Security OAuth2.
Prerequisites
Section titled “Prerequisites”- Spring Boot 3.x application (recommended)
- Java 17 or higher (Java 21 recommended for new projects)
- Skycloak cluster with configured realm and client
- Basic understanding of Spring Security
Quick Start
Section titled “Quick Start”1. Add Dependencies
Section titled “1. Add Dependencies”Add Spring Security OAuth2 dependencies to your pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
Or for Gradle (build.gradle.kts):
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
2. Configure Application Properties
Section titled “2. Configure Application Properties”Add the following to your application.yml:
spring:
security:
oauth2:
client:
registration:
skycloak:
client-id: your-client-id
client-secret: your-client-secret
authorization-grant-type: authorization_code
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
scope: openid, profile, email
provider:
skycloak:
issuer-uri: https://your-cluster-id.app.skycloak.io/realms/your-realm
resourceserver:
jwt:
issuer-uri: https://your-cluster-id.app.skycloak.io/realms/your-realm
3. Security Configuration
Section titled “3. Security Configuration”Create your security configuration class:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasAuthority("ROLE_ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("realm_access.roles");
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
Controller Examples
Section titled “Controller Examples”Protected REST API
Section titled “Protected REST API”@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/user/profile")
@PreAuthorize("hasRole('USER')")
public ResponseEntity<UserProfile> getUserProfile(Authentication authentication) {
Jwt jwt = (Jwt) authentication.getPrincipal();
UserProfile profile = new UserProfile();
profile.setId(jwt.getSubject());
profile.setUsername(jwt.getClaimAsString("preferred_username"));
profile.setEmail(jwt.getClaimAsString("email"));
profile.setRoles(extractRoles(jwt));
return ResponseEntity.ok(profile);
}
@PostMapping("/admin/users")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) {
// Admin-only endpoint
User user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
private List<String> extractRoles(Jwt jwt) {
Map<String, Object> realmAccess = jwt.getClaim("realm_access");
if (realmAccess != null && realmAccess.containsKey("roles")) {
return (List<String>) realmAccess.get("roles");
}
return Collections.emptyList();
}
}
Method-Level Security
Section titled “Method-Level Security”@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN') or #username == authentication.name")
public UserData getUserData(String username) {
// Users can access their own data, admins can access any
return userRepository.findByUsername(username);
}
@PostAuthorize("returnObject.owner == authentication.name")
public Document getDocument(Long documentId) {
// Ensures users can only retrieve their own documents
return documentRepository.findById(documentId);
}
@PreAuthorize("@securityService.hasPermission(#resourceId, 'WRITE')")
public void updateResource(Long resourceId, ResourceData data) {
// Custom permission checking
resourceRepository.update(resourceId, data);
}
}
Advanced Configuration
Section titled “Advanced Configuration”Custom JWT Claims Converter
Section titled “Custom JWT Claims Converter”@Component
public class KeycloakJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
@Override
public AbstractAuthenticationToken convert(Jwt jwt) {
Collection<GrantedAuthority> authorities = extractAuthorities(jwt);
Map<String, Object> claims = jwt.getClaims();
// Extract custom claims
String tenant = jwt.getClaimAsString("tenant");
List<String> groups = jwt.getClaimAsStringList("groups");
// Create custom authentication token
CustomAuthenticationToken token = new CustomAuthenticationToken(
jwt.getSubject(),
authorities,
tenant,
groups
);
return token;
}
private Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
Set<GrantedAuthority> authorities = new HashSet<>();
// Extract realm roles
Map<String, Object> realmAccess = jwt.getClaim("realm_access");
if (realmAccess != null) {
List<String> roles = (List<String>) realmAccess.get("roles");
roles.forEach(role -> authorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
}
// Extract resource roles
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
if (resourceAccess != null) {
resourceAccess.forEach((resource, access) -> {
Map<String, Object> resourceRoles = (Map<String, Object>) access;
List<String> roles = (List<String>) resourceRoles.get("roles");
roles.forEach(role -> authorities.add(
new SimpleGrantedAuthority(resource + "_" + role)
));
});
}
return authorities;
}
}
Multi-Tenancy Support
Section titled “Multi-Tenancy Support”@Configuration
public class MultiTenantSecurityConfig {
@Bean
public JwtIssuerAuthenticationManagerResolver authenticationManagerResolver() {
Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>();
return new JwtIssuerAuthenticationManagerResolver(
issuer -> authenticationManagers.computeIfAbsent(issuer, this::createAuthenticationManager)
);
}
private AuthenticationManager createAuthenticationManager(String issuer) {
JwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(issuer);
JwtAuthenticationProvider provider = new JwtAuthenticationProvider(jwtDecoder);
provider.setJwtAuthenticationConverter(jwtAuthenticationConverter());
return provider::authenticate;
}
}
Service-to-Service Authentication
Section titled “Service-to-Service Authentication”@Configuration
public class ServiceAuthenticationConfig {
@Value("${keycloak.client-id}")
private String clientId;
@Value("${keycloak.client-secret}")
private String clientSecret;
@Value("${keycloak.token-uri}")
private String tokenUri;
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
@Bean
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("skycloak");
return WebClient.builder()
.filter(oauth2Client)
.build();
}
}
Using the WebClient for authenticated requests:
@Service
public class ExternalApiService {
private final WebClient webClient;
public ExternalApiService(WebClient webClient) {
this.webClient = webClient;
}
public Mono<ApiResponse> callProtectedApi() {
return webClient
.get()
.uri("https://api.example.com/protected")
.retrieve()
.bodyToMono(ApiResponse.class);
}
}
Testing
Section titled “Testing”Integration Tests
Section titled “Integration Tests”@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = {
"spring.security.oauth2.resourceserver.jwt.issuer-uri=https://test.skycloak.io/realms/test"
})
class ApiControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockJwtAuth(authorities = "ROLE_USER", claims = @OpenIdClaims(sub = "user123"))
void getUserProfile_WithValidToken_ReturnsProfile() throws Exception {
mockMvc.perform(get("/api/user/profile"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value("user123"));
}
@Test
void getProtectedEndpoint_WithoutToken_ReturnsUnauthorized() throws Exception {
mockMvc.perform(get("/api/user/profile"))
.andExpect(status().isUnauthorized());
}
}
Mock JWT Authentication
Section titled “Mock JWT Authentication”@TestConfiguration
public class SecurityTestConfig {
@Bean
public JwtDecoder jwtDecoder() {
return mock(JwtDecoder.class);
}
@Bean
public JwtAuthenticationToken mockJwtAuthentication() {
Jwt jwt = Jwt.withTokenValue("mock-token")
.header("alg", "RS256")
.claim("sub", "test-user")
.claim("preferred_username", "testuser")
.claim("realm_access", Map.of("roles", List.of("USER", "ADMIN")))
.build();
return new JwtAuthenticationToken(jwt);
}
}
Production Best Practices
Section titled “Production Best Practices”1. Token Validation Caching
Section titled “1. Token Validation Caching”@Configuration
public class JwtCacheConfig {
@Bean
public JwtDecoder cachedJwtDecoder() {
NimbusJwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri);
// Configure caching
jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
JwtValidators.createDefaultWithIssuer(issuerUri),
new JwtTimestampValidator(Duration.ofMinutes(5))
));
// Add cache
return new CachingJwtDecoder(jwtDecoder);
}
}
2. Circuit Breaker for Token Validation
Section titled “2. Circuit Breaker for Token Validation”@Component
public class ResilientJwtDecoder implements JwtDecoder {
private final JwtDecoder delegate;
private final CircuitBreaker circuitBreaker;
public ResilientJwtDecoder(JwtDecoder delegate) {
this.delegate = delegate;
this.circuitBreaker = CircuitBreaker.ofDefaults("jwt-decoder");
circuitBreaker.getEventPublisher()
.onError(event -> log.error("JWT decoding error: {}", event.getThrowable().getMessage()));
}
@Override
public Jwt decode(String token) throws JwtException {
return circuitBreaker.executeSupplier(() -> delegate.decode(token));
}
}
3. Security Headers
Section titled “3. Security Headers”@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.frameOptions().deny()
.xssProtection().and()
.contentSecurityPolicy("default-src 'self'")
)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
return http.build();
}
4. Audit Logging
Section titled “4. Audit Logging”@Component
@Slf4j
public class SecurityAuditListener {
@EventListener
public void handleAuthenticationSuccess(AuthenticationSuccessEvent event) {
Authentication auth = event.getAuthentication();
log.info("User {} successfully authenticated", auth.getName());
auditService.logAuthenticationSuccess(auth);
}
@EventListener
public void handleAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
log.warn("Authentication failed: {}", event.getException().getMessage());
auditService.logAuthenticationFailure(event.getException());
}
@EventListener
public void handleAuthorizationFailure(AuthorizationDeniedEvent event) {
log.warn("Authorization denied for user {}: {}",
event.getAuthentication().getName(),
event.getAccessDeniedException().getMessage());
auditService.logAuthorizationFailure(event);
}
}
Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”-
401 Unauthorized Errors
- Verify token issuer URI matches exactly
- Check token expiration and clock skew
- Ensure client credentials are correct
-
Role Mapping Issues
- Verify role claim path (realm_access.roles vs resource_access)
- Check role prefix configuration (ROLE_ prefix)
- Ensure roles are assigned in Skycloak
-
CORS Problems
- Configure allowed origins properly
- Include Authorization in allowed headers
- Handle preflight requests
Debug Configuration
Section titled “Debug Configuration”logging:
level:
org.springframework.security: DEBUG
org.springframework.security.oauth2: TRACE
org.keycloak: DEBUG