티스토리 뷰

IT/(경험)문제해결

[Spring Security/문제해결] 구글 OAuth 로그인, loadUser가 호출되지 않았던 이유

주인장 진빼이

스프링 시큐리티 구글 소셜 로그인을 작업하고 있었는데, 자꾸 loadUser가 호출되지 않았다.

관련하여 문제 해결 시도 방법과 관련하여 공유하고자 한다.

 

문제의 발생

구글 소셜 로그인이 성공적으로 진행되면 DefaultOAuth2UserService를 상속받은

CustomOauth2UserService 내 loadUser함수가 실행되어야 토큰, 로그인, 유저 처리가 가능하다.

왜인지 모르겠으나, 구글 OAuth 소셜 로그인을 성공적으로 진행했으나, loadUser 메소드는 호출되지 않았다.

검색키워드1: loadUser 호출 안됨

해결시도1 (실패)

커스터마이징을 처음으로 시도해서 그랬던걸까?, redirect_uri 변경 시도 및 원상복구

구글 OAuth Client 등록정보 전면 수정 및 재등록, 기본 OAuth 관련 URI로 변경 시도 등

정말 사소한 변경은 모두 해본 것 같았다. 대략 4시간 가량 삽질을 했고, 구글링을 했지만 결국 문제를 찾지 못했다.

아직까지도 SuccesssHandler만 호출 될 뿐 CustomOAuth2UserService의 loadUser 메소드는 실행되지 않았다.

 

해결시도2 (실패)

그저 loadUser메소드는 호출되지 않고 원인이 제대로 파악되지 않아 결국 문제 해결하는 방향보다

loadUser 메소드가 호출되지 않는 근본적인 이유를 찾기 위해 정상으로 작동하는 프로젝트를 찾아 실행했다.

 

해당 프로젝트의 정보를 기존 프로젝트 정보와 동일하게 변경해보았고, 어떻게 바꾸었든,

시큐리티를 어떻게 설정했든 기존 프로젝트는 loadUser가 호출되지 않았고,

정상 프로젝트에선 시큐리티 관련 정보의 변경을 여러번 했어도 loadUser메소드가 호출되었다.

 

약 6시간이 지난 것 같다. 오랜만에 이렇게 오랫동안 삽질해보긴 처음이었다.

이곳 저곳에 질문도 던져봤지만 찾은 답은 없었다.

 

해결시도3 (호출 스택 및 디버거 사용, 원인 분석1)

문제의 원인을 찾기 위해 인터넷에 공유되어 있는 정상코드, 정상으로 작동하는 프로젝트에 기대었다.

작업하고 있는 기존 프로젝트는 킵해두고,

정상으로 작동하는 프로젝트에 디버거를 이용하여 loadUser 관련 호출 스택을 살펴보았다.

 

[중단점1: CustomOAuth2UserService]

[호출 스택: OAuth2LoginAuthenticationProvider(최상단)]

loadUser 메소드가 호출되기 직전에 OAuthLoginAuthenticationProvider 클래스 내에

authenticate 메소드가 호출되는 것을 확인할 수 있었다.

 

authenticate 메소드를 중점으로 아주 여러단계에 걸쳐 중단점을 찍었다.

결국 중단점이 걸려 확인해본 바, 최상단 조건문 걸려 실행 흐름이 종료되는 것을 파악할 수 있었다.

if (!authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationRequest().getScopes().contains("openid")) {
    return null; // 이로 인해 다음 loadUser가 호출되지 않음
} else {
   // 코드 생략
}

오호라? 자세히보면 openid가 scopes에 포함되어 있는 것을 볼 수 있었고, 그리하여 최상단 조건문에 적합하여

null 반환 코드가 실행되어 객체 반환과 동시에 실행 흐름이 종료되어 loadUser가 호출되지 않는 것을 알 수 있었다.

해결시도4 (실패)

scope는 application.yml regstrationID 관련해서 등록할 떄에도 보았고, 구글 OAuth 등록화면에서도 볼 수 있었고,

