본문 바로가기
OAuth/Google Login API

[Google Login API] 구글 로그인 Access Token 및 Refresh Token 발급 - 마무리 (Spring Boot 레퍼런스를 보면서 구현해보는 구글 소셜 로그인 REST API - 5)

by 임채훈 2020. 10. 18.

Spring Boot 환경에서 구글 소셜 로그인 API를 REST 방식으로 구현하기

이전글

2020/10/18 - [OAuth/Google Login API] - [Google Login API] 소셜 로그인 요청 Redirect 처리 - 2 (Spring Boot 레퍼런스를 보면서 구현해보는 구글 소셜 로그인 REST API - 4)

 

[Google Login API] 소셜 로그인 요청 Redirect 처리 - 2 (Spring Boot 레퍼런스를 보면서 구현해보는 구글 소

Spring Boot 환경에서 구글 소셜 로그인 API를 REST 방식으로 구현하기 Step Google APIs 신규 프로젝트 생성 및 개발환경 구성 새 프로젝트 생성 OAuth 동의 화면 구성 API Key 생성 OAuth 클라이언트 ID 생성(사

antdev.tistory.com

 

Step

  1. Google APIs 신규 프로젝트 생성 및 개발환경 구성
    1. 새 프로젝트 생성
    2. OAuth 동의 화면 구성
    3. API Key 생성
    4. OAuth 클라이언트 ID 생성(사용자 인증 정보 생성)
  2. 개발환경 구성 및 Spring Boot 프로젝트 생성
    1. 개발환경
    2. Spring Boot 프로젝트 생성
  3. 소셜 로그인 요청 Redirect 처리 (로그인 최초 요청 처리)
  4. 소셜 로그인 요청 Redirect 처리 - 2
  5. 구글 로그인 Access Token 및 Refresh Token 발급


5. 구글 로그인 Access Token 및 Refresh Token 발급 요청

우선에 앞장에서 Google Login 요청을 했을때 Google API Server에서 code값을 넘겨주며 Redirect되는 Callback URL을 Controller에서 매핑처리하여 code 값을 받도록 Controller 소스를 수정하겠습니다.

 

1. OauthController에 callback mapping method 추가

@RestController
@CrossOrigin
@RequiredArgsConstructor
@RequestMapping(value = "/auth")
@Slf4j
public class OauthController {
    private final OauthService oauthService;

    /**
     * 사용자로부터 SNS 로그인 요청을 Social Login Type 을 받아 처리
     * @param socialLoginType (GOOGLE, FACEBOOK, NAVER, KAKAO)
     */
    @GetMapping(value = "/{socialLoginType}")
    public void socialLoginType(
            @PathVariable(name = "socialLoginType") SocialLoginType socialLoginType) {
        log.info(">> 사용자로부터 SNS 로그인 요청을 받음 :: {} Social Login", socialLoginType);
        oauthService.request(socialLoginType);
    }

	/**
     * Social Login API Server 요청에 의한 callback 을 처리
     * @param socialLoginType (GOOGLE, FACEBOOK, NAVER, KAKAO)
     * @param code API Server 로부터 넘어노는 code
     * @return SNS Login 요청 결과로 받은 Json 형태의 String 문자열 (access_token, refresh_token 등)
     */
    @GetMapping(value = "/{socialLoginType}/callback")
    public String callback(
            @PathVariable(name = "socialLoginType") SocialLoginType socialLoginType,
            @RequestParam(name = "code") String code) {
        log.info(">> 소셜 로그인 API 서버로부터 받은 code :: {}", code);
        return "";
    }
}

callback 메소드 추가 이후 다시 앞장에서 테스트했던 URL인 http://localhost:8080/auth/google 로 요청을 해보면 로그상에 아래와 같이 코드를 성공적으로 받을수 있음을 확인할 수 있습니다.

이제부터 받아온 code를 활용하여 Access Token 및 Refresh Token을 발급하도록 소스를 추가하도록 합니다.

 

2. SocialLogin 공통 interface에 accessToken요청 메소드 추가

public interface SocialOauth {
    /**
     * 각 Social Login 페이지로 Redirect 처리할 URL Build
     * 사용자로부터 로그인 요청을 받아 Social Login Server 인증용 code 요
     */
    String getOauthRedirectURL();

    /**
     * API Server로부터 받은 code를 활용하여 사용자 인증 정보 요청
     * @param code API Server 에서 받아온 code
     * @return API 서버로 부터 응답받은 Json 형태의 결과를 string으로 반
     */
    String requestAccessToken(String code);
}

이 후 각 하위 클래스들(GoogleOauth, FacebookOauth, KakaoOauth 등)에도 implements 해줍니다.

 

3. OauthService 클래스에 access token 요청 메소드 추가

@Service
@RequiredArgsConstructor
public class OauthService {
    private final FacebookOauth facebookOauth;
    private final GoogleOauth googleOauth;
    private final KakaoOauth kakaoOauth;
    private final NaverOauth naverOauth;
    private final HttpServletResponse response;

    public void request(SocialLoginType socialLoginType) {
        String redirectURL;
        switch (socialLoginType) {
            case GOOGLE: {
                redirectURL = googleOauth.getOauthRedirectURL();
            } break;
            case FACEBOOK: {
                redirectURL = facebookOauth.getOauthRedirectURL();
            } break;
            case KAKAO: {
                redirectURL = kakaoOauth.getOauthRedirectURL();
            } break;
            case NAVER: {
                redirectURL = naverOauth.getOauthRedirectURL();
            } break;
            default: {
                throw new IllegalArgumentException("알 수 없는 소셜 로그인 형식입니다.");
            }
        }
        try {
            response.sendRedirect(redirectURL);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String requestAccessToken(SocialLoginType socialLoginType, String code) {
        switch (socialLoginType) {
            case GOOGLE: {
                return googleOauth.requestAccessToken(code);
            }
            case FACEBOOK: {
                return facebookOauth.requestAccessToken(code);
            }
            case KAKAO: {
                return kakaoOauth.requestAccessToken(code);
            }
            case NAVER: {
                return naverOauth.requestAccessToken(code);
            }
            default: {
                throw new IllegalArgumentException("알 수 없는 소셜 로그인 형식입니다.");
            }
        }
    }
}

여기서 잠깐 .. Service 코드의 리팩토링이 절실히 필요해보입니다..

두개의 메소드에서 동일한 switch 구문이 존재하여 코드가 장황해집니다.

 

4. SocialOauth 인터페이스에 타입 구분 default method 추가

public interface SocialOauth {
    /**
     * 각 Social Login 페이지로 Redirect 처리할 URL Build
     * 사용자로부터 로그인 요청을 받아 Social Login Server 인증용 code 요
     */
    String getOauthRedirectURL();

    /**
     * API Server로부터 받은 code를 활용하여 사용자 인증 정보 요청
     * @param code API Server 에서 받아온 code
     * @return API 서버로 부터 응답받은 Json 형태의 결과를 string으로 반
     */
    String requestAccessToken(String code);

    default SocialLoginType type() {
        if (this instanceof FacebookOauth) {
            return SocialLoginType.FACEBOOK;
        } else if (this instanceof GoogleOauth) {
            return SocialLoginType.GOOGLE;
        } else if (this instanceof NaverOauth) {
            return SocialLoginType.NAVER;
        } else if (this instanceof KakaoOauth) {
            return SocialLoginType.KAKAO;
        } else {
            return null;
        }
    }
}

 

5. OauthService 클래스 소스 전체 수정

@Service
@RequiredArgsConstructor
public class OauthService {
    private final List<SocialOauth> socialOauthList;
    private final HttpServletResponse response;

    public void request(SocialLoginType socialLoginType) {
        SocialOauth socialOauth = this.findSocialOauthByType(socialLoginType);
        String redirectURL = socialOauth.getOauthRedirectURL();
        try {
            response.sendRedirect(redirectURL);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String requestAccessToken(SocialLoginType socialLoginType, String code) {
        SocialOauth socialOauth = this.findSocialOauthByType(socialLoginType);
        return socialOauth.requestAccessToken(code);
    }

    private SocialOauth findSocialOauthByType(SocialLoginType socialLoginType) {
        return socialOauthList.stream()
                .filter(x -> x.type() == socialLoginType)
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("알 수 없는 SocialLoginType 입니다."));
    }
}

먼저 Field에서 @RequiredArgsConstructor를 통해 SocialOauth 타입의 객체들이 List 형태로 Injection 되도록 필드를 List 타입으로 수정한 후

그다음 SocialLoginType에 맞는 SocialOauth 객체를 반환하는 findSocialOauthByType 메소드 생성하여 각 request, requestAccessToken 메소드에서 SocialLoginType에 맞는 SocialOauth 클래스를 findSocialOauthByType 함수를 통해 초기화되도록 수정합니다.

 

6. OauthController에 추가한 callback method 내용 수정

@RestController
@CrossOrigin
@RequiredArgsConstructor
@RequestMapping(value = "/auth")
@Slf4j
public class OauthController {
    private final OauthService oauthService;
    
    @GetMapping(value = "/{socialLoginType}")
    public void socialLoginType(
            @PathVariable(name = "socialLoginType") SocialLoginType socialLoginType) { ... }

	/**
     * Social Login API Server 요청에 의한 callback 을 처리
     * @param socialLoginType (GOOGLE, FACEBOOK, NAVER, KAKAO)
     * @param code API Server 로부터 넘어노는 code
     * @return SNS Login 요청 결과로 받은 Json 형태의 String 문자열 (access_token, refresh_token 등)
     */
    @GetMapping(value = "/{socialLoginType}/callback")
    public String callback(
            @PathVariable(name = "socialLoginType") SocialLoginType socialLoginType,
            @RequestParam(name = "code") String code) {
        log.info(">> 소셜 로그인 API 서버로부터 받은 code :: {}", code);
        return oauthService.requestAccessToken(socialLoginType, code);
    }
}

 

이제 실제 requestAccessToken 메소드에서 처리될 AccessToken 요청 로직을 구성하기 위하여 Google Reference를 참고하도록 합니다.

  • Google APIs Reference
 

Using OAuth 2.0 for Web Server Applications  |  Google ID 플랫폼

This document explains how web server applications use Google API Client Libraries or Google OAuth 2.0 endpoints to implement OAuth 2.0 authorization to access Google APIs. OAuth 2.0 allows users to share specific data with an application while keeping the

developers.google.com

  • Reference - Step 5. Exchange authorization code for refresh and access tokens

앞서 확인한 redirect 부분과 동일하게 필수 파라미터 설명이 나와있고 추가로 HTTP 요청 양식도 Sample로 안내해줍니다.

요청 URL은 https://oauth2.googleapis.com/token 이고 

client_id는 Google APIs 프로젝트를 생성하면서 받은 client_id

client_secret 또한 client_id와 함께 받은 secret

grant_typeauthorization_code로 넣어주면 되고

redirect_uri은 앞서 사용한 callback URL

code는 redirect callback 처리로 받은 code의 값을 넘겨주면 되는것 같다.

또한 요청이 성공적으로 처리되었을때의 응답 메세지 Sample도 각 필드의 설명과 함께 나타나있습니다.

그럼 확인한 레퍼런스 내용을 바탕으로 소스로 돌아가 실제로 처리되도록 로직을 구성해보도록 합니다.

 

7. GoogleOauth 클래스의 requestAccessToken 메소드 구현

@Component
@RequiredArgsConstructor
public class GoogleOauth implements SocialOauth {
    private final String GOOGLE_SNS_BASE_URL = "https://accounts.google.com/o/oauth2/v2/auth";
    private final String GOOGLE_SNS_CLIENT_ID = "792142333254-9hr0movghp81k68jdgrmu2bpbraerf88.apps.googleusercontent.com";
    private final String GOOGLE_SNS_CALLBACK_URL = "http://localhost:8080/auth/google/callback";
    private final String GOOGLE_SNS_CLIENT_SECRET = "4furtWRSntSGuKosjmdXInjt";
    private final String GOOGLE_SNS_TOKEN_BASE_URL = "https://oauth2.googleapis.com/token";

    @Override
    public String getOauthRedirectURL() { ... }

    @Override
    public String requestAccessToken(String code) {
    	RestTemplate restTemplate = new RestTemplate();

        Map<String, Object> params = new HashMap<>();
        params.put("code", code);
        params.put("client_id", GOOGLE_SNS_CLIENT_ID);
        params.put("client_secret", GOOGLE_SNS_CLIENT_SECRET);
        params.put("redirect_uri", GOOGLE_SNS_CALLBACK_URL);
        params.put("grant_type", "authorization_code");

        ResponseEntity<String> responseEntity =
                restTemplate.postForEntity(GOOGLE_SNS_TOKEN_BASE_URL, params, String.class);

        if (responseEntity.getStatusCode() == HttpStatus.OK) {
            return responseEntity.getBody();
        }
        return "구글 로그인 요청 처리 실패";
    }
}
  • Java 표준 URL 통신 방식(Normal HTTP Request)

@Component
@RequiredArgsConstructor
public class GoogleOauth implements SocialOauth {
    private final String GOOGLE_SNS_BASE_URL = "https://accounts.google.com/o/oauth2/v2/auth";
    private final String GOOGLE_SNS_CLIENT_ID = "792142333254-9hr0movghp81k68jdgrmu2bpbraerf88.apps.googleusercontent.com";
    private final String GOOGLE_SNS_CALLBACK_URL = "http://localhost:8080/auth/google/callback";
    private final String GOOGLE_SNS_CLIENT_SECRET = "4furtWRSntSGuKosjmdXInjt";
    private final String GOOGLE_SNS_TOKEN_BASE_URL = "https://oauth2.googleapis.com/token";

    @Override
    public String getOauthRedirectURL() { ... }

    @Override
    public String requestAccessToken(String code) {
        try {
            URL url = new URL(GOOGLE_SNS_TOKEN_BASE_URL);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
            conn.setDoOutput(true);

            Map<String, Object> params = new HashMap<>();
            params.put("code", code);
            params.put("client_id", GOOGLE_SNS_CLIENT_ID);
            params.put("client_secret", GOOGLE_SNS_CLIENT_SECRET);
            params.put("redirect_uri", GOOGLE_SNS_CALLBACK_URL);
            params.put("grant_type", "authorization_code");

            String parameterString = params.entrySet().stream()
                    .map(x -> x.getKey() + "=" + x.getValue())
                    .collect(Collectors.joining("&"));

            BufferedOutputStream bous = new BufferedOutputStream(conn.getOutputStream());
            bous.write(parameterString.getBytes());
            bous.flush();
            bous.close();

            BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));

            StringBuilder sb = new StringBuilder();
            String line;

            while ((line = br.readLine()) != null) {
                sb.append(line);
            }

            if (conn.getResponseCode() == 200) {
                return sb.toString();
            }
            return "구글 로그인 요청 처리 실패";
        } catch (IOException e) {
            throw new IllegalArgumentException("알 수 없는 구글 로그인 Access Token 요청 URL 입니다 :: " + GOOGLE_SNS_TOKEN_BASE_URL);
        }
    }
}

 

이제 테스트를 해봅니다.

자신의 브라우저에서 http://localhost:8080/auth/google 로 요청을 했을때

위와 같이 Json 형태의 데이터가 등장하면 성공적으로 Access Token 및 Refresh Token이 발급된것입니다.

이제 이후에 자신이 구축중인 시스템 정책에 맞게 해당 access token 및 refresh token 값을 사용자 고유 Social Oauth Key 값으로 사용하여 입맛에 맞게 처리하면 됩니다.

 

여기까지 

구글 로그인 Access Token 및 Refresh Token 발급Spring Boot 레퍼런스를 보면서 구현해보는 구글 소셜 로그인 REST API

시리즈 포스팅은 마무리되었습니다.


혹시나 오류가 발생하거나 질문할 사항이 있으시면 댓글 및 카카오톡 1:1 오픈채팅을 통해서 문의해주시면 친절하게 알려드리겠습니다~!

 

지나가던 백엔드 개발자

 

open.kakao.com

전체 소스코드는 개인 Github에 올려두었습니다.

 

youspend8/google-login-api-sample-blog

Contribute to youspend8/google-login-api-sample-blog development by creating an account on GitHub.

github.com

 

댓글