1. Authorization Code
Initiating the Authorization Request
OAuth2AuthorizationRequestRedirectFilter
- 로그인 요청을 시작하는 필터
- OAuth2AuthorizationRequestResolver를 사용해서 OAuth2AuthorizationRequest 생성
- user-agent를 Authorization Sever의 authorization endpoint로 리디렉션함
OAuth2AuthorizationRequestResolver
- 웹 요청 정보에서 OAuth2AuthorizationRequest를 구성해주는 역할
PKCE (Proof Key for Code Exchange)
- OAuth 2.0 Authorization Code Grant 흐름의 보안 강화를 위해 도입된 추가 인증 메커니즘
| 항목 | 내용 | 
| 목적 | Authorization Code 탈취 방지 | 
| 취약점 | - code는 redirect_uri를 통해 전달되므로 브라우저 주소창에 노출됨 - 악성 앱이나 프록시가 이 인가 코드를 가로챌 수 있음 | 
| 사용 대상 | authorization_code grant type을 사용하는 Public Client (SPA, Mobile 앱 등) | 
| 주요 흐름 | 1. client: code_verifier 생성 2. user-agent: 인가 요청 시 code_challenge를 authorization server에 보냄 3. client: 토큰 요청 시 code_verifier를 authorization server에 보냄 4. authorization server가 둘이 일치하는지 확인 후 토큰 발급 | 
| 용어 | - code_verifier: 클라이언트가 생성한 랜덤 문자열 - code_challenge: code_verifier를 SHA256 후 base64url 인코딩한 값 - code_challenge_method: 보통 "S256" (해시 방식 지정) | 
Customizing the Authorization Request
- OAuth2 인가 요청 외에 추가 파라미터를 인가 요청에 포함시키고 싶을 때 사용
- OAuth2AuthorizationRequestResolver를 커스터마이징해서 처리
커스터마이징) authorizationRequestResolver
더보기
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authz -> authz.anyRequest().authenticated())
        .oauth2Login(oauth2 -> oauth2
            .authorizationEndpoint(endpoint -> endpoint
                .authorizationRequestResolver(
                    customAuthorizationRequestResolver(clientRegistrationRepository)
                )
            )
        );
    return http.build();
}private OAuth2AuthorizationRequestResolver customAuthorizationRequestResolver(
        ClientRegistrationRepository clientRegistrationRepository) {
    DefaultOAuth2AuthorizationRequestResolver resolver =
        new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, "/oauth2/authorization");
    resolver.setAuthorizationRequestCustomizer(
        customizer -> customizer
            .additionalParameters(params -> params.put("prompt", "consent"))
    );
    return resolver;
}
Storing the Authorization Request
- AuthorizationRequestRepository: 인가 요청과 인가 응답 사이에 요청 정보를 저장하고 복구하는 역할
- HttpSessionOAuth2AuthorizationRequestRepository (기본) : HttpSession에 OAuth2AuthorizationRequest 저장
커스터마이징) AuthorizationRequestRepository
더보기
@Bean
public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {
    return new CustomOAuth2AuthorizationRequestRepository(); // 직접 구현한 저장소
}- 저장소, 쿠키 등 구현 가능
설정) SecurityFilterChain
더보기
.oauth2Login(oauth2 -> oauth2
    .authorizationEndpoint(endpoint -> endpoint
        .authorizationRequestRepository(authorizationRequestRepository())
    )
)
.oauth2Client(oauth2 -> oauth2
    .authorizationCodeGrant(codeGrant -> codeGrant
        .authorizationRequestRepository(authorizationRequestRepository())
    )
)- login, server to server에서 사용하므로 두 곳에 설정해주기
Requesting an Access Token
OAuth2AccessTokenResponseClient
- authorization_code로 받은 code를 이용해 token_endpoint로 HTTP 요청을 보내는 역할을 하는 객체
| 구현체 | 설명 | 
| DefaultAuthorizationCodeTokenResponseClient | 기본 구현 (기존 RestTemplate 기반) | 
| RestClientAuthorizationCodeTokenResponseClient | 새로운 구현 (RestClient 기반, WebClient 스타일) | 
설정) RestClientAuthorizationCodeTokenResponseClient
더보기
@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
    return new RestClientAuthorizationCodeTokenResponseClient();
}- 이 Bean을 등록해두면, OAuth2AuthorizedClientManager 또는 로그인 필터가 자동으로 사용함
커스터마이징) RestClientAuthorizationCodeTokenResponseClient
더보기
@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
    RestClientAuthorizationCodeTokenResponseClient client =
        new RestClientAuthorizationCodeTokenResponseClient();
    // 필요에 따라 RequestEntityConverter, ResponseConverter 등을 설정 가능
    client.setRequestEntityConverter(customRequestConverter());
    client.setRestClient(customRestClient());
    return client;
}private Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> customRequestConverter() {
    return request -> {
        OAuth2AuthorizationCodeGrantRequest authRequest = request;
        ClientRegistration registration = authRequest.getClientRegistration();
        MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
        form.add("grant_type", "authorization_code");
        form.add("code", authRequest.getAuthorizationExchange().getAuthorizationResponse().getCode());
        form.add("redirect_uri", authRequest.getAuthorizationExchange().getAuthorizationRequest().getRedirectUri());
        // ✅ 기본 방식은 client_id + client_secret
        // 필요하면 여기에 다른 custom 파라미터도 추가 가능
        form.add("client_id", registration.getClientId());
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        return new RequestEntity<>(form, headers, HttpMethod.POST, URI.create(registration.getProviderDetails().getTokenUri()));
    };
}private RestClient customRestClient() {
    return RestClient.builder()
        .requestInterceptor((request, body, execution) -> {
            // ✅ 예: 로그 출력 또는 추가 헤더 삽입
            request.getHeaders().add("X-Custom-Header", "MyValue");
            return execution.execute(request, body);
        })
        .build();
}
Customizing the Access Token Request
- Access Token 요청의 헤더 및 파라미터 커스터마이징
- RestClientAuthorizationCodeTokenResponseClient 설정을 통해 가능
커스터마이징) 요청 헤더
더보기
addHeadersConverter()
accessTokenResponseClient.addHeadersConverter(grantRequest -> {
    HttpHeaders headers = new HttpHeaders();
    if (grantRequest.getClientRegistration().getRegistrationId().equals("spring")) {
        headers.set(HttpHeaders.USER_AGENT, "my-user-agent");
    }
    return headers;
});- 필요한 헤더만 추가됨 (기본 헤더 유지)
setHeadersConverter()
DefaultOAuth2TokenRequestHeadersConverter headersConverter =
    new DefaultOAuth2TokenRequestHeadersConverter();
