코딩항해기

[API] 구글 로그인 API (구글 OAuth 2.0 웹 로그인) 본문

기타/API

[API] 구글 로그인 API (구글 OAuth 2.0 웹 로그인)

miniBcake 2024. 9. 11. 12:31

 

 

구글 로그인을 구현해보자.

먼저 구글 로그인은 최근 업데이트로 인해 기존 소스들을 사용하기 어려우므로 최신 방법을 잘 찾아야한다.

 

해당 글은 24년 9월 기준으로 작성됐다.

이번 글에서는 구글 로그인을 통해 회원정보를 받아오는 방식에 대해 정리할 예정이다.

 

준비하기

먼저 구글 OAuth 2.0 프로젝트를 만들어줘야한다.

구글 클라우드 플랫폼에서 프로젝트를 생성할 수 있다.

https://console.cloud.google.com

 

Google 클라우드 플랫폼

로그인 Google 클라우드 플랫폼으로 이동

accounts.google.com

 

클라우드 플랫폼에 들어가서 로그인을 하면 상단에 프로젝트를 고를 수 있는 부분이 있다.

이 부분을 클릭하면 프로젝트 리스트가 나오며 새 프로젝트를 만들 수 있다.

 

새 프로젝트 만들기를 선택하면 프로젝트 명을 설정할 수 있는 페이지가 뜬다.

프로젝트에 맞춰 이름을 작성하고 만들기를 누르면 된다. 

 

잠시간의 대기 시간 후 프로젝트 생성이 완료되면, 다시 상단에서 만든 프로젝트를 선택할 수 있다.

 

프로젝트를 선택한 후 OAuth 동의 화면으로 이동해 외부를 선택한 후 만들기를 누른다.

 

 

프로젝트 앱 정보를 입력할 수 있는 창으로 넘어가는데, 어떤 화면에서 어떻게 뜨는 정보인지는 구글 설명에서 이미지로 보여주고있다. 연습 진행할 예정이기 때문에, *(별표) 표시된 필수 데이터만 작성했다.

앱 이름, 사용자 지원 이메일, 이메일 주소 3가지만 입력하면 된다. 입력 완료한 뒤에는 하단의 저장 후 계속을 누른다.

 

 

다음 페이지에서는 받아올 정보의 범위를 정하는 페이지가 나온다.

민감한 정보를 받아올수록 인증이 까다로워지므로, 가장 최소한의 정보를 선택했다. 선택 한 후에는 저장 후 계속을 누른다.

 

 

다음은 테스트 계정 추가 페이지이다. 테스트 할 구글 계정을 등록한다.

 

 

 

요약에서 입력한 정보가 맞는지 확인 한 뒤 완료하면 프로젝트 설정은 거의 마무리 됐다.

이제 사용자 인증 정보로 이동한다.

 

 

구글 로그인 api를 사용하기 위해서는 OAuth 클라이언트 ID가 필요하므로 상단의 사용자 인증 정보 만들기를 눌러 OAuth 클라이언트 ID를 선택한다.

 

 

 

해당 정보들을 입력한 후 저장을 눌러 클라이언트 ID를 생성하면되는데, 이 때 주의할 점은 승인된 JavaScript 원본에 URI를 작성할 때 포트 번호를 적으면 정상적으로 작동하지 않으므로 localhost까지만 작성해야한다.

이 주소가 잘못되면 팝업창이 뜰 때 빈 팝업만 뜨게 된다.

 

클라이언트 ID가 생성 완료되면 클라이언트 ID를 다음에 사용할 예정이므로 복사하거나 기록해둔다.

언제든 사용자 인증 정보에서 다시 확인할 수 있다.

 

 

HTML 버튼 태그 만들기

구글에서는 여러 버튼 모양을 제공하고 있는데, 이 버튼 HTML도 제공하고 있다.

https://developers.google.com/identity/gsi/web/tools/configurator?hl=ko

 

HTML 코드 생성  |  Authentication  |  Google for Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 의견 보내기 HTML 코드 생성 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 의견 보내기 달리 명시되지

developers.google.com

 

 

 

여기에 아까 복사해두었던 클라이언트 ID를 입력하고, 따로 자바스크립트에서 콜백함수를 사용하는 방식으로 진행할 예정이기 때문에 자바스크립트 콜백으로 전환을 눌러준다.

 

 

콜백함수는 handleCredentialResponse를 사용할 예정이므로 이 단계에서 미리 입력해놔도 좋고, 따로 태그를 넣은 뒤 변경해도 된다.

 

 

원탭을 사용할지 Google 계정으로 로그인 버튼을 사용 설정할지 결정하고, 지금은 Google 계정으로 로그인 버튼 사용으로 진행할 예정이다. 선택하면 디자인을 고를 수 있는 부분이 나타나고 원하는 디자인을 고른 뒤 코드받기를 눌러 해당 코드를 HTML에서 원하는 위치에 넣으면 된다.

 

 

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>구글 로그인</title>
    <script src="https://accounts.google.com/gsi/client" async defer></script>
