들어가는 글

항상 백앤드와 프론트가 한 서버에 존재하는 개발만 해보았고 구글로그인도 그 때 해봤지만 이 번에 하는 프로젝트는 프론트와 백앤드의 서버가 각자 존재한다. 이렇게 프론트와 백앤드서버가 따로 존재할 때는 구글로그인을 프론트에서 다 해주거나 프론트에서 인가코드를 받고 그 코드를 백앤드에 넘겨 이어서 진행하는 방법으로 해야한다.

소셜로그인 흐름

  1. 리액트에서 유저가 구글로그인을 시도하여 인가코드를 요청한다.
  2. 구글에서 응답으로 구글 로그인을 시도한 유저의 인가코드를 같이 준다.
  3. 리액트에서는 받은 인가코드를 서버에 엑세스토큰과 리플레쉬토큰을 응답받기위해 요청헤더에 담아 보낸다.
  4. 인가코드를 받은 서버에서는 구글에게 구글플랫폼에 등록한 redirectUrl, clientId, clientSecret, 인가코드, 설정을 Body에 담아 구글에 엑세스토큰을 요청한다.
  5. 구글해서는 리액트와 동일한 redirectUrl, clientId, clientSecret 그리고 인가코드를 가지고 있는지 확인 후 유저의 토큰을 보내준다.
  6. 구글에서 준 토큰을 구글에게 요청하고 구글은 그 토큰에 맞는 유저를 보내준다.
  7. 서버는 구글에게 받은 유저정보를 받는다.
  8. 서버는 그 유저정보를 db에 저장하고 커스텀 토큰을 리액트에게 준다.

구글 플래폼 설정하는법

 

[Spring Boot] Google 로그인 REST API 로만 구현해보기!(코드, 스샷)

안녕하세요? Spring Boot와 Java를 이용해서 쉽고 간단하게 구글 로그인하는 API를 구현해봤습니다. 여러 블로그 및 공식 문서를 참고했습니다. https://developers.google.com/identity/protocols/oauth2/web-ser..

maivve.tistory.com

 

 

리액트에는 정확하게 어떻게하는지는 모르지만 아래 래퍼런스에서 자바스크립트를 좀 알아보는 걸 추천한다.

 

웹 서버 애플리케이션용 OAuth 2.0 사용  |  Google ID 플랫폼  |  Google Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English 웹 서버 애플리케이션용 OAuth 2.0 사용 이 문서에서는 웹 서버 애플리케이션에서 Google API 클라이언트 라이브러리 또는 Google OAu

developers.google.com

아래 링크는 메모용으로 가져가는 링크 아래 링크는 인가코드를 받는 로직은 없지만 인가코드를 건너 띄고 엑세스토큰부터 가능하다.

 

@react-oauth/google

Google OAuth2 using the new Google Identity Services SDK for React @react-oauth/google

react-oauth.vercel.app

토큰을 받을 때 사용한 로직

  • GoogleLoginType 는 구글과 통신할 때 사용하는 상수를 enum으로 정리했다.
public enum GoogleLoginType {
    GOOGLE_ACCESS_TOKEN_URL("googleAccessTokenUrl", "accounts.google.com"),
    GOOGLE_USER_INFO_URL("googleUserInfoUrl", "www.googleapis.com"),
    GOOGLE_ACCESS_PATH("path", "/o/oauth2/token"),
    GOOGLE_USER_INFO_PATH("path", "/oauth2/v3/tokeninfo"),
    ACCESS_TOKEN("access_token"),
    PROTOCOL("protocol", "https"),
    CLIENT_ID("client_id"),
    CLIENT_SECRET("client_secret"),
    CODE("code"),
    GRANT_TYPE("grant_type", "authorization_code"),
    REDIRECT_URI("redirect_uri", "http://localhost:3000");

    private String name;
    private String value;

    GoogleLoginType(String name, String value) {
        this.name = name;
        this.value = value;
    }

    GoogleLoginType(String name) {
        this.name = name;
    }
}
  • 구글에 엑세스토큰을 받기위해 사용한 로직
