[웹 개발 프로젝트] 2. 구글 소셜 로그인 구현

adorableco·2024년 1월 5일
0

구글 로그인 구현은 아래의 블로그를 굉장히 많이 참고했습니다!!! 너무너무너무너무 도움이 많이 됐어요 감사합니다...
https://antdev.tistory.com/71



도메인 계층 구조 나타내려고 드디어 tree도 설치하였다! 이번 소셜 로그인을 구현할 때 영향을 받은 도메인은 다음과 같다.

└── com
    └── example
        └── fit_friends
            ├── FitFriendsApplication.java
            ├── config
            │   └── auth
            │       ├── GoogleOAuth.java
            │       ├── SecurityConfig.java
            │       ├── SocialOAuth.java
            │       └── dto
            │           ├── GoogleRequestAccessTokenDto.java
            │           ├── OAuthAttributes.java
            │           └── SessionUser.java
            ├── controller
            │   ├── LoginController.java
            │   └── PostApiController.java

SocialOAuth.java


public interface SocialOAuth {
    String getOAuthRedirectUrl();

    String requestAccessToken(String code);
}

getOAuthRedirectUrl()

  • 구글 로그인 창으로 이동하는 url을 구성하는 메서드

requestAccessToken(String code)

  • 구글 로그인 창에서 로그인 후 받는 code를 이용해 accessToken을 만들 수 있도록하는 url을 구성하는 메서드

GoogleOAuth.java

  • SocialOAuth 의 구현체
@Component
@RequiredArgsConstructor
public class GoogleOAuth implements SocialOAuth{

    @Value("${google.client.id}")
    private String clientId;
    @Value("${google.redirect.uri}")
    private String redirectUri;

    @Value("${google.base.uri}")
    private String googleBaseUri;

    @Value("${google.client.secret}")
    private String clientSecret;

    @Value("${google.token_uri}")
    private String tokenUri;
    @Override
    public String getOAuthRedirectUrl() {
        Map<String,Object> params = new HashMap<>();
        params.put("client_id",clientId);
        params.put("response_type","code");
        params.put("redirect_uri",redirectUri);
        params.put("scope","profile");

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

        return googleBaseUri+"?"+paramsString;
    }

    @Override
    public String requestAccessToken(String code) {

        RestTemplate restTemplate = new RestTemplate();

        Map<String,Object> params = new HashMap<>();
        params.put("code",code);
        params.put("client_id",clientId);
        params.put("grant_type","authorization_code");
        params.put("response_type","code");
        params.put("redirect_uri",redirectUri);
        params.put("scope","profile");
        params.put("client_secret",clientSecret);

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

        if(responseEntity.getStatusCode()== HttpStatus.OK){
            return responseEntity.getBody();
        }

        return "구글 로그인 요청 처리 실패";
    }
}

@Value() 로 가져온 application.properties 의 변수값이 null 로 뜬다면,

  • 해당 클래스를 new 로 가져온건 아닌지 체크
  • @Value() 가 properties의 값을 가져오는건 클래스가 스프링 빈으로 등록되는 순간인데, 다른 클래스에서 new 연산자를 통해 이 클래스를 생성해버리면 스프링 빈으로 등록되지 않기 때문에 @Value() 를 통해 변수에 값이 저장되지도 않는 것이다. ➡️ 이런 실수를 할 때면 분명 스프링으로 개발하고 있는데 스프링의 기능을 제대로 쓰지 않고 있다는게 느껴진다..

getOAuthRedirectUrl()

에서 필요한 request params은 4가지이다.

  • client_id, response_type, redirect_uri,scope
  • Map<> 을 이용해 params를 구성한다. 중요한 정보는 application.properties 등에 따로 빼두고 꺼내 쓰기!!
String paramsString = params.entrySet().stream()
                .map(x->x.getKey()+"="+x.getValue())
                .collect(Collectors.joining("&"));
  • 람다식으로 구성하니 코드도 짧아지고 좋다.....!

Map.entrySet() :Map의 Key-Value 쌍의 모음
Stream() : 데이터 처리 연산을 지원하도록 저장되어 있는 요소들을 추출하여 반복적인 처리를 가능하게 하는 기능


requestAccessToken(String code)

에서 필요한 request params은 6가지이다.

  • code, client_id, grant_type, response_type, redirect_uri, scope, client_secret
  • redirect_uri 는 모두 구글 클라우드에서 미리 입력해준 uri과 일치해야 한다.