headersConverter.setEncodeClientCredentials(false); // 기본 Basic 인증 제거
accessTokenResponseClient.setHeadersConverter(headersConverter);- 특정 헤더 제거 가능
- 전체 헤더 새로 구성 가능
커스터마이징) 파라미터
더보기
addParametersConverter()
accessTokenResponseClient.addParametersConverter(grantRequest -> {
    MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
    if (grantRequest.getClientRegistration().getRegistrationId().equals("keycloak")) {
        parameters.set(OAuth2ParameterNames.AUDIENCE, "my-audience");
    }
    return parameters;
});- 필요한 파라미터만 추가 (기본 파라미터 유지)
setParametersConverter()
accessTokenResponseClient.setParametersConverter(grantRequest -> {
    LinkedMultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
    if (grantRequest.getClientRegistration().getRegistrationId().equals("okta")) {
        parameters.set(OAuth2ParameterNames.CLIENT_ID, "my-client");
    }
    return parameters;
});- 직접 기본 파라미터도 모두 넣어야 함
setParametersCustomizer()
accessTokenResponseClient.setParametersCustomizer(parameters -> {
    if (parameters.containsKey(OAuth2ParameterNames.CLIENT_ASSERTION)) {
        parameters.remove(OAuth2ParameterNames.CLIENT_ID);
    }
});- Spring Security가 구성한 기본 파라미터 리스트에 대해 후처리 (제거 및 조건부 수정 가능)
Customizing the Access Token Response
- OAuth2 Access Token 응답을 커스터마이징할 수 있음
기본) RestClient
더보기
RestClient restClient = RestClient.builder()
    .messageConverters(converters -> {
        converters.clear();
        converters.add(new FormHttpMessageConverter()); // request
        converters.add(new OAuth2AccessTokenResponseHttpMessageConverter()); // response
    })
    .defaultStatusHandler(new OAuth2ErrorResponseErrorHandler()) // error
    .build();- FormHttpMessageConverter: access token 요청에 사용됨 (x-www-form-urlencoded 전송)