code는 리액트에서 받은 인가코드 
clientId와  clientSecret는 구글플랫폼에서 받은 값을 사용하면된다.
    public SocialLoginRequestDto findAccessTokenByCode(String code) {
        HttpHeaders headers = new HttpHeaders();
        ResponseEntity<SocialLoginRequestDto> responseEntity;
        try {
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            UriComponents googleUrl = UriComponentsBuilder.newInstance()
                    .scheme(PROTOCOL.getValue())
                    .host(GOOGLE_ACCESS_TOKEN_URL.getValue())
                    .path(GOOGLE_ACCESS_PATH.getValue())
                    .build(true);

            UriComponents body = UriComponentsBuilder.newInstance()
                    .queryParam(CODE.getName(), code)
                    .queryParam(CLIENT_ID.getName(), clientId)
                    .queryParam(CLIENT_SECRET.getName(), clientSecret)
                    .queryParam(GRANT_TYPE.getName(), GRANT_TYPE.getValue())
                    .queryParam(REDIRECT_URI.getName(), REDIRECT_URI.getValue())
                    .build(true);

            responseEntity = restTemplate.exchange(
                    googleUrl.toString(),
                    HttpMethod.POST,
                    new HttpEntity<String>(body.toString().substring(1), headers),
                    SocialLoginRequestDto.class
            );
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
        return statusCheck(responseEntity);
    }
  • 엑세스토큰을 가지고 유저정보를 구글에게 요청할 때 사용한 로직
public SocialLoginRequestDto googleUserInfoByAccessToken(String accessToken) {
        ResponseEntity<SocialLoginRequestDto> responseEntity;
        try {
            UriComponents googleUrl = UriComponentsBuilder.newInstance()
                    .scheme(PROTOCOL.getValue())
                    .host(GOOGLE_USER_INFO_URL.getValue())
                    .path(GOOGLE_USER_INFO_PATH.getValue())
                    .queryParam(ACCESS_TOKEN.getName(), accessToken)
                    .build(true);

            responseEntity = restTemplate.exchange(
                    googleUrl.toString(),
                    HttpMethod.GET,
                    new HttpEntity<String>("", new HttpHeaders()),
                    SocialLoginRequestDto.class
            );
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
        return statusCheck(responseEntity);
    }

전체로직

  • controller
    @PostMapping("/user/social")
    public TokenResponseDto socialLogin(@RequestHeader(SOCIAL_HEADER_KEY) String code) {
        return userService.findAccessTokenByCode(code);
    }
  • service
@Transactional
public TokenResponseDto findAccessTokenByCode(String code) {
    SocialLoginRequestDto response = socialLoginRestTemplate.findAccessTokenByCode(code);
    SocialLoginRequestDto socialLoginRequestDto = googleUserInfoByAccessToken(response.getAccess_token());

    Users user = userRepository.findByUsername(socialLoginRequestDto.getEmail())
            .orElseGet(() ->
                    userRepository.save(
                            new Users(
                                    socialLoginRequestDto,
                                    userRepository.findAllCount()
                            )
                    )
            );
    return createTokens2(user.getUsername());
}
  • RestTemplate로 통신하는 API
@Component
public class SocialLoginRestTemplate {

    private final RestTemplate restTemplate;
    private final String clientId;
    private final String clientSecret;

    public SocialLoginRestTemplate(RestTemplate restTemplate,
                                   @Value("${client_id}") String clientId,
                                   @Value("${secret_key}") String clientSecret) {
        this.restTemplate = restTemplate;
        this.clientId = clientId;
        this.clientSecret = clientSecret;
    }

    public SocialLoginRequestDto findAccessTokenByCode(String code) {
        HttpHeaders headers = new HttpHeaders();
        ResponseEntity<SocialLoginRequestDto> responseEntity;
        try {
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            UriComponents googleUrl = UriComponentsBuilder.newInstance()
                    .scheme(PROTOCOL.getValue())
                    .host(GOOGLE_ACCESS_TOKEN_URL.getValue())
                    .path(GOOGLE_ACCESS_PATH.getValue())
                    .build(true);

            UriComponents body = UriComponentsBuilder.newInstance()
                    .queryParam(CODE.getName(), code)
                    .queryParam(CLIENT_ID.getName(), clientId)
                    .queryParam(CLIENT_SECRET.getName(), clientSecret)
                    .queryParam(GRANT_TYPE.getName(), GRANT_TYPE.getValue())
                    .queryParam(REDIRECT_URI.getName(), REDIRECT_URI.getValue())
                    .build(true);

            responseEntity = restTemplate.exchange(
                    googleUrl.toString(),
                    HttpMethod.POST,
                    new HttpEntity<String>(body.toString().substring(1), headers),
                    SocialLoginRequestDto.class
            );
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
        return statusCheck(responseEntity);
    }

    public SocialLoginRequestDto googleUserInfoByAccessToken(String accessToken) {
        ResponseEntity<SocialLoginRequestDto> responseEntity;
        try {
            UriComponents googleUrl = UriComponentsBuilder.newInstance()
                    .scheme(PROTOCOL.getValue())
                    .host(GOOGLE_USER_INFO_URL.getValue())
                    .path(GOOGLE_USER_INFO_PATH.getValue())
                    .queryParam(ACCESS_TOKEN.getName(), accessToken)
                    .build(true);

            responseEntity = restTemplate.exchange(
                    googleUrl.toString(),
                    HttpMethod.GET,
                    new HttpEntity<String>("", new HttpHeaders()),
                    SocialLoginRequestDto.class
            );
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
        return statusCheck(responseEntity);
    }

    private SocialLoginRequestDto statusCheck(ResponseEntity<SocialLoginRequestDto> socialLoginRequestDtoResponseEntity){
        int statusCode = socialLoginRequestDtoResponseEntity.getStatusCode().value();

        if (statusCode != 200) {
            throw new RuntimeException("statusCode = " + statusCode);
        }
        return socialLoginRequestDtoResponseEntity.getBody();
    }
}

 

마치는 글 

RestTemplate를 한번 이용해보고 싶었지만 이렇게나마 사용 할 수 있어서 만족했고 최대한 깔끔한 코드를 작성하고 싶었는데 그래도 나름 만족을 하고있다.  시큐리티를 사용해 Oauth2를 이용하는 소셜로그인보다 더 깔끔하고 쉬워서 놀랍다.

복사했습니다!