</head>
<body>
	<div id="g_id_onload"
	     data-client_id="클라이언트 ID 입력하는 곳"
	     data-context="signup"
	     data-ux_mode="popup"
	     data-callback="handleCredentialResponse"
	     data-auto_prompt="false">
	</div>
	
	<div class="g_id_signin"
	     data-type="standard"
	     data-shape="pill"
	     data-theme="outline"
	     data-text="signin_with"
	     data-size="large"
	     data-logo_alignment="left">
	</div>
</body>
</html>

 

해당 버튼의 모양과 기능을 google에서 받아올 수 있도록 script 태그를 상단에 추가한다.

 

<script src="https://accounts.google.com/gsi/client" async defer></script>

 

 

외부 스크립트로 작성할 예정이므로 작성할 외부 스크립트 태그도 넣어준다.

(js 폴더 내에 있는 loginGoogleAPI.js 파일을 연결한 태그)

 

<script type="text/javascript" src="js/loginGoogleAPI.js" defer></script>

 

 

 

자바스크립트 작성

이제 html 준비는 끝났다. 아까 미리 작성해둔 위치에 js파일을 만든다. defer 설정을 했으므로 window.onload는 생략할 수 있다.

먼저 콜백함수를 작성해줄 예정인데, 전역 함수가 되어야하므로 window를 붙여준다. 전역함수가 아니라면 호출되지 않는다.

 

//전역 함수로 설정
window.handleCredentialResponse = function (response) {
	console.log('handleCredentialResponse 호출');
	//토큰 값을 디코딩해서 JSON으로 반환
	//decodeJwtResponse <- 디코딩하는 함수
	const responsePayload = decodeJwtResponse(response.credential);
	
	//디코딩한 정보를 콘솔창에 출력
	console.log('Full Name: ' + responsePayload.name);
	console.log('Email: ' + responsePayload.email);
	
	//값 전송
	sendforwardGooglelogin(responsePayload.name, responsePayload.email)
}

 

response.credential를 통해 토큰 값을 받아와 디코딩을 통해 원하는 정보를 JSON으로 만들어 사용할 수 있다.

기본적으로 활용 가능한 정보는 id, Full Name, Given Name, Family Name, Image URL, Email 정도가 있다.

추가로 정보를 받아올 수 있도록 설정하면 추가할 수 있다.

해당 handleCredentialResponse 함수 기본 형식은 구글에서 제공하고 있다.

 

https://developers.google.com/identity/gsi/web/guides/handle-credential-responses-js-functions?hl=ko

 

자바스크립트 함수로 사용자 인증 정보 응답 처리  |  Authentication  |  Google for Developers

Google ID 서비스가 FedCM API로 마이그레이션됩니다. 이전 가이드에 따라 잠재적인 변경사항을 검토하고 웹사이트에 대한 사용자 로그인에 부정적인 영향을 미치지 않도록 하세요. 이 페이지는 Cloud

developers.google.com

(구글 기본 제공 handleCredentialResponse )

<script>
  function handleCredentialResponse(response) {
     // decodeJwtResponse() is a custom function defined by you
     // to decode the credential response.
     const responsePayload = decodeJwtResponse(response.credential);

     console.log("ID: " + responsePayload.sub);
     console.log('Full Name: ' + responsePayload.name);
     console.log('Given Name: ' + responsePayload.given_name);
     console.log('Family Name: ' + responsePayload.family_name);
     console.log("Image URL: " + responsePayload.picture);
     console.log("Email: " + responsePayload.email);
  }
</script>

 

 

여기서 decodeJwtResponse 함수는 따로 직접 디코딩해야하는 함수이다. 디코딩 방식은 따로 검색해서 작성했으며, 최종적으로 JSON 타입으로 변경되어 반환된다.

 

//디코딩 함수
function decodeJwtResponse(id_token) {
	console.log('decodeJwtResponse 호출');
	//받아온 토큰 값을 디코딩하여 정보 전송
	//id_token을 '.'으로 나누어 중간에 있는 payload 부분(base64Url)을 추출
	const base64Url = id_token.split('.')[1];
	//URL-safe Base64 형식에서 표준 Base64 형식으로 변환 ('-' -> '+', '_' -> '/')
	const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
	//Base64로 인코딩된 문자열을 디코딩하고 각 문자의 유니코드 값을 %인코딩된 형식으로 변환한 후, 이를 다시 문자열로 조합하여 JSON 형식의 payload로 만듦
	const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
		//각 문자의 유니코드 값을 %XX 형식으로 변환
		return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
	}).join('')); //변환된 값을 하나의 문자열로 조합
	//최종적으로 JSON 타입으로 변환해 반환
	return JSON.parse(jsonPayload);
}

 

 

디코딩 함수까지 작성하고나면 console에서 정상적으로 값이 찍히는 것을 확인할 수 있다. 이제 이 정보를 Controller로 전달해주면 된다.

 