restTemplate.postForEntity(String url, Object request, Class responseType)
  • POST 요청을 보내고 결과로 ResponseEntity로 반환
    • url : 요청을 보낼 서버의 url
    • request : POST 요청 시 함께 보낼 객체
      • 여기에서 나는 accessToken을 얻기 위해 필요한 params을 담았다.
    • responseType : 서버의 응답을 어떤 형태로 받을지를 지정하는 Class 객체
  • POST 요청 시에 헤더값도 추가해야한다면?
    + HttpEntity 를 생성하여 원래 보내야하는 객체와, HttpHeaders 객체를 함께 담는다.
    ✅ 이외에도 서버의 응답을 객체로 반환하는 PostforObject 나 GET 요청을 수행하는 getForEntity 등 다양하게 존재한다.

LoginController.java

public class LoginController{

    @Autowired
    private final SocialOAuth socialOAuth;
    private final HttpServletResponse response;

    @GetMapping("/api/login")
    public void loginGoogle() throws Exception{

        response.sendRedirect(socialOAuth.getOAuthRedirectUrl());
    }

    @GetMapping("/login/oauth2/code/google")
    public String requestToken(@RequestParam(name="code") String code) throws Exception{
        return socialOAuth.requestAccessToken(code);
    }
  • 사실 처음에 가장 헤맸던 부분이다.. 로그인 요청을 하고 리디렉트되면 그걸 다시 GetMapping 하고 그 다음엔 어떻게 해야하지..? 라는 생각에서 막혀서 진짜 한참을 헤맸다. 계속 controller의 한 메서드 내에서 code 받은걸 다시 accessToken 을 받기 위해 주소를 리디렉트하고.. 복잡하게 생각했는데 그냥 메서드를 각각 다 나누면 생각하기도 훨씬 쉽다!

끝인줄 알았는데 생각해보니 accessToken 을 이용해서 유저 정보까지 로드해야했다.! 킵고잉.

token을 얻는 것을 끝으로 하는 LoginControllerrequestToken 을 수정하여 유저 정보까지 로드할 수 있도록 한다.

    @GetMapping("/login/oauth2/code/google")
    public String requestUserInfo(@RequestParam(name="code") String code) throws Exception{
        final String accessToken =  socialOAuth.requestAccessToken(code);

        return socialOAuth.getUserInfo(accessToken);
    }

메서드 이름도 requestUserInfo 로 변경하였다.
socialOAuth.requestAccessToken(code) 값을 리턴하지 않고 SocialOAuthgetUserInfo 에 담아 유저 정보 로드를 할 수 있도록
한다.


`GoogleOAuth` (SocialOAuth의 구현체) 에도 유저 정보 로드 메서드를 추가한다.
    @Override
    public String getUserInfo(String authorizationCode) {

        RestTemplate restTemplate = new RestTemplate();

        Map<String,Object> params = new HashMap<>();
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization","Bearer "+ authorizationCode);

        HttpEntity<String> request = new HttpEntity<>(headers);


        ResponseEntity<String> responseEntity = restTemplate.exchange(userInfo_uri, HttpMethod.GET,request,String.class);

        if(responseEntity.getStatusCode() == HttpStatus.OK){
            return responseEntity.getBody();
        }

        return "구글 사용자 정보 로드 실패";

    }

이전과 마찬가지로 RestTemplate 을 이용한다. 한가지 차이는 유저 정보 로드 시에는 헤더 값에 Authorization 필드가 필요하므로 HttpHeaders 클래스에 헤더를 추가하고 이걸 HttpEntity 에 담아서 RestTemplate 로 전송하는 방식을 취한다.

✅ GET method 는 헤더를 담지 않는데..?

그래서 RestTemplategetforEntity 메서드를 사용할 수 없고 exchange 메서드를 이용한다.

➡️ exchange : 모든 메서드로 사용 가능하고 (ex. GET, POST, PUT...) Http 헤더를 새로 만들 수 있다.

  • 모든 메서드로 사용이 가능하기 때문에 전달 인자로 메서드 종류를 명시해주어야한다. (ex. HttpMethod.GET)

다음 할 일

  • 구글 로그인을 기반으로 하도록 User 도메인 수정
  • User 도메인과 관계가 있는 여러 도메인 더러 수정(...)
  • 데이터베이스도 수정해야 하지 않을까.
profile
👩🏻‍💻

0개의 댓글