- OAuth2AccessTokenResponseHttpMessageConverter: token 응답을 Java 객체로 변환
- OAuth2ErrorResponseErrorHandler: 오류 응답 처리 (400, 401 등)
커스터마이징) RestClient
더보기
OAuth2AccessTokenResponseHttpMessageConverter accessTokenResponseMessageConverter =
    new OAuth2AccessTokenResponseHttpMessageConverter();
accessTokenResponseMessageConverter.setAccessTokenResponseConverter(parameters -> {
    // 이곳에서 응답 파라미터 직접 가공 가능
    String accessToken = parameters.get("access_token").toString();
    return OAuth2AccessTokenResponse.withToken("custom-" + accessToken)
        .tokenType(OAuth2AccessToken.TokenType.BEARER)
        .expiresIn(3600)
        .build();
});- JSON 파라미터 → OAuth2AccessTokenResponse로 가공하는 Converter를 직접 정의
커스터마이징) RestClient - errorHandler converter
더보기
OAuth2ErrorHttpMessageConverter errorConverter = new OAuth2ErrorHttpMessageConverter();
errorConverter.setErrorConverter(parameters -> {
    return new OAuth2Error(
        "custom_error_code",
        "Custom error description",
        "https://example.com/docs/errors#custom"
    );
});
OAuth2ErrorResponseErrorHandler errorHandler = new OAuth2ErrorResponseErrorHandler();
errorHandler.setErrorConverter(errorConverter);- 인증 서버의 오류를 원하는 객체로 변환하고 싶을 때 사용
Customize using the DSL
설정) OAuth2AccessTokenResponseClient
더보기
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .oauth2Client(oauth2 -> oauth2
            .authorizationCodeGrant(codeGrant -> codeGrant
                .accessTokenResponseClient(this.accessTokenResponseClient())
                // ...
            )
        );
    return http.build();
}
2. Refresh Token
Refreshing an Access Token
RestClientRefreshTokenTokenResponseClient
- 기본 구현체
- reactive 방식의 요청
설정) RestClientRefreshTokenTokenResponseClient
더보기
@Bean
public OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient() {
	return new RestClientRefreshTokenTokenResponseClient();
}
Customizing the Access Token Request
커스터마이징) 헤더 추가
더보기
accessTokenResponseClient.addHeadersConverter(grantRequest -> {
    ClientRegistration clientRegistration = grantRequest.getClientRegistration();
    HttpHeaders headers = new HttpHeaders();
    if (clientRegistration.getRegistrationId().equals("spring")) {
        headers.set(HttpHeaders.USER_AGENT, "my-user-agent"); // 사용자 정의 User-Agent 추가
    }
    return headers;
});
커스터마이징) 헤더 재정의
더보기
DefaultOAuth2TokenRequestHeadersConverter headersConverter =
	new DefaultOAuth2TokenRequestHeadersConverter();
headersConverter.setEncodeClientCredentials(false); // 기본 헤더에서 client 자격증명 form으로 전송
accessTokenResponseClient.setHeadersConverter(headersConverter);
커스터마이징) 파라미터 추가
더보기
accessTokenResponseClient.addParametersConverter(grantRequest -> {
    ClientRegistration clientRegistration = grantRequest.getClientRegistration();
    MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
    if (clientRegistration.getRegistrationId().equals("keycloak")) {
        parameters.set(OAuth2ParameterNames.AUDIENCE, "my-audience");
    }
    return parameters;
});
커스터마이징) 파라미터 오버라이드
더보기
accessTokenResponseClient.setParametersConverter(grantRequest -> {
    ClientRegistration clientRegistration = grantRequest.getClientRegistration();
    LinkedMultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
    if (clientRegistration.getRegistrationId().equals("okta")) {
        parameters.set(OAuth2ParameterNames.CLIENT_ID, "my-client");
    }
    return parameters;
});
커스터마이징) 파라미터 재정의
더보기
accessTokenResponseClient.setParametersCustomizer(parameters -> {
    if (parameters.containsKey(OAuth2ParameterNames.CLIENT_ASSERTION)) {
        parameters.remove(OAuth2ParameterNames.CLIENT_ID); // client_assertion이 있을 경우 client_id 제거
    }
});
Customizing the Access Token Response
- 요청을 담당하는 restClient를 set하는 방식
- restClient에 사용될 converter를 세팅한 후, RestClientRefreshTokenTokenResponseClient에 설정된 restClient를 셋팅
커스터마이징) 반환 객체
더보기
OAuth2AccessTokenResponseHttpMessageConverter accessTokenResponseMessageConverter =
	new OAuth2AccessTokenResponseHttpMessageConverter();
