title: Keycloak + Spring Security OAuth 2.0 Resource Server date: 2024-12-19T23:00+09:00 tags:
- 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'
}
spring.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
์๋น์ค ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ฌ์ฉ๋ ์ ํ๋ฆฌ์ผ์ด์ ์์ฑ์ ์์ ๊ฐ์ผ๋ฉฐ ์คํ๋ง ์ํ๋ฆฌํฐ์ ๋ํ ์ค์ ์ ์งํํด๋ณด์.
@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();
}
}
public 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 ๋ฐฉ์์ผ๋ก ํ ํฐ์ ๋ฐ๊ธํ์ฌ ๋ฐฑ์๋์ ์ ๋ฌํ์ฌ ํ ์คํธ ํด๋ณผ ์ ์๋ค.