[Spring] Google 로그인 API 사용 + Google Search Console로 사이트 인증 + 프로젝트에 적용하기

yunSeok·2023년 11월 23일
0

사이드 프로젝트

목록 보기
2/14

프로젝트 진행중 로그인 부분에서 현재 카카오와 네이버 로그인은 구현했지만 구글 로그인 API는 조금 다른부분이 있는거 같아 기록해보겠습니다!!


흐름 자체는 다른 소셜 로그인과 비슷합니다.

  1. 클라이언트에서 구글 로그인 요청

  2. Redirect URL로 인가 코드 전달

  3. 토큰 요청과 획득

  4. 토큰을 이용해 사용자 정보 조회

순서로 진행됩니다.


1. Google 로그인 API 연동

1-1. OAuth 동의 화면 설정

💨 Google Cloud Platform 접속

카카오 개발자 센터, 네이버 개발자 센터같은거라고 생각하면될거같아요.

https://console.cloud.google.com/

위 사이트에 접속하면 위와 같은 화면이 나오는데요, 먼저 프로젝트를 생성해줘야하기 때문에 위에 빨간색 박스있는 곳을 클릭해주겠습니다.


오른쪽 위에 새프로젝트 클릭!


원하시는 이름으로 프로젝트 이름을 작성해주고 만들기 눌러주세요.


생성한 프로젝트를 클릭해서 접속해줄게요.


메인 화면에서 API 및 서비스 클릭해주세요.


OAuth 동의 화면을 설정해야합니다.

왼쪽 메뉴에서 OAuth consent screen 에 들어가주고
User Type은 외부로 설정하고 만들기 눌러주세요!


(1). OAuth 동의 화면

앱 이름,
사용자 지원 이메일,
개발자 연락처 정보 작성해주세요.

입력 후 저장 해주세요!
(도메인 및 홈페이지 정보는 현재 단계에서 작성하지 않아도 됩니다.)


(2). 범위

범위는 사용자의 API 요청 정보의 범위를 설정합니다.

범위 추가 또는 삭제 클릭!


일반적인 로그인만 구현하는거면 맨 위에 email만 선택해도 상관없습니다!!

선택하고 아래 업데이트 눌러주세요.


(3). 테스트 사용자

+ ADD USER 눌러서 본인의 계정 추가해주세요.

OAuth 동의 화면 설정은 끝났습니다.


1-2. OAuth 클라이언트 설정

왼쪽 메뉴에 사용자 인증 정보 -> 사용자 인증 정보 만들기 -> OAuth 클라이언트 ID 차례로 클릭해주세요.


애플리케이션 유형은 웹 애플리케이션으로 설정하겠습니다. 웹 프로젝트니까요..!

승인된 자바스크립트 원본에는 사용중인 프로젝트 URI 주소 입력하고

아래는 프로젝트에서 설정한 리다이렉트 주소 입력해주세요!!


생성된 클라이언트에 들어가보면


프로젝트에서 사용할 클라이언트 ID와 클라이언트 보안 비밀번호를 확인할 수 있습니다!!


2. 프로젝트 구현

프로젝트 내에서 카카오, 네이버, 구글 로그인을 구현했지만,
사실 개발자센터에서 클라이언트 ID와 클라이언트 보안 비밀번호를 발급받고 요청하는 URI에 대한 컨트롤러와 컴포넌트를 작성해주는 과정은 거의 같기 때문에 대표적으로 (구글 로그인이 조금 어렵다는 의견이 많더라구요..) 구글 로그인을 포스팅해보도록 하겠습니다!!!

2-1. jsp

<div class="social-login-buttons">
	<img class="naver-login-img" alt="네이버로그인" src="<c:url value="/assets/images/naver_login.png"/>" 
		  onclick="location.href='<c:url value="/naver/login"/>';" role="button">
	<img class="kakao-login-img" alt="카카오로그인" src="<c:url value="/assets/images/kakao_login.png"/>"
	      onclick="location.href='<c:url value="/kakao/login"/>';" role="button">
	<img class="google-login-img" alt="구글로그인" src="<c:url value="/assets/images/google_login.png"/>"
	      onclick="location.href='<c:url value="/google/login"/>';" role="button">
