ํ๋ก ํธ์๋ ์ฑ๋์์ 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 ๋ฐฉ์์ผ๋ก ํ ํฐ์ ๋ฐ๊ธํ์ฌ ๋ฐฑ์๋์ ์ ๋ฌํ์ฌ ํ ์คํธ ํด๋ณผ ์ ์๋ค.