๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Spring

Spring Boot 3 ํ™˜๊ฒฝ์—์„œ KeyCloak ์ ์šฉํ•˜๊ธฐ

by ํ‘์‹œ๋ฐ” 2023. 6. 25.

๐Ÿคฉ ๋ฐฐ๊ฒฝ

์Šคํ”„๋ง ๋ถ€ํŠธ 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

https://www.baeldung.com/spring-boot-keycloak

๋Œ“๊ธ€