</div>

화면

로그인 모달창 아래에 각각 소셜로그인 이미지를 추가하고
여기에 location.href 설정을 해주었습니다.


2-2. Component

security 패키지를 생성하고 패키지 안에 GoogleLoginBean과 GoogleLoginApi 클래스를 생성하도록 하겠습니다.

(1). GoogleLoginApi

package com.project.security;

import com.github.scribejava.core.builder.api.DefaultApi20;

// DefaultApi20 클래스 : 로그인 관련 API 정보를 제공
public class GoogleLoginApi extends DefaultApi20 {
	
	protected GoogleLoginApi() {

	}

	private static class InstanceHolder {
		private static final GoogleLoginApi INSTANCE = new GoogleLoginApi();
	}

	public static GoogleLoginApi instance() {
		return InstanceHolder.INSTANCE;
	}

	// 사용자 접근 토큰을 제공받기 위한 API의 URL 주소를 반환
	@Override
	public String getAccessTokenEndpoint() {
		return "https://accounts.google.com/o/oauth2/token";
	}
	
	// 로그인 처리를 위한 API의 URL 주소를 반환
	@Override
	protected String getAuthorizationBaseUrl() {
		return "https://accounts.google.com/o/oauth2/v2/auth";
	}
}

싱글톤 패턴을 사용합니다. (코드자체를 제가 구현하진 않았고.. 학원에서 배운것과 구글링 한 것을 참고해서 구현했습니다..!)

InstanceHolder 클래스는 정적 중첩 클래스 입니다.GoogleLoginApi 클래스의 인스턴스인 INSTANCE 필드를 생성합니다. 이 필드는 InstanceHolder 클래스가 로드될 때 GoogleLoginApi의 새 인스턴스로 초기화됩니다.

instance() 메서드InstanceHolder 클래스에서 INSTANCE 필드를 반환해줍니다.

(2). GoogleLoginBean

package com.project.security;

import java.io.IOException;
import java.util.UUID;

import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import com.github.scribejava.core.builder.ServiceBuilder;
import com.github.scribejava.core.model.OAuth2AccessToken;
import com.github.scribejava.core.model.OAuthRequest;
import com.github.scribejava.core.model.Response;
import com.github.scribejava.core.model.Verb;
import com.github.scribejava.core.oauth.OAuth20Service;


@Component
public class GoogleLoginBean {
	private final static String GOOGLE_CLIENT_ID = "12047(....).apps.googleusercontent.com";
    private final static String GOOGLE_CLIENT_SECRET = "GOCSPX-(.....)CvZvy";
    private final static String GOOGLE_REDIRECT_URI = "(....)/google/callback";
    private final static String GOOGLE_SCOPE = "email openid profile";
    private final static String PROFILE_API_URL ="https://www.googleapis.com/oauth2/v2/userinfo";
	private static final String SESSION_STATE="googleSessionState";
	
	// 구글 로그인 기능을 제공하는 API를 호출하여 결과(code와 stae)를 반환하는 메소드
	public String getAuthorizationUrl(HttpSession session) {
		
		// 세션의 유효성 검증을 위한 난수값을 생성
		String state=UUID.randomUUID().toString();
		// 난수값을 세션 속성값으로 저장
		session.setAttribute(SESSION_STATE, state);
		session.setMaxInactiveInterval(60 * 60); 
		
		// 로그인 기능을 요청하기 위한 정보가 저장된 OAuth20Service 객체 생성
		OAuth20Service oauthService=new ServiceBuilder()
				.apiKey(GOOGLE_CLIENT_ID)
				.apiSecret(GOOGLE_CLIENT_SECRET)
				.callback(GOOGLE_REDIRECT_URI)
				.state(state)
				.scope(GOOGLE_SCOPE)
				.build(GoogleLoginApi.instance());
		
		// 인증 URL
		String authorizationUrl = oauthService.getAuthorizationUrl();
		
		return authorizationUrl;
	}
	
