Keycloak + Spring Security OAuth 2.0 Resource Server
νλ‘ νΈμλ μ±λμμ Keycloak λ₯Ό ν΅ν΄ λ°κΈνμ¬ λ°±μλ μμ²μΌλ‘ μ λ¬λλ JWT ν ν°μ λν κ²μ¦μ μν΄ Spring Securityμ OAuth 2.0 Resource Serverλ₯Ό μ¬μ©νμ¬ JWT ν ν°μ΄ μ λ’°ν μ μλ κ³³μμ μλͺ λμλμ§ νμΈνλ κ²μ μμ보μ. λ³Έ κΈμμλ μ¬μ©μ μΈμ¦μ λν Authorization Code Flowλ₯Ό λ°±μλ μ±λλ‘ μ λ¬νμ¬ ν ν°μ κ΅ννλ κ³Όμ μ κ±°μΉμ§ μκ³ JavaScript Keycloak Adapterλ₯Ό μ¬μ©νμ¬ PKCE κΈ°λ°μΌλ‘ λ°κΈλ ν ν°μ μ λ¬λ°λλ€κ³ κ°μ νλ€.
- Keycloak 26.0.6
- Spring Boot 3.4.0
- spring-security-oauth2-resource-server:6.4.1
OAuth 2.0 Resource Server
μΌλ°μ μΈ μμ μμλ OAuth2 Authorization Server, OAuth2 Client λ₯Ό ν¬ν¨νμ§λ§ ν ν° λ°κΈ κ³Όμ μ μλΉμ€ μ ν리μΌμ΄μ
μμ μννμ§ μλλ€λ©΄ 리μμ€ μλ²μ λν μμ‘΄μ±μΈ spring-boot-starter-oauth2-resource-server
λ§μ ν¬ν¨ν΄λ λλ€
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-security'
}
application.ymlspring.security: oauth2: resourceserver: jwt: issuer-uri: ${keycloak.issuer-uri} keycloak: issuer-uri: http://localhost:8080/realms/mambo jwk-set-uri: http://localhost:8080/realms/mambo/protocol/openid-connect/cert client-id: backend
μλΉμ€ μ ν리μΌμ΄μ μμ μ¬μ©λ μ ν리μΌμ΄μ μμ±μ μμ κ°μΌλ©° μ€νλ§ μν리ν°μ λν μ€μ μ μ§νν΄λ³΄μ.
SecurityConfiguration@Configuration @RequiredArgsConstructor public class SecurityConfiguration { private final KeycloakProperties keycloakProperties; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.formLogin(AbstractHttpConfigurer::disable); http.httpBasic(AbstractHttpConfigurer::disable); http.csrf(AbstractHttpConfigurer::disable); http.authorizeHttpRequests(auth -> auth .requestMatchers("/").permitAll() .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() .anyRequest().denyAll()); http.oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.jwtAuthenticationConverter(new KeycloakJwtAuthenticationConverter()))); return http.build(); } }
KeycloakJwtAuthenticationConverterpublic class KeycloakJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> { private static final String TYPE = "type"; private static final String RESOURCE_ACCESS = "resource_access"; private static final String ACCOUNT = "account"; private static final String ROLES = "roles"; private static final String ROLE_PREFIX = "ROLE_"; @Override public AbstractAuthenticationToken convert(Jwt jwt) { Object username = jwt.getClaims().get("preferred_username"); Set<GrantedAuthority> authorities = extractAuthorities(jwt); return new JwtAuthenticationToken(jwt, authorities, username == null ? null : String.valueOf(username)); } private Set<GrantedAuthority> extractAuthorities(Jwt jwt) { Set<String> roleSet = new HashSet<>(); Map<String, Object> claims = jwt.getClaims(); for (Map.Entry<String, Object> entry : claims.entrySet()) { String key = entry.getKey(); if (key.equals(TYPE)) { roleSet.add(String.valueOf(entry.getValue())); } else if (key.equals(RESOURCE_ACCESS)) { Map<String, List<String>> resourceAccess = (Map<String, List<String>>) entry.getValue(); if (resourceAccess.containsKey(ACCOUNT)) { Map<String, List<String>> account = (Map<String, List<String>>) resourceAccess.get("account"); if (account.containsKey(ROLES)) { roleSet.addAll(account.get(ROLES)); } } } } return roleSet.stream() .map(role -> new SimpleGrantedAuthority(ROLE_PREFIX + role)) .collect(Collectors.toSet()); } }
μ μμμ κ°μ΄ KeycloakJwtAuthenticationConverter λ₯Ό ꡬννμ§ μκ³ JwtAuthenticationConveter λ₯Ό μ¬μ©νκ³ JwtGrantedAuthoritiesConverter λ₯Ό μμ±ν΄λ λλ€. μ°Έκ³ λ‘ Keycloak μμ λ°κΈλλ OIDC ν ν°μλ iss κ° ν¬ν¨λμ΄ μμΌλ―λ‘ JwtDecoder λλ issuer-uri μ jwk-set-uriλ₯Ό λͺ μνλκ² νμνμ§ μλ€.
JWT ν ν° κ²μ¦ μ€λ₯ μ
κΈ°λ³Έμ μΈ BearerTokenAuthenticationEntryPoint λ μ€λ₯μ λν λ΄μ©μ WWW-Authenticate
μλ΅ ν€λμ ν¬ν¨λλ€. λ§μ½, 401 Unauthorized
μ λν μ€λ₯ λ΄μ©μ μλ΅ λ°λλ‘ λ°ννκ³ μ νλ€λ©΄ AuthenticationEntryPoint 컀μ€ν°λ§μ΄μ§ ν΄μ μ€μ νλλ‘ νμ.
Resource Owner Password Credentials
ν΄λΌμ΄μΈνΈμ Direct access grants μ΅μ μ νμ±ν λμ΄μλ€λ©΄ Postmanμ ν΅ν΄ Resource Owner Password Credentials λ°©μμΌλ‘ ν ν°μ λ°κΈνμ¬ λ°±μλμ μ λ¬νμ¬ ν μ€νΈ ν΄λ³Ό μ μλ€.