본문 바로가기

Programming/구현

[Spring Security] User Info Request, Response 커스터마이징 - Tistory OAuth2 로그인 구현하기(2)

Access Token Request, Response 커스터마이징을 통해 로그인 인증은 완료되었지만, Tistory에서 Access Token만 전달해주기 때문에 지정할 수 있는 유일한 Type인 Bearer로 임시로 지정해 두었다.

하지만 Spring Security에서 제공하는 user-info를 얻어오는 작업을 하게되면 지정했던 Bearer타입을 붙인 채 Access Token을 요청과 함께 전달하기 때문에 이부분도 커스텀이 필요해졌다.

 

 

먼저 SecurityConfig에서 설정을 해야한다.

@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    private final CustomOAuth2UserService customOAuth2UserService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.oauth2Login()
                ...
                .userInfoEndpoint(userInfo -> userInfo 
                        .userService(customOAuth2UserService)
                )
        ;
    }
    ...

userInfoEndpoint는 OAuth2 로그인 성공 이후 사용자 정보를 가져올 때 설정을 담당한다. 이 때, OAuth2UserService를 통해 userInfo를 얻어오는 요청을 보내게 되는데 이를 커스텀하면 된다.

 

 

기본으로 사용하는 DefaultOAuth2UserService를 보면, OAuth2UserRequestRequestEntity로 변경하는 OAuth2UserRequestEntityConverter를 사용하는데 이를 변경해주면 유저정보를 얻어오는 요청을 커스텀할 수 있게 된다.

 

 

public class CustomOAuth2UserRequestEntityConverter implements Converter<OAuth2UserRequest, RequestEntity<?>> {

    @Override
    public RequestEntity<?> convert(OAuth2UserRequest userRequest) {
        ClientRegistration clientRegistration = userRequest.getClientRegistration();
        HttpMethod httpMethod = getHttpMethod(clientRegistration);
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        UriComponentsBuilder uriBuilder = UriComponentsBuilder
                .fromUriString(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri());

        RequestEntity<?> request;
        if (HttpMethod.POST.equals(httpMethod)) {
            headers.setContentType(DEFAULT_CONTENT_TYPE);
            MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
            formParameters.add(OAuth2ParameterNames.ACCESS_TOKEN, userRequest.getAccessToken().getTokenValue());
            request = new RequestEntity<>(formParameters, headers, httpMethod, uriBuilder.build().toUri());
        } else {
            URI uri = uriBuilder.queryParam(OAuth2ParameterNames.ACCESS_TOKEN, userRequest.getAccessToken().getTokenValue())
                    .queryParam("output", "json").build().toUri();
            request = new RequestEntity<>(headers, httpMethod, uri);
        }

        return request;
    }
 	...

Tistory API는 userInfo요청을 GET으로 받아 처리한다. 이때 "output"을 지정하지 않으면 xml로 응답이 오기때문에 "json"으로 지정해주었고, "bearer"를 뗀 Access Token도 함께 첨부해 주었다.

 

 

요청만 변경하면 되면 DefaultOAuth2UserService에서 public 으로 제공하는 setRequestEntityConverter() 메소드를 통해 Converter를 생성하고, 변경할 수 있다.

 

 

{
  "tistory": {
    "status": "200",
    "item": {
      "id": "blog_oauth_test@daum.net",
      "userId": "12345",
      "blogs": [
        ...
      ]
    }
  }
}

하지만, Tistory는 응답도 "Tistory"로 감싸 주기 때문에 이를 다시 커스텀하기 위해 이를 상속받은 CustomOAuth2UserService를 작성하고 필요한 부분을 변경해주어야 했다.

 

@Service
public class CustomOAuth2UserService extends OAuth2UserService {

    ...

    private RestOperations restOperations;

    private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new CustomOAuth2UserRequestEntityConverter();

    public CustomOAuth2UserService() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
        this.restOperations = restTemplate;
    }

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        ...
        RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
        ResponseEntity<Map<String, Object>> response = getResponse(userRequest, request);
        Map<String, Object> userAttributes = response.getBody();

        // RegistrationId가 tistory일 때 내용 item 얻어오기
        if(userRequest.getClientRegistration().getRegistrationId().equals("tistory")){
            userAttributes = (Map<String, Object>) ((Map<String, Object>) userAttributes.get("tistory")).get("item");
        }

        Set<GrantedAuthority> authorities = new LinkedHashSet<>();
        authorities.add(new OAuth2UserAuthority(userAttributes));
        OAuth2AccessToken token = userRequest.getAccessToken();
        for (String authority : token.getScopes()) {
            authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
        }
        return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
    }

    private ResponseEntity<Map<String, Object>> getResponse(OAuth2UserRequest userRequest, RequestEntity<?> request){
        ...
    }
}

 

기본적인 구현체 DefaultOAuth2UserService를 참조하고 OAuth2UserService 인터페이스를 확장해 작성하였다.

먼저, 위에서 작성한 Converter를 사용하기 위해 requestEntityConverter에 지정해 주었다.

이후 loadUser메소드에서 응답받은 객체에서 유저정보를 얻기 위해 불필요한 정보들을 제거하는 작업을 더해주었다.

필요한 정보를 담고있는 부분은 tistory.item 안에 있는 정보들만 필요했기 때문에 나머지를 모두 버리고 item만 userAttributes로 재할당해주었다.

 

이렇게하면 성공적으로 Tistory 로그인에서부터 유저 정보를 얻어오는 작업까지 완료되게 된다....

 

 

 


📚 Reference

Spring Security Reference Docs

https://tistory.github.io/document-tistory-apis/apis/v1/blog/list.html