Spring으로 구글 로그인 구현하기

Jiwoo Yi·2021년 3월 26일
0
post-thumbnail

이 글을 작성하는 이유

다른 블로그들의 글을 보면서 구글 로그인을 구현하다가 403 Forbidden 이라는 문제에 직면했다..

Connection<Google> connection = googleConnectionFactory.createConnection(accessGrant);

위 부분에서 발생하는 문제 같았다. 하지만 아무리 찾아봐도 Google+ API를 활성화하라는 답변 밖에 없었는데, 나는 이미 Google+ API가 활성화 되어 있는 상태였다. 혹시 오류일까봐 몇 번이나 껐다 켰다 해봤지만 증상은 여전했다.

발생했던 오류

WARN : org.springframework.web.client.RestTemplate - GET request for "https://www.googleapis.com/plus/v1/people/me" resulted in 403 (Forbidden); invoking error handler
심각: 경로 []의 컨텍스트 내의 서블릿 [appServlet]을(를) 위한 Servlet.service() 호출이, 근본 원인(root cause)과 함께, 예외 [Request processing failed; nested exception is org.springframework.web.client.HttpClientErrorException: 403 Forbidden]을(를) 발생시켰습니다.

그렇게 열심히 뒤적거리다가 Google+ API 서비스가 종료되었다는 걸 알았다.. 이게 문제였던 것이다...

그래서 어쩔 수 없이 블로그 글이 아닌 공식 문서를 보며 로그인을 구현했다. (제발 공식 문서 보는 습관을 좀 길러야겠다.)

1. 프로젝트 생성 및 사용자 인증 정보 생성

https://console.cloud.google.com/home/dashboard?hl=ko

먼저 위 페이지에서 프로젝트를 생성하고 사용자 인증 정보를 생성해야 한다.

사이드바의 API 및 라이브러리 - OAuth 동의 화면 에 들어간다.

여기서 OAuth 클라이언트 ID를 누르고 애플리케이션 유형을 웹 애플리케이션으로 설정해준다.
User type은 외부로 한다. 승인된 자바스크립트 원본은 아직 배포할 것이 아니므로 localhost로 맞춰주었고, 승인된 리디렉션 URI에는 구글 로그인을 실행할 컨트롤러에 매핑될 URI를 적어주었다. (포트 설정이 따로 되어있지 않다면 localhost 뒤에 :8080을 붙여야 한다.)

이 작업이 완료되면 클라이언트 ID가 생성되는데, 이를 따로 기록해둔다. 구글 로그인을 구현할 때 사용될 예정이다!

2. 구글 로그인 구현하기

참고한 문서

https://developers.google.com/identity/sign-in/web/sign-in?hl=ko

login.html (jsp)

빨간 네모박스에 아까 저장한 클라이언트 ID를 넣고 저 두 줄을 login.html에 추가해준다.
(나는 메인에서 Modal을 띄우는 방식을 쓰면서 jsp 대신 html을 썼고, 이 때문에 밑의 구현 과정에서도 코드가 기존 방식과 약간 달라졌다.)
이것을 추가하면 구글 로그인 버튼이 생성된다.
이제 로그인 폼을 열고 정보를 보내야 하는데, 여기서 방식이 나뉜다.

🌟 기존 방식

function onSignIn(googleUser) {
	  var profile = googleUser.getBasicProfile();
	  var id_token = googleUser.getAuthResponse().id_token;
	  var xhr = new XMLHttpRequest();
	  xhr.open('POST', '승인된 리디렉션 URI');
	  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
	  xhr.onload = function() {
	    console.log('Signed in as: ' + xhr.responseText);
	  };
	  xhr.send('idtoken=' + id_token);
}//onSignIn

🌟 Modal 방식

