우아한테크코스 레벨3 미션인 팀 프로젝트 미션에서 구현한 구글 OAuth 기능이다.

https://github.com/woowacourse-teams/2022-momo

1. 사용자에게 구글 로그인 URL 제공

com/woowacourse/momo/auth/controller/OauthController.java

package com.woowacourse.momo.auth.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;

import com.woowacourse.momo.auth.service.OauthService;
import com.woowacourse.momo.auth.service.dto.response.LoginResponse;
import com.woowacourse.momo.auth.service.dto.response.OauthLinkResponse;

@RequiredArgsConstructor
@RequestMapping("/api/auth/oauth2/google")
@RestController
public class OauthController {

    private final OauthService oauthService;

    @GetMapping(value = "/login", params = {"redirectUrl"})
    public ResponseEntity<OauthLinkResponse> access(@RequestParam String redirectUrl) {
        OauthLinkResponse response = oauthService.generateAuthUrl(redirectUrl);
        return ResponseEntity.ok(response);
    }
}

구글 OAuth 를 구현하기 위한 controller 이다. 첫번째 메서드는 웹 사이트를 이용하는 사용자가 구글 OAuth 를 이용하여 로그인하고 싶다는 것을 알리는 메서드이다. 구글 로그인 페이지에 접속할 수 있도록 URL 을 반환한다.

com/woowacourse/momo/auth/service/OauthService.java

package com.woowacourse.momo.auth.service;

import static com.woowacourse.momo.global.exception.exception.ErrorCode.OAUTH_USERINFO_REQUEST_FAILED_BY_NON_2XX_STATUS;
import static com.woowacourse.momo.global.exception.exception.ErrorCode.OAUTH_USERINFO_REQUEST_FAILED_BY_NON_EXIST_BODY;

import java.util.Optional;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;

import com.woowacourse.momo.auth.service.dto.response.LoginResponse;
import com.woowacourse.momo.auth.service.dto.response.OauthLinkResponse;
import com.woowacourse.momo.auth.support.JwtTokenProvider;
import com.woowacourse.momo.auth.support.PasswordEncoder;
import com.woowacourse.momo.auth.support.google.GoogleConnector;
import com.woowacourse.momo.auth.support.google.GoogleProvider;
import com.woowacourse.momo.auth.support.google.dto.GoogleUserResponse;
import com.woowacourse.momo.global.exception.exception.MomoException;
import com.woowacourse.momo.member.domain.Member;
import com.woowacourse.momo.member.domain.MemberRepository;

@RequiredArgsConstructor
@Service
public class OauthService {

    private final TokenService tokenService;
    private final MemberRepository memberRepository;
    private final JwtTokenProvider jwtTokenProvider;
    private final PasswordEncoder passwordEncoder;
    private final GoogleConnector oauthConnector;
    private final GoogleProvider oauthProvider;

    public OauthLinkResponse generateAuthUrl(String redirectUrl) {
        String oauthLink = oauthProvider.generateAuthUrl(redirectUrl);
        return new OauthLinkResponse(oauthLink);
    }
}

서비스 계층에서는 단순하게 반환된 값을 전달하는 역할만을 한다.

com/woowacourse/momo/auth/support/google/GoogleProvider.java

package com.woowacourse.momo.auth.support.google;

import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import lombok.Getter;

@Getter
@Component
public class GoogleProvider {

    private final String clientId;
    private final String clientSecret;
    private final String authUrl;
    private final String accessTokenUrl;
    private final String userInfoUrl;
    private final String grantType;
    private final String scope;
    private final String temporaryPassword;

    public GoogleProvider(@Value("${oauth2.google.client.id}") String clientId,
                          @Value("${oauth2.google.client.secret}") String clientSecret,
                          @Value("${oauth2.google.url.auth}") String authUrl,
                          @Value("${oauth2.google.url.token}") String accessTokenUrl,
                          @Value("${oauth2.google.url.userinfo}") String userInfoUrl,
                          @Value("${oauth2.google.grant-type}") String grantType,
                          @Value("${oauth2.google.scope}") String scope,
                          @Value("${oauth2.member.temporary-password}") String temporaryPassword) {
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.authUrl = authUrl;
        this.accessTokenUrl = accessTokenUrl;
        this.userInfoUrl = userInfoUrl;
        this.grantType = grantType;
        this.scope = scope;
        this.temporaryPassword = temporaryPassword;
    }

    public String generateAuthUrl(String redirectUrl) {
        Map<String, String> params = new HashMap<>();
        params.put("scope", scope);
        params.put("response_type", "code");
        params.put("client_id", clientId);
        params.put("redirect_uri", redirectUrl);
        return authUrl + "?" + concatParams(params);
    }

    private String concatParams(Map<String, String> params) {
        return params.entrySet()
                .stream()
                .map(entry -> entry.getKey() + "=" + entry.getValue())
                .collect(Collectors.joining("&"));
    }
}

GoogleProvider 객체에서는 미리 등록해둔 정보를 이용하여 사용자가 구글 로그인을 하기 위한 URL 을 만든다. 사용자는 이 URL 을 전달받아 화면에 표출하여 구글 로그인 화면에 접속할 수 있다.

resources/security/application-oauth2.yml

oauth2:
  redirect-path: /auth/google
  google:
    client:
      id: 755121207871-raacm7vn0v386ep86q22kl4s35cvg0io.apps.googleusercontent.com
      secret: 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
    url:
      auth: <https://accounts.google.com/o/oauth2/auth>
      token: <https://oauth2.googleapis.com/token>
      userinfo: <https://www.googleapis.com/oauth2/v2/userinfo>
    grant-type: authorization_code
    scope: openid%20https://www.googleapis.com/auth/userinfo.email%20https://www.googleapis.com/auth/userinfo.profile
  member.temporary-password: 4ey6d756ft7gye5g7a86

해당 정보는 외부에 노출되면 안되는 민감한 정보이므로 서브모듈을 이용하여 관리하고 있다.

2. 구글 Access Token 발급

com/woowacourse/momo/auth/controller/OauthController.java

package com.woowacourse.momo.auth.controller;

@RequiredArgsConstructor
@RequestMapping("/api/auth/oauth2/google")
@RestController
public class OauthController {

    private final OauthService oauthService;

    @GetMapping(value = "/login", params = {"code", "redirectUrl"})
    public ResponseEntity<LoginResponse> login(@RequestParam String code, @RequestParam String redirectUrl) {
        LoginResponse loginResponse = oauthService.requestAccessToken(code, redirectUrl);
        return ResponseEntity.ok(loginResponse);
    }
}

code 에는 Authorization Code 을 입력받는다.

com/woowacourse/momo/auth/service/OauthService.java