application.yml, 구글 OAuth 동의화면 관련 설정에서 scope관련 변경을 시도 했으나

문제는 해결되지 않았고 아직까지도 loadUser 메소드가 호출되지 않는 문제는 계속되었다.

 

원인을 파악하지 못하고 SecurityConfig에서 URI를 수정하고 application.yml을 변경하고,

로그인하고, 서버 재시작하고를 반복하다보니 갑자기 눈에 헛것이 들어왔나..

구글 OAuth 로그인 endpoint로 접속 시 자동으로 redirect 되는 url에 관심을 가지게 되었다.

뜻 밖에 URL Prameter 차이 발견 

두 URL의 차이가 &scope=에서 서로 다름을 확인할 수 있었다.

[authorizationEndPoint 접속 시 다음과 같은 URL로 리다이렉트 됨]
[loadUser 메소드가 정상 호출됨]
https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?response_type=code&client_id=826963357824-er9rh820fefnmpsbus8spaa5t1uht190.apps.googleusercontent.com
&scope=email%20profile
&state=0qLq5geTDQJGPK4DD94iDpWrdOf4Zt9amWL24QSZChg%3D&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Foauth2%2Fcallback%2Fgoogle&service=lso&o2v=2&flowName=GeneralOAuthFlow
[authorizationEndPoint 접속 시 다음과 같은 URL로 리다이렉트 됨]
[loadUser 메소드가 호출되지 않음]
https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?response_type=code&client_id=826963357824-er9rh820fefnmpsbus8spaa5t1uht190.apps.googleusercontent.com
&scope=openid%20profile%20email
&state=wFUR9UzdDxVBQzZLsePXlLjE5zmsixfWnJRoir3Mxqk%3D
&redirect_uri=http%3A%2F%2Flocalhost%3A8082%2Flogin%2Foauth2%2Fcode%2Fgoogle
&nonce=cjMqjMGhQ7muGzaLaywaVQh9_ARkqnH3BKHF0VN_P_s&service=lso&o2v=2&flowName=GeneralOAuthFlow

 

아까도 그렇고 지금도 그렇고..

결국 원인은 clientRegstration이 멤버 scopes에 openid 관련 데이터가 없어야만

loadUser 메소드가 정상 호출된다는 결론에 도달했다.

7시간 8시간이 지나갈 쯤 지치기 시작했다.

 

정상작동 프로젝트와 기존프로젝트 비교

결국 loadUser 메소드가 호출되지 않는 기존 프로젝트의 시큐리티 설정관련 코드 삭제 후,

정상으로 작동하는 프로젝트의 코드를 완전히 덮었고 한줄 한줄 차이점을 살펴보았다.

유일하게 아래 블록에 적힌 코드가 기존 프로젝트와 정상 프로젝트를 비교했을 때 차이가 있었다.

찾았다 요놈 (문제파악 완료, 해결법)

직접 만든 코드가 아니라서 모든 내용을 알 수 없었지만 ClientRegistrationRepository가 Bean에 등록되었을 때

위에 중단점 찍었던 authentication 인스턴스 변수 내 멤버변수 clientRegistration가 가지고 있는

scopes 속성에 opeid가 포함되는 것을 확인할 수 있었다.

 

아래 코드처럼 ClientRegistrationRepository가 Bean에 등록되어 loadUser가 호출되지 않았던 것이다.

(참고: InMemoryClientRegistrationRepository는 ClientRegistrationRepository 인터페이스의 구현체이다)

@Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        List<ClientRegistration> registrations = clients.stream()
                .map(c -> getRegistration(c))
                .filter(registration -> registration != null)
                .collect(Collectors.toList());

        return new InMemoryClientRegistrationRepository(registrations);
    }

    private static String CLIENT_PROPERTY_KEY
            = "spring.security.oauth2.client.registration.";

    @Autowired
    private Environment env;

    private ClientRegistration getRegistration(String client) {
        String clientId = env.getProperty(
                CLIENT_PROPERTY_KEY + client + ".client-id");

        if (clientId == null) {
            return null;
        }

        String clientSecret = env.getProperty(
                CLIENT_PROPERTY_KEY + client + ".client-secret");

        if (client.equals("google")) {
            return CommonOAuth2Provider.GOOGLE.getBuilder(client)
                    .clientId(clientId).clientSecret(clientSecret).build();
        }
        if (client.equals("facebook")) {
            return CommonOAuth2Provider.FACEBOOK.getBuilder(client)
                    .clientId(clientId).clientSecret(clientSecret).build();
        }
        return null;
    }

해당 코드를 주석 처리 후 테스트를 진행해보니 loadUser 메소드가 정상 호출됨을 확인했다.

 

어디서 끼어들어온 scopes에 존재하는 openid일까?

왜? 인지 원인을 분석해보자.

가장 중요한 것은 loadUser 메소드가 호출되지 않았던 이유는

OAuth Regstration scopes에 openid가 포함되어 있는 것이었고,

ClientRegistrationRepository를 Bean 객체에 등록하는 코드를 보면

List<String> stream()을 통해 getRegistration을 실행하는 것을 볼 수 있었다.

getRegistration 코드를 자세히 보면 CommonOAuth2Provider Builder를 사용하여 Provider 정보를 등록한다.


private static String CLIENT_PROPERTY_KEY = "spring.security.oauth2.client.registration.";
String clientId = env.getProperty(CLIENT_PROPERTY_KEY + client + ".client-id");
String clientSecret = env.getProperty(CLIENT_PROPERTY_KEY + client + ".client-secret");

if (client.equals("google")) {
    return CommonOAuth2Provider.GOOGLE.getBuilder(client)
            .clientId(clientId).clientSecret(clientSecret).build();
}

 

CommonOAuth2Provider는 OAuth Provider 공통 정보를 생성해주는 빌더인데

살펴보면 사진과 같이 builder.scope("openid") 를 확인할 수 있었다.

이로 인해 OAuth 구글 소셜 로그인 진행 시, authentication 인스턴스 내 clientRegistration 객체 내 socpes 에 openid가 포함되어 있는 것이었다.

 

해결방법은 무척 간단하다. CommonOAuth2Provider Builder를 이용하지 않고

appliation.yml에 순수한 Provider 정보를 등록하면 된다. (테스트는 성공적이었다)

결과 및 되돌아보기

여러 시간을 걸쳐 수정을 엄청 많이 했지만, 결국 헛삽질, 헛방이었다.

EndPoint.BaseUri는 문제가 없었고, 클라이언트 키, 구글 OAuth 서버 새로고침 관련 문제도 아니었다.

그저 코드를 이해하지 못하고 사용해서 발생한 일이다.

삽질(시작)은 장대했으나, 결말은 허무했다.

 

스프링 구조가 크다보니 사소한 클래스를 넘겨 뛰게 되는 것 같다. 앞으로 Bean 객체는 다시보고 또 보도록 하자.

Google 관련 OAuth2를 로그인하는데 있어 ClientRegistration 정보를 CommonOAuth2Provider Builder 사용하여

Bean 객체로 등록하는 부분은 지양하고 application.yml로 간단히 등록하자. 

 

ClientRegstration이 등록됨 + scopes에 openid가 포함되었을 때에는 어떤 방법으로 로그인을 처리하는지 알아보기도 해야할 것 같다.

(많은 예제들이 loadUser 메소드를 이용하여 처리한다)

 

아직 모든 스프링 시큐리티 구조를 파악하진 못했으나, 알면 알 수록 재미는 있네 요놈 ~

 

 

도움 채택

여러 분들이 도움을 주셨지만

정확히 방향을 집어주셔서 문제 해결에 도달했습니다 감사합니다.

 

추가 정보 참고 링크:

https://velog.io/@max9106/OAuth3

 

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함