팀 프로젝트
MVC모델을 이용한 SNS 로그인(네이버,구글,카카오) 구현
본인은 구글 부분을 맡아서 처리했습니다.
다 만들고 작성하는거라 틀린 부분이 있을수도 있으니 참고만 부탁드립니다.
참고 문서
https://developers.google.com/identity/gsi/web/guides/overview?hl=ko
https://developers.google.com/identity/protocols/oauth2/web-server?hl=ko#httprest_5
구글 HTML 통합 코드 생성
https://developers.google.com/identity/gsi/web/tools/configurator?hl=ko
java 1.8
spring framework 5.0.7
mysql, mybatis 1.3.2, HikariCP 2.7.4
Apache Tomcat 8.5
Oauth 2.0
google login_sequence diagram
https://console.developers.google.com
해당 링크로 들어가 왼쪽 탭에 '사용자 인증정보' 클릭
글쓴이는 이미 만들어둔 상태라 아래와 같은 화면이다. '사용자 인증 정보 만들기' 클릭
OAuth 클라이언트 ID 클릭
본인에게 맞는 유형 클릭
글쓴이는 '웹 애플리케이션'이므로 웹 애플리케이션 선택함
설정은 아래와 같이 하거나 본인에게 맞는 설정을 해주자.
생성이 완료되면 다음과 같은 창이 나타난다.
클라이언트 ID와 클라이언트 보안 비밀번호는 자주 쓰이니 따로 기록하거나 즐겨찾기로 자주 보는걸 추천합니다.
View 와 Controller 부분은 필요한 기능이 있으면 따로 추가해주세요.
pom.xml에 다음과 같이 의존성 추가
<!-- http client(구글)-->
<dependency>
<groupId>com.google.api-client</groupId>
<artifactId>google-api-client</artifactId>
<version>1.31.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.http-client/google-http-client-jackson2 -->
<dependency>
<groupId>com.google.http-client</groupId>
<artifactId>google-http-client-jackson2</artifactId>
<version>1.31.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.thymeleaf/thymeleaf -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.0.11.RELEASE</version>
</dependency>
home.jsp (view 부분)
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
// 최상단에 넣기
<c:url value="/oauth2/google/login" var="googleOAuthUrl" />
<a href="${googleOAuthUrl}"><img src="구글 버튼 이미지 경로" class="social-icon" id="google"></a>
GoogleProfile.jsp
package com.tit.model;
public class GoogleProfile {
private String email;
private String nickname;
private String id;
private String sns;
public GoogleProfile() {
}
public GoogleProfile(String email, String nickname, String id) {
this.email = email;
this.nickname = nickname;
this.id = id;
this.sns = "google";
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getSns() {
return sns;
}
public void setSns(String sns) {
this.sns = sns;
}
@Override
public String toString() {
return "UserProfile [email=" + email + ", nickname=" + nickname + ", sns=" + sns + "]";
}
}
GoogleLoginBO 작성
package 본인 패키지명;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Arrays;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Service;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
@Service // @Service로 주석 처리된 서비스 클래스로 Spring 서비스 구성 요소임을 나타냅니다.
public class GoogleLoginBO {
/* 인증 요청문을 구성하는 파라미터 */
// client_id: 애플리케이션 등록 후 발급받은 클라이언트 아이디
// client_secret : 애플리케이션 등록 후 발급받은 클라이언트 시크릿
// redirect_uri: 로그인 인증의 결과를 전달받을 콜백 URL(URL 인코딩). 애플리케이션을 등록할 때 Callback URL에 설정한 정보입니다.
// SCOPE = 요청할 권한의 범위를 지정하는 값입니다. 구글 API에 따라 다르게 설정할 수 있습니다.
private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
private final static String CLIENT_ID = "본인 클라이언트 아이디";
private final static String CLIENT_SECRET = "본인 클라이언트 시크릿";
private final static String REDIRECT_URI = "http://localhost:8080/oauth2/google/callback";
private final static String SCOPE = "openid email profile"; // 요청할 권한의 범위
private GoogleAuthorizationCodeFlow getGoogleAuthorizationCodeFlow() {
GoogleClientSecrets.Details web = new GoogleClientSecrets.Details();
web.setClientId(CLIENT_ID);
web.setClientSecret(CLIENT_SECRET);
GoogleClientSecrets clientSecrets = new GoogleClientSecrets().setWeb(web);
return new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT, JSON_FACTORY, clientSecrets,Arrays.asList(SCOPE.split(" "))).setAccessType("offline").setApprovalPrompt("force").build();
}
public String getAuthorizationUrl(HttpSession session) {
GoogleAuthorizationCodeFlow flow = getGoogleAuthorizationCodeFlow();
String url = flow.newAuthorizationUrl().setRedirectUri(REDIRECT_URI).build();
// String url = flow.newAuthorizationUrl().setRedirectUri(REDIRECT_URI).setState("state").build(); // state는 CSRF 공격을 방지하려면 작성
return url;
}
public String[] getUserProfile(String authCode) throws IOException {
GoogleTokenResponse tokenResponse = new GoogleAuthorizationCodeTokenRequest(HTTP_TRANSPORT, JSON_FACTORY,
CLIENT_ID, CLIENT_SECRET, authCode, REDIRECT_URI).execute();
GoogleIdToken idToken = tokenResponse.parseIdToken();
GoogleIdToken.Payload payload = idToken.getPayload();
String email = payload.getEmail();
String name = (String) payload.get("name");
String id = payload.getSubject();
return new String[]{email, name, id};
}
// 회원 탈퇴
public void revokeToken(String accessToken) throws IOException { // 사용자의 액세스 토큰을 취소
URL url = new URL("https://oauth2.googleapis.com/revoke?token=" + accessToken);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.getResponseCode(); // 응답 코드를 읽어야 API 요청이 발생합니다.
}
}
GoogleProfileService 작성
package 본인 패키지명;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import 본인 패키지명.GoogleMapper;
import 본인 패키지명.GoogleProfile;
@Service
public class GoogleProfileService {
@Autowired
private GoogleMapper googleMapper;
public void addUserProfile(GoogleProfile googleProfile) {
googleMapper.insertUserProfile(googleProfile);
} // GoogleProfile 객체를 가져와 googleMapper의 insertUserProfile 메서드를 사용하여 데이터베이스에 삽입
public int findUserProfileByEmail(String email) {
return googleMapper.findByEmail(email);
} // 이메일 주소를 매개변수로 받아 findByEmail 메서드를 사용하여 해당 이메일로 등록된 사용자 프로필 수를 반환하는 public 메서드입니다. 데이터베이스에 이미 해당 사용자 프로필이 있는지 확인할때 사용
public void delUserProfile(GoogleProfile delProfile) {
googleMapper.delUserProfile(delProfile);
// 회원탈퇴 -> DB에 있는 회원 삭제
} // GoogleProfile 객체를 가져와 googleMapper의 delUserProfile 메서드를 사용하여 데이터베이스에서 해당 사용자 프로필을 삭제
}
GoogleMapper.java 작성
package 본인 패키지명;
import 본인 패키지명.GoogleProfile;
public interface GoogleMapper {
void insertUserProfile(GoogleProfile googleProfile); // GoogleProfile 객체를 DB에 삽입(insert)하는 메소드
int findByEmail(String email);
void delUserProfile(GoogleProfile delProfile);
// int로 설정한 이유는 삭제된 행의 수를 반환하기 위해서입니다.
// MyBatis에서는 INSERT, UPDATE, DELETE와 같은 쓰기 작업을 수행할 때 영향을 받은 행의 수를 반환합니다. 이렇게 함으로써, 호출자가 해당 작업이 실제로 수행되었는지 확인할 수 있습니다.
// 예를 들어, deleteUserProfile 메소드를 호출한 후 반환된 값이 1이면 한 명의 사용자가 삭제된 것을 알 수 있습니다. 반면, 반환된 값이 0이면 이메일 주소와 일치하는 사용자가 없어 삭제되지 않았음을 알 수 있습니다.
// 이를 통해 오류를 처리하거나 로깅 목적으로 사용할 수 있습니다.
String checksns(String email);
// 이메일 주소를 입력받아 해당 이메일 주소와 연동된 SNS 계정이 있는지 조회(select)하는 메소드입니다. 반환값은 SNS 계정이 존재한다면 해당 SNS 계정 종류를 문자열(String)로 반환하고, 존재하지 않으면 null을 반환
}
GoogleMapper.xml 작성
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tit.mapper.GoogleMapper">
<insert id="insertUserProfile" parameterType="com.tit.model.GoogleProfile">
INSERT INTO member (id, email, nickname, sns)
VALUES (#{id}, #{email}, #{nickname}, #{sns})
</insert>
<select id="findByEmail" parameterType="String" resultType="int">
SELECT COUNT(*) FROM member
WHERE email = #{email}
</select>
<delete id="delUserProfile">
DELETE FROM member WHERE email = #{email}
</delete>
<select id="checksns" parameterType="string" resultType="string">
SELECT sns FROM member WHERE email = #{email};
</select>
</mapper>
본인은 아래와 같이 파일을 구성했다.
파일별로 기능을 정리해보자면
findpassword : 비밀번호 찾기 페이지
home : 홈페이지 가장 첫화면(여기에 SNS로그인을 넣음)
Medical : 로그인 후 도착하는 페이지
MemberJoin : 아이디가 없는 회원이 회원 가입했을때 오는 페이지
GoogleController.java 작성
package com.tit.app;
import java.io.IOException;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import com.tit.mapper.GoogleMapper;
import com.tit.model.GoogleProfile;
import com.tit.service.GoogleLoginBO;
import com.tit.service.GoogleProfileService;
@Controller
public class GoogleLoginController {
// GoogleLoginBO, GoogleProfileService, GoogleMapper @Autowired를 이용해 의존성 주입
// Google OAuth2 인증, Google 프로필 정보를 DB에 CRUD 작업, Google 프로필 정보를 DB에 저장하는 데 사용
@Autowired
private GoogleLoginBO googleLoginBO;
@Autowired
private GoogleProfileService googleProfileService;
@Autowired
private GoogleMapper googleMapper;
// GET /oauth2/google/login 요청이 들어오면, GoogleLoginBO를 이용하여 구글 로그인 URL을 생성
// 이를 포함한 redirect 응답을 보냄 이 과정을 통해 사용자는 구글 계정으로 로그인
@GetMapping("/oauth2/google/login")
public String login(HttpSession session) {
String googleAuthorizationUrl = googleLoginBO.getAuthorizationUrl(session);
return "redirect:" + googleAuthorizationUrl;
}
// REDIRECT_URI /oauth2/google/callback
// GET /oauth2/google/callback 요청이 들어오면, GoogleLoginBO를 이용하여 사용자의 access token을 받아
// getUserProfile 메소드를 이용하여 access token으로부터 사용자의 이메일, 이름, ID 등의 정보를 받아옵니다.
// 받아온 정보를 바탕으로 GoogleProfile 객체를 생성
@RequestMapping("/oauth2/google/callback") // 이부분 유효한지 확인
public String callback(@RequestParam("code") String authCode, HttpSession session) throws IOException {
String[] userProfileData = googleLoginBO.getUserProfile(authCode);
String email = userProfileData[0];
String name = userProfileData[1];
String id = userProfileData[2];
GoogleProfile userProfile = new GoogleProfile(email, name, id);
int count = googleProfileService.findUserProfileByEmail(userProfile.getEmail());
if(count > 0) {
session.setAttribute("UserProfile", userProfile);
session.setAttribute("oauthToken", authCode);
session.setAttribute("googleNickname", name);
String snsid = googleMapper.checksns(email);
session.setAttribute("Snsid", snsid);
System.out.println(snsid); //test
return "redirect:/Medical";
} else {
googleProfileService.addUserProfile(userProfile);
session.setAttribute("UserProfile", userProfile);
session.setAttribute("googleNickname", name);
String snsid = googleMapper.checksns(email);
session.setAttribute("Snsid", snsid);
return "redirect:/MemberJoin";
}
}
// 로그아웃
// /oauth2/google/logout 요청이 들어오면, 세션을 초기화하고,
// 홈 화면으로 redirect 합니다 이 과정을 통해 사용자는 로그아웃 할 수 있다.
@RequestMapping(value = "/oauth2/google/logout", method = { RequestMethod.GET, RequestMethod.POST })
public String logout(HttpSession session) throws IOException {
session.invalidate();
return "redirect:/";
}
// 회원 탈퇴
// GET /oauth2/google/account_rm 요청이 들어오면
// GoogleProfileService의 delUserProfile 메소드를 호출하여 DB에서 해당 사용자 정보를 삭제
// GoogleLoginBO의 revokeToken 메소드를 호출하여 access token을 폐기합니다. 세션을 초기화하고, 홈 화면으로 redirect 한다.
// 이 과정을 통해 사용자는 회원 탈퇴를 할 수 있다.
@RequestMapping(value = "/oauth2/google/acount_rm", method = { RequestMethod.GET, RequestMethod.POST })
public String revokeAccessToken(HttpSession session) throws IOException {
String oauthToken = (String) session.getAttribute("oauthToken");
GoogleProfile delProfile = (GoogleProfile) session.getAttribute("UserProfile");
System.out.println(oauthToken);
System.out.println(delProfile);
googleProfileService.delUserProfile(delProfile);
googleLoginBO.revokeToken(oauthToken);
session.invalidate();
return "redirect:/";
}
}
spring security를 사용하지 못한게 아쉬웠던 프로젝트 입니다.
다음번에는 공부해서 security를 적용해보고 싶네요..
이상으로 OAuth2를 이용한 SNS구글 로그인을 마치도록 하겠습니다.