	// 로그인 처리 후 로그인 사용자의 접근 토큰을 발급받는 API를 호출 -> 사용자 접근 토큰을 반환
	public OAuth2AccessToken getAccessToken(HttpSession session, String code, String state) throws IOException {
		// 세션의 유효성 검증을 위해 세션에 저장된 속성값을 반환받아 저장
		String sessionState=(String)session.getAttribute(SESSION_STATE);
		
		// 매개변수로 받은 값과 세션에 저장된 값이 다른 경우
		if(!StringUtils.pathEquals(sessionState, state)) {
			return null;
		}
		
		// 사용자 접근 토큰을 발급 받기 위한 정보가 저장된 OAuth20Service 객체 생성
		OAuth20Service oAuth20Service=new ServiceBuilder()
				.apiKey(GOOGLE_CLIENT_ID)
				.apiSecret(GOOGLE_CLIENT_SECRET)
				.callback(GOOGLE_REDIRECT_URI)
				.state(state)
				.scope(GOOGLE_SCOPE)
				.build(GoogleLoginApi.instance());
		
		// 사용자 접근 토큰을 발급하는 API를 요청하여 토큰을 발급받아 저장
		OAuth2AccessToken accessToken=oAuth20Service.getAccessToken(code);
		
		return accessToken;
	}
	
	// 사용자 접근 토큰을 사용하여 사용자의 프로필을 제공하는 API를 호출
	public String getUserProfile(OAuth2AccessToken oauthToken) throws IOException{
		OAuth20Service oauthService=new ServiceBuilder()
				.apiKey(GOOGLE_CLIENT_ID)
				.apiSecret(GOOGLE_CLIENT_SECRET)
				.callback(GOOGLE_REDIRECT_URI)
				.scope(GOOGLE_SCOPE)
				.build(GoogleLoginApi.instance());
		
		// 사용자의 프로필을 제공하는 API를 요청하기 위한 객체 생성
		OAuthRequest request = new OAuthRequest(Verb.GET, PROFILE_API_URL, oauthService);
		// 사용자 접속 토큰과 API 요청 객체를 전달하여 로그인 사용의 프로필 요청
		oauthService.signRequest(oauthToken, request);
		// 사용자 프로필을 저장
		Response response = request.send();
		String responseBody = response.getBody();
		
		return responseBody;
	}
}

GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET

의 값들을 (...)으로 대체했습니다. 발급받은 클라이언트 값을 직접 넣어주시면됩니다.

GOOGLE_REDIRECT_URI 에는 자신의 도메인이나 로컬에서 하고있다면 localhost... 등 자신의 콜백 URI로 작성해주세요!!


2-3. Controller

package com.project.auth;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import javax.servlet.http.HttpSession;

import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.github.scribejava.core.model.OAuth2AccessToken;
import com.project.dto.Userinfo;
import com.project.dto.UserinfoAuth;
import com.project.exception.ExistsUserinfoException;
import com.project.exception.UserinfoNotFoundException;
import com.project.security.CustomUserDetails;
import com.project.security.GoogleLoginBean;
import com.project.service.UserinfoService;

import lombok.RequiredArgsConstructor;

@Controller
@RequestMapping("/google")
@RequiredArgsConstructor
public class GoogleLoginController {
	private final GoogleLoginBean googleLoginBean;
	private final UserinfoService userinfoService;

	@RequestMapping("/login")
	public String googlelogin(HttpSession session) throws UnsupportedEncodingException {
		String googleAuthUrl = googleLoginBean.getAuthorizationUrl(session);
		return "redirect:" + googleAuthUrl;
	}

