๐คฉ ๋ฐฐ๊ฒฝ
์คํ๋ง ๋ถํธ 2 ๋ฒ์ ์์๋ Keycloak ํด๋ผ์ด์ธํธ ์ด๋ํฐ(KeycloakWebSecurityConfigurerAdapter)๋ฅผ ์ฌ์ฉํ์ฌ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฝ๊ฒ ์ฐ๊ฒฐํด์ ์ฌ์ฉํ๋ค. ํ์ง๋ง ์คํ๋ง ๋ถํธ 3๋ถํฐ ์ผ๋ถ ํด๋์ค, ๋ฉ์๋, ์์ฑ ๋ฐ ์ด๋ ธํ ์ด์ ์ด ์ ๊ฑฐ๋์๋ค.
ํด๋น ํฌ์คํธ์์๋ ์คํ๋ง ๋ถํธ 3 ํ๊ฒฝ์์ Spring Security Oauth2์ KeyCloak์ ์ฐ๊ฒฐํด์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๊ณต์ ํ๋ค.
1. Dependency ์ถ๊ฐ
์ฐ์ , oauth2 resource service ์์กด์ฑ์ ์ถ๊ฐํ๋ค.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
2. properties ์ค์
Spring Security์ ๊ด๋ จ ์ค์ ์ ์ถ๊ฐํ๋ค.
spring.security.oauth2.resourceserver.jwt.issuer-uri = http://localhost:8180/realms/spmia-realm
spring.security.oauth2.resourceserver.jwt.jwk-set-uri = ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs
jwt.auth.converter.resource-id = {userid}
jwt.auth.converter.principal-attribute = preferred_username
ํด๋น ์ ๋ณด๋ KeyCloak - Realm settings ๋ฉ๋ด์์ Endpoints - OpenID Endpoint Configuration์์ ํ์ธ ๊ฐ๋ฅํ๋ค.
OpenID Endpoint Configuration์ ํด๋ฆญํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ์ ๋ณด๋ฅผ ์ป์ ์ ์๋ค.
๊ทธ์ค์์ issuer, jwks_uri์ ๋ํ ์ ๋ณด๋ฅผ properties์ ์ ๋ ฅํ๋ฉด ๋๋ค.
3. ์ฝ๋ ์์ฑ
์ฐ์ , properties์์ ์ค์ ํ jwt ๊ตฌ์ฑ ์ ๋ณด๋ฅผ ์ ํ๋ฆฌ์ผ์ด์ ์ผ๋ก ๊ฐ์ง๊ณ ์ค๋ Properties ํด๋์ค๋ฅผ ์์ฑํ๋ค.
@Data
@Validated
@Configuration
@ConfigurationProperties(prefix = "jwt.auth.converter")
public class JwtAuthConverterProperties {
private String resourceId;
private String principalAttribute;
}
๋ค์์ผ๋ก, JWT ๋ฐ์ดํฐ์์ ์ญํ ๋ฑ์ ์ ๋ณด๋ฅผ ์ถ์ถํด์ JwtAuthenticationToken(Authentication)์ผ๋ก ๋ณํํ๋ Convert ํด๋์ค๋ฅผ ์ถ๊ฐํ๋ค.
@Component
public class JwtAuthConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
private final JwtAuthConverterProperties properties;
public JwtAuthConverter(JwtAuthConverterProperties properties) {
this.properties = properties;
}
@Override
public AbstractAuthenticationToken convert(Jwt jwt) {
Collection<GrantedAuthority> authorities = Stream.concat(
jwtGrantedAuthoritiesConverter.convert(jwt).stream(),
extractResourceRoles(jwt).stream()).collect(Collectors.toSet());
return new JwtAuthenticationToken(jwt, authorities, getPrincipalClaimName(jwt));
}
private String getPrincipalClaimName(Jwt jwt) {
String claimName = JwtClaimNames.SUB;
if (properties.getPrincipalAttribute() != null) {
claimName = properties.getPrincipalAttribute();
}
return jwt.getClaim(claimName);
}
private Collection<? extends GrantedAuthority> extractResourceRoles(Jwt jwt) {
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
Map<String, Object> resource;
Collection<String> resourceRoles;
if (resourceAccess == null
|| (resource = (Map<String, Object>) resourceAccess.get(properties.getResourceId())) == null
|| (resourceRoles = (Collection<String>) resource.get("roles")) == null) {
return Set.of();
}
return resourceRoles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toSet());
}
}
๋ง์ง๋ง์ผ๋ก, Spring Security์์ Oauth2 ResourceServer ์ค์ ์ ์ถ๊ฐํ๋ค.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthConverter jwtAuthConverter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authz -> authz.anyRequest().authenticated())
.oauth2ResourceServer(authz -> authz.jwt(jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwtAuthConverter)))
.sessionManagement(authz -> authz.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(authz -> authz.disable());
return http.build();
}
}
4. ํ ์คํธ
KeyCloak ์๋ฒ์์ ์ธ์ฆ ํ access_token ๊ฐ์ Bearer Token ๋ฐฉ์์ผ๋ก ์ ๋ฌํ๋ฉด ๋๋ค.
REFERNECE
https://medium.com/geekculture/using-keycloak-with-spring-boot-3-0-376fa9f60e0b
๋๊ธ