accessTokenResponseMessageConverter.setAccessTokenResponseConverter(parameters -> {
	// 파라미터 기반으로 직접 응답 객체 구성
	return OAuth2AccessTokenResponse.withToken("custom-token")
		.tokenType(OAuth2AccessToken.TokenType.BEARER)
		.expiresIn(3600)
		.build();
});
커스터마이징) 에러 핸들
더보기
OAuth2ErrorHttpMessageConverter errorConverter = new OAuth2ErrorHttpMessageConverter();
errorConverter.setErrorConverter(parameters -> {
	// 에러 파라미터 기반으로 OAuth2Error 구성
	return new OAuth2Error("custom-error", "custom description", "custom-uri");
});
OAuth2ErrorResponseErrorHandler errorHandler = new OAuth2ErrorResponseErrorHandler();
errorHandler.setErrorConverter(errorConverter);
Customize using the Builder
- OAuth2AccessTokenResponseClient를 Builder 방식으로 구현하는 방법
설정) OAuth2AccessTokenResponseClient
더보기
// 사용자 정의 구현체 (예: 커스터마이징한 RestClientRefreshTokenTokenResponseClient)
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient = ...;
// Provider 구성
OAuth2AuthorizedClientProvider authorizedClientProvider =
	OAuth2AuthorizedClientProviderBuilder.builder()
		.authorizationCode()
		.refreshToken(configurer ->
			configurer.accessTokenResponseClient(refreshTokenTokenResponseClient)) // 여기서 설정
		.build();
// Manager에 설정
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);- OAuth2AuthorizedClientProviderBuilder로 OAuth2AuthorizedClientProvider를 정의함
- OAuth2AuthorizedClientManager에 셋팅하는 방식
6. Token Exchange
- 기존 토큰을 다른 종류의 토큰으로 교환할 수 있게 해주는 OAuth2.0의 확장 기능
Requesting an Access Token
- RestClientTokenExchangeTokenResponseClient를 사용하여 요청함
Using the Access Token
- token-exchange는 관련 provider 객체를 자동으로 등록해주지 않음
- 추가적인 설정 필요
설정) OAuth2AuthorizedClientManager
더보기
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {
    // Token Exchange Provider 구성
    TokenExchangeOAuth2AuthorizedClientProvider tokenExchangeProvider =
        new TokenExchangeOAuth2AuthorizedClientProvider();
    // 필요 시 SubjectTokenResolver 설정 (카카오 access_token 전달)
    tokenExchangeProvider.setSubjectTokenResolver(context -> {
        // 카카오 토큰을 subject_token으로 설정
        OAuth2AuthenticationToken kakaoAuth = (OAuth2AuthenticationToken) context.getPrincipal();
        OAuth2AuthorizedClient kakaoClient = context.getAuthorizedClientRepository()
            .loadAuthorizedClient("kakao", kakaoAuth, context.getRequest());
        return kakaoClient.getAccessToken(); // or getIdToken() depending on your use
    });
    // Provider 빌더로 등록
    OAuth2AuthorizedClientProvider authorizedClientProvider =
        OAuth2AuthorizedClientProviderBuilder.builder()
            .provider(tokenExchangeProvider)
            .build();
    // 매니저 설정
    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
        new DefaultOAuth2AuthorizedClientManager(
            clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
    return authorizedClientManager;
}
출처
'Spring > Spring Security' 카테고리의 다른 글
| [Spring Authorization Server] 1. Configuration Model (0) | 2025.07.20 | 
|---|---|
| [Spring Security] 7-4. OAuth2 Client: Authorized Client Features (0) | 2025.07.20 | 
| [Spring Security] 7-1. OAuth2 Client: Core Interfaces and Classes (0) | 2025.07.18 | 
| [Spring Security] 6-4. OAuth2 Login: OIDC Logout (1) | 2025.07.18 | 
| [Spring Security] 6-3. OAuth2 Login: Advanced Configuration (0) | 2025.07.17 |