	@RequestMapping("/callback")
	public String googlelogin(@RequestParam String code, @RequestParam String state, HttpSession session)
			throws IOException, ParseException, ExistsUserinfoException, UserinfoNotFoundException, ParseException {
		OAuth2AccessToken accessToken = googleLoginBean.getAccessToken(session, code, state);

		String apiResult = googleLoginBean.getUserProfile(accessToken);

		JSONParser parser = new JSONParser();
		Object object = parser.parse(apiResult);
		JSONObject responseObject = (JSONObject) object;

		// 사용자 json 데이터를 각각 id, email 등 각각 나눠서 저장
		String id = (String) responseObject.get("id");
		String email = (String) responseObject.get("email");
		String name = (String) responseObject.get("name");

		// (spring-security) 소셜 로그인 계정에 "ROLE_SOCIAL" 권한 부여
		UserinfoAuth auth = new UserinfoAuth();
		auth.setId("google_" + id);
		auth.setAuth("ROLE_SOCIAL");

		List<UserinfoAuth> authList = new ArrayList<UserinfoAuth>();
		authList.add(auth);

		Userinfo userinfo = new Userinfo();
		userinfo.setId("google_" + id);
		userinfo.setPw(UUID.randomUUID().toString());
		userinfo.setName(null);
		userinfo.setNickname(name);
		userinfo.setEmail(email);
		userinfo.setAddress(null);
		userinfo.setEnabled("0");
		userinfo.setSecurityAuthList(authList);
		
		// 사용자의 정보를 userinfo 테이블과 auth 테이블에 저장
		userinfoService.addUserinfo(userinfo, "ROLE_SOCIAL");
		userinfoService.addUserinfoAuth(auth);
		userinfoService.updateUserLogindate(userinfo.getId());
		
		// 여기부턴 spring-security 적용 관련입니다.
		// 네이버 로그인 사용자 정보를 사용하여 UserDetails 객체(로그인 사용자)를 생성하여 저장
		CustomUserDetails customUserDetails=new CustomUserDetails(userinfo);
		
		// UsernamePasswordAuthenticationToken 객체를 생성하여 Spring Security가 사용 가능한 인증 사용자로 등록 처리
		// UsernamePasswordAuthenticationToken 객체 : 인증 성공한 사용자를 Spring Security가 사용 가능한 인증 사용자로 등록 처리하는 객체
		Authentication authentication=new UsernamePasswordAuthenticationToken
				(customUserDetails, null, customUserDetails.getAuthorities());
		
		// SecurityContextHolder 객체 : 인증 사용자의 권한 관련 정보를 저장하기 위한 객체
		SecurityContextHolder.getContext().setAuthentication(authentication);
			
		return "redirect:/";
	}
}

구글 로그인 이미지 클릭시 .../google/login으로 요청 ->
컨트롤러의 /google/login에서 GoogleLoginBean 클래스의 getAuthorizationUrl 메서드를 이용해 인증 URL을 반환하고 callback 처리 되어 집니다.
컨트롤러의 /google/callback 응답에서는 사용자의 정보를 user 테이블에 저장하고 권한을 부여하게 됩니다.

**

2-4. 구현 화면

구글 로그인 요청시 계정 선택 화면이 나오게되고,
계정 선택시 해당 사용자의 정보로 로그인되어지게 됩니다.

사용자 정보 페이지에서 보면 전달받은 사용자의 json 데이터인 email, name을 확인할 수 있고, 닉네임을 변경할 수 있도록 구현했습니다.


3. Google Search Console

사용할 도메인 인증을 받도록 하겠습니다.
https://search.google.com/search-console/welcome

접속 후

도메인 작성칸에 도메인 입력해주세요. (http, https 프로토콜은 입력 x)


도메인을 구입한 곳에서 DNS 설정을 추가해줍니다.
저는 Route53에서 도메인을 구매했기 때문에 AWS에서 설정하도록 하겠습니다!


레코드 생성 클릭


레코드 유형을 TXT로 설정하고
값에는 Google Search Console에서 복사한 값을 넣어주세요!


다시 Google Search Console로 돌아가서 확인 버튼을 누르고 조금 기다리면 확인이 잘 되는것을 볼 수 있습니다!!

0개의 댓글