//받아온 값을 보내는 함수
function sendforwardGooglelogin(fullname, email) {
    //새로운 폼 요소 생성합니다.
    let googleloginForm = document.createElement("form");
	googleloginForm.style.display = "none";
    googleloginForm.method = "POST"; // POST 요청 방식
    googleloginForm.action = "login.do"; // 요청을 보낼 URL

    //이름 값을 보낼 input태그 만들기
    let nameField = document.createElement("input");
    nameField.type = "hidden"; // 폼에 표시되지 않도록 숨김 필드로 설정
    nameField.name = "name"; // 서버에서 받을 변수 이름
    nameField.value = fullname; // 보낼 데이터

    //이메일값을 보낼 input태그 만들기
    let emailField = document.createElement("input");
    emailField.type = "hidden";
    emailField.name = "email";
    emailField.value = email;
	
    //구글 로그인임을 알려줄 input태그 만들기
    let typeField = document.createElement("input");
    typeField.type = "hidden";
    typeField.name = "type";
    typeField.value = "googleLogin";

	//만든 input태그를 form에 추가합니다.
    googleloginForm.appendChild(nameField);
    googleloginForm.appendChild(emailField);
    googleloginForm.appendChild(typeField);
    //폼을 현재 페이지에 추가한 후 전송합니다.
    document.body.appendChild(googleloginForm);
    googleloginForm.submit(); 
}

 

여러 방법이 존재하겠지만, 별도의 form을 만들어 input 태그에 값을 담아 전송하는 방식을 선택했다.

 

이 정보를 바로 페이지에서 확인하기 위해 계정 정보가 페이지에 뜨는 방식으로 다시 만들어 봤는데, 로그인에 성공하면 하면에 기본 정보들이 바로 뜨는 것을 확인할 수 있다.

 

(JSP 파일)

더보기
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>구글 로그인</title>
    <script src="https://accounts.google.com/gsi/client" async defer></script>
</head>
<body>
	<div id="g_id_onload"
	     data-client_id="527110201666-pq2c94j9632upcejh4d5s3psr95o1v27.apps.googleusercontent.com"
	     data-context="signup"
	     data-ux_mode="popup"
	     data-callback="handleCredentialResponse"
	     data-auto_prompt="false">
	</div>
	
	<div class="g_id_signin"
	     data-type="standard"
	     data-shape="pill"
	     data-theme="outline"
	     data-text="signin_with"
	     data-size="large"
	     data-logo_alignment="left">
	</div>
	
    <div id="user-info">
	    <p id="user-id"></p>
	    <p id="user-name"></p>
	    <p id="user-given-name"></p>
	    <p id="user-family-name"></p>
	    <p id="user-picture"></p>
	    <p id="user-email"></p>
	</div>

    <script>
        function handleCredentialResponse(response) {
            const id_token = response.credential;
            console.log("ID Token: " + id_token);
            
            const responsePayload = decodeJwtResponse(id_token);

            console.log('ID: ' + responsePayload.sub);
            console.log('Full Name: ' + responsePayload.name);
            console.log('Given Name: ' + responsePayload.given_name);
            console.log('Family Name: ' + responsePayload.family_name);
            console.log('Image URL: ' + responsePayload.picture);
            console.log('Email: ' + responsePayload.email);
            
         // 화면에 정보 표시
            document.getElementById('user-id').textContent = "ID: " + responsePayload.sub;
            document.getElementById('user-name').textContent = "Full Name: " + responsePayload.name;
            document.getElementById('user-given-name').textContent = "Given Name: " + responsePayload.given_name;
            document.getElementById('user-family-name').textContent = "Family Name: " + responsePayload.family_name;
            document.getElementById('user-picture').innerHTML = "Image: <img src='" + responsePayload.picture + "' alt='Profile Picture' />";
            document.getElementById('user-email').textContent = "Email: " + responsePayload.email;
            
        }
        
      //디코딩 함수
        function decodeJwtResponse(id_token) {
        	console.log('decodeJwtResponse 호출');
        	//받아온 토큰 값을 디코딩하여 정보를 가져옵니다.
        	//id_token을 '.'으로 나누어 중간에 있는 payload 부분(base64Url)을 추출합니다.
        	const base64Url = id_token.split('.')[1];
        	//URL-safe Base64 형식에서 표준 Base64 형식으로 변환합니다. ('-' -> '+', '_' -> '/')
        	const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
        	//Base64로 인코딩된 문자열을 디코딩하고 각 문자의 유니코드 값을 %인코딩된 형식으로 변환한 후, 이를 다시 문자열로 조합하여 JSON 형식의 payload를 만듭니다.
        	const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
        		//각 문자의 유니코드 값을 %XX 형식으로 변환합니다.
        		return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
        	}).join('')); //변환된 값을 하나의 문자열로 조합합니다.
        	//최종적으로 JSON 타입으로 변환해 반환합니다.
        	return JSON.parse(jsonPayload);
        }
    </script>
</body>
</html>