function onSignIn(googleUser) {
	  var profile = googleUser.getBasicProfile();
	  var id_token = googleUser.getAuthResponse().id_token;
	  $("#googleBtn").click(function(){
		  $.ajax({
			  url: '승인된 리디렉션 URI',
			  type: 'POST',
			  data: 'idtoken=' + id_token, 
			  dataType: 'JSON',
			  beforeSend : function(xhr){
				  xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
			  },
			  success: function(json) {
				  if (json.login_result == "success"){
					  location.href = "로그인 완료 후 이동할 main 주소";
				  }//end if
	          }//success
		  });//ajax
	  });//click

아까 말했듯이 나는 Modal을 사용했기 때문에 ajax로 다시 값을 받아와야 main 페이지로 이동할 수 있었다. (다른 방법이 있을지도 모르지만 이게 내가 생각해낼 수 있는 최선이었다..)
#googleBtn 은 구글 로그인 버튼에 준 ID인데, 이게 없으면 로그인 폼을 열자마자 버튼을 누르지 않아도 구글 로그인이 되어버린다. 그래서 버튼 클릭 시에만 구글 로그인이 동작되도록 했다.

Controller

Modal 방식 기준

@RequestMapping(value="member/google_login", method=POST)
@ResponseBody
public String googleLogin(String idtoken, Model model) throws GeneralSecurityException, IOException {
	HttpTransport transport = Utils.getDefaultTransport();
	JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
	
	GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory)
			.setAudience(Collections.singletonList("클라이언트 ID")).build();
	
	JSONObject json = new JSONObject();
	
	GoogleIdToken idToken = verifier.verify(idtoken);
	if (idToken != null) {
		Payload payload = idToken.getPayload();
		
		if (dupId((String) payload.get("email")).contains("false")) { //회원가입이 안 되어 있는 경우
			SocialJoinVO sjVO = new SocialJoinVO();
			sjVO.setId((String) payload.get("email"));
			sjVO.setAuth_email((String) payload.get("email"));
			sjVO.setNickname((String) payload.get("given_name"));
			sjVO.setBlog_name((String) payload.get("given_name"));
			sjVO.setProfile_img((String) payload.get("picture"));
			sjVO.setPlatform("google");
			sjVO.setAccess_token(idtoken);
			
			new MemberService().googleJoin(sjVO);
		}//end if
			
		model.addAttribute("id", (String) payload.get("email"));
		json.put("login_result", "success");
			
	} else { //유효하지 않은 토큰
		json.put("login_result", "fail");
	}//end else
		
	return json.toJSONString();
    
}//googleLogin

공식 문서에 나온 내용을 나에게 맞게 조금 수정했다. Modal 방식이라 값을 ajax로 다시 보내야 하기 때문에 @ResponseBody를 사용했고, 리턴 값도 json으로 했다.

모달을 사용하지 않는다면, JSONObject을 삭제하고, @ResponseBody를 제거한 뒤 return에는 로그인 후 이동할 페이지 주소를 적어주면 된다.

dupId(payload.getSubject()).contains("false") 는 회원가입이 되어 있지 않은 경우에 한해 로그인과 동시에 회원가입을 진행하기 위해 나눈 분기점이다. 내가 미리 구현했던 아이디 중복을 확인하는 메소드인 dupId의 결과가 JSON을 String으로 변환한 것이기 때문에 .contains()를 사용했다. 이는 각자 맞게 수정하면 될 듯 하다.

로그인만 구현한다면 컨트롤러 작성으로 충분하고, 회원가입까지 진행한다면 이를 위한 service, DAO, mapper도 작성해주어야 한다. VO에 저장할 값은 payload.get() 으로 가져왔는데, 제공하는 것은 다음과 같다. (JavaScript API지만 제공하는 내용은 똑같은 것 같다!)

이렇게 구글 로그인 및 회원가입이 완성되었다.

profile
Backend Developer

1개의 댓글

comment-user-thumbnail
2021년 7월 14일

많이 도움됐습니다 감사합니다

답글 달기