팀프로젝트/SpringBoot

스프링부트 팀플) 20240327 구글로그인 기능 구현

일일일코_장민기 2024. 3. 27. 18:17
728x90
application.properties

#Google Login

google.auth.url=https://oauth2.googleapis.com

google.login.url=https://accounts.google.com

google.redirect.url=등록한 사이트 주소

google.client.id= 발급받은 아이디

google.secret

= 발급받은 보안 비밀번호

 

-> 만약 git에 올릴 것이라면 이 부분은 지우고 다르게 배포해야 한다(github에서 구글보안코드는 막게 되어 있음)

 

GoogleLoginResponse(롬복 필수)
package com.moonBam.controller.member.OpenApi;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class GoogleLoginResponse {
    private String access_token; 	// 애플리케이션이 Google API 요청을 승인하기 위해 보내는 토큰
    private String expires_in;   	// Access Token의 남은 수명
    private String refreshToken;    // 새 액세스 토큰을 얻는 데 사용할 수 있는 토큰
    private String scope;
    private String token_type;   	// 반환된 토큰 유형(Bearer 고정)
    private String id_token;
}

 

GoogleOAuthRequest(롬복 필수)
package com.moonBam.controller.member.OpenApi;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class GoogleOAuthRequest {
    private String redirectUri;
    private String clientId;
    private String clientSecret;
    private String code;
    private String responseType;
    private String scope;
    private String accessType;
    private String grantType;
    private String state;
    private String includeGrantedScopes;
    private String loginHint;
    private String prompt;
}

 

create table socialMemberDB (
 primaryNum VARCHAR2(50) primary key, --sub
 email VARCHAR2(50) not null,
 NICKNAME VARCHAR2(50) NOT NULL
);

 

그런데 문제가 발생

기존의 MemberDTO를 session으로 넣어서 사용하고 있었다는 문제가 발생했다.

나 혼자만 사용하는 것이라면 괜찮은데 다른 게시판이나 댓글에서도 사용 중인 부분이었기 때문에 바꾸기도 어려운 상황...

결국 새로 만든 DTO를 폐기하고 MemberDTO로 통합하기로 했다.

 

또한 기존 MemberDTO로 크게 수정해야 했다.

거의 모든 값에 null이 붙어 있는 상태인데, 이걸 소셜로그인에도 적용하면 사실상 소셜로그인의 의미가 없어진다.

이중 SSN, 핸드폰 번호에는 null 제한을 풀고 나머지는 임의 처리

아이디는 소셜 이니셜 + sub, 비밀번호는 단방향 암호화를 통해 사실상 사용할 수 없도록 변경했다.

이름은 받아오는 값이 있으니 그대로 입력

닉네임은 기본값으로 임의값을 부여하되, 유저가 수정할 수 있도록 했다.

이메일 아이디와 도메인도 받아오는 값이 있으니 그대로 입력하고,

가입일과, 유저 타입도 기본값으로 설정

 

CREATE TABLE memberDB (
    userId VARCHAR2(30) PRIMARY KEY,
    userPw VARCHAR2(50) NOT NULL CHECK (LENGTHB(userPw) >= 4),
    userName VARCHAR2(30) NOT NULL CHECK (LENGTHB(userName) >= 2),
    userSSN1 VARCHAR2(10),
    userSSN2 VARCHAR2(10),
    userGender VARCHAR2(20) CHECK (userGender IN ('male', 'female')),
    nickname VARCHAR2(30) UNIQUE,
    userPhoneNum1 VARCHAR2(3),
    userPhoneNum2 VARCHAR2(4),
    userPhoneNum3 VARCHAR2(4),
    userEmailId VARCHAR2(50) NOT NULL,
    userEmailDomain VARCHAR2(50) NOT NULL,
    userSignDate VARCHAR2(10) DEFAULT TO_CHAR(SYSDATE, 'yyyy/MM/dd') NOT NULL,
    userType VARCHAR2(1) DEFAULT '1' CHECK (userType IN ('0', '1', '2', '3', '4')) -- 0 for admin, 1 for member, default is member
);

 

 

 

 

 

 

 

 

 

GoogleLoginController
package com.moonBam.controller.member.OpenApi;
import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.util.UriComponentsBuilder;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.moonBam.controller.member.DebugBoardController;
import com.moonBam.controller.member.SecurityController;
import com.moonBam.dto.MemberDTO;
import com.moonBam.service.member.OpenApiService;
import com.moonBam.service.member.RegisterService;

import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;

@Slf4j
@RestController
public class GoogleLoginController {

    @Value("${google.auth.url}")
    private String googleAuthUrl;

    @Value("${google.login.url}")
    private String googleLoginUrl;

    @Value("${google.client.id}")
    private String googleClientId;

    @Value("${google.redirect.url}")
    private String googleRedirectUrl;

    @Value("${google.secret}")
    private String googleClientSecret;
    
    @Autowired
    OpenApiService serv;
    
    @Autowired
    RegisterService rServ;
    
    @Autowired
    DebugBoardController dbc;
    
    @Autowired
    SecurityController sc;

    // 구글 로그인창 호출
    @GetMapping(value = "Login/getGoogleAuthUrl")
    public ResponseEntity<?> getGoogleAuthUrl(HttpServletRequest request) throws Exception {

        String reqUrl = googleLoginUrl + "/o/oauth2/v2/auth?client_id=" + googleClientId + "&redirect_uri=" + googleRedirectUrl
                + "&response_type=code&scope=email%20profile%20openid&access_type=offline";

        log.info("myLog-LoginUrl : {}",googleLoginUrl);
        log.info("myLog-ClientId : {}",googleClientId);
        log.info("myLog-RedirectUrl : {}",googleRedirectUrl);

        HttpHeaders headers = new HttpHeaders();
        headers.setLocation(URI.create(reqUrl));

        return new ResponseEntity<>(headers, HttpStatus.MOVED_PERMANENTLY);	//로그인 후 /Login/google으로 리다이렉션
    }

    // 구글에서 리다이렉션
    @GetMapping(value = "Login/google")
    public ModelAndView oauth_google_check(HttpServletRequest request, @RequestParam(value = "code") String authCode, HttpServletResponse response, HttpSession session) throws Exception{
    	
    	//구글에 등록된 설정정보를 보내어 약속된 토큰을 받기 위한 객체 생성
    	GoogleOAuthRequest googleOAuthRequest = GoogleOAuthRequest
	        .builder()
	        .clientId(googleClientId)
	        .clientSecret(googleClientSecret)
	        .code(authCode)
	        .redirectUri(googleRedirectUrl)
	        .grantType("authorization_code")
	        .build();
        RestTemplate restTemplate = new RestTemplate();
        
        //토큰 요청
        ResponseEntity<GoogleLoginResponse> apiResponse = restTemplate.postForEntity(googleAuthUrl + "/token", googleOAuthRequest, GoogleLoginResponse.class); 	
        
        //받은 토큰을 토큰객체에 저장
        GoogleLoginResponse googleLoginResponse = apiResponse.getBody();
        log.info("responseBody {}",googleLoginResponse.toString());
        String googleToken = googleLoginResponse.getId_token();

        //받은 토큰을 구글에 보내 유저정보를 얻음
        String requestUrl = UriComponentsBuilder.fromHttpUrl(googleAuthUrl + "/tokeninfo").queryParam("id_token",googleToken).toUriString();

        //허가된 토큰의 유저정보를 결과로 받음
        String resultJson = restTemplate.getForObject(requestUrl, String.class);

        //받은 JSON데이터를 사용할 Map데이터로 변환
        ObjectMapper objectMapper = new ObjectMapper();
        String resultString = objectMapper.writeValueAsString(resultJson);
        Map<String, Object> map = objectMapper.readValue(resultJson, Map.class);
        
        //이미 가입한 사람인지 확인
        MemberDTO check  = serv.selectOneAPIMember(sc.encrypt((String) map.get("sub")));
        ModelAndView mav = new ModelAndView();
      
        //미가입자일 경우, 자동 가입
        if(check == null) {
    	  
		//MemberDTO 사용을 위한 임의의 값 입력
		String pw = sc.encrypt("Google"+dbc.getNum(16));
		String[] emailParts = ((String) map.get("email")).split("@");
		Date currentDate = new Date();
			SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");
			String userSignDate = dateFormat.format(currentDate);
		  
		MemberDTO dto = new MemberDTO();
      		dto.setUserId(sc.encrypt((String) map.get("sub")));
          	dto.setUserPw(pw);					
          	dto.setUserName(((String) map.get("name")).replace(" ", ""));
          	dto.setNickname(dbc.getNum(10)+"-G");
          	dto.setUserEmailId(emailParts[0]);			
          	dto.setUserEmailDomain(emailParts[1]);				
          	dto.setUserSignDate(userSignDate);
  			dto.setUserType("1");
  		
  		//회원가입
  		serv.insertAPIMember(dto);	
    	
  		//닉네임 변경하는 화면으로 이동
  		MemberDTO nDTO  = serv.selectOneAPIMember(dto.getUserId());
  		session.setAttribute("loginUser", nDTO);
  		mav.addObject("dto", nDTO);
	    mav.setViewName("member/Login/APILogin");
	    return mav; 
        
      //가입자일 경우, 메인으로 이동
      } else {
	    session.setAttribute("loginUser", check);
        mav.setViewName("redirect:/");
        return mav;
      }
        
    }
    
}
OpenApiService
package com.moonBam.service.member;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.moonBam.dao.member.OpenApiDAO;
import com.moonBam.dto.MemberDTO;

@Service
public class OpenApiService {

	@Autowired
	OpenApiDAO dao;

	public void insertAPIMember(MemberDTO dto) {
		dao.insertAPIMember(dto);
	}

	public MemberDTO selectOneAPIMember(String userId) {
		MemberDTO dto = dao.selectOneAPIMember(userId);
		return dto;
	}

	public void updateAPIMemberNickname(Map<String, String> map) {
		dao.updateAPIMemberNickname(map);
	}
	
}
OpenApiDAO
package com.moonBam.dao.member;

import java.util.Map;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.moonBam.dto.MemberDTO;

@Repository
public class OpenApiDAO {

	@Autowired
	SqlSessionTemplate session;

	public void insertAPIMember(MemberDTO dto) {
		session.insert("insertAPIMember", dto);
	}

	public MemberDTO selectOneAPIMember(String userId) {
		MemberDTO dto = session.selectOne("selectOneAPIMember", userId);
		return dto;
	}

	public void updateAPIMemberNickname(Map<String, String> map) {
		session.update("updateAPIMemberNickname", map);
		
	}
	
	
}
MemberMapper.xml
	<!-- **************************************************************** -->
	<!-- *********************API Mapper********************************* -->
	<!-- **************************************************************** -->
	
	<!-- API 회원 출력 -->
	<select id="selectOneAPIMember" resultType="MemberDTO">
		SELECT 	*
		FROM 	memberDB
		WHERE	userId = #{userId}
	</select>
	
	<!-- API 회원가입 -->
	<insert id="insertAPIMember" parameterType="MemberDTO">
		INSERT INTO memberDB (
			userId, userPw, userName, 
			nickname, userEmailId, userEmailDomain
		)
		VALUES (
			#{userId}, #{userPw}, #{userName},
			#{nickname}, #{userEmailId}, #{userEmailDomain}
		)
	</insert>
	
	<!-- API 회원가입된 유저 닉네임 변경 -->
	<update id="updateAPIMemberNickname" parameterType="hashmap">
		UPDATE 	memberDB
		SET		nickname = #{nickname}
		WHERE	userId = #{userId}
	</update>

 

 

 

APILogin.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>닉네임 변경</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous"> 
    <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
</head>

<body>
    <div class="container mt-5">
        <div class="card">
            <div class="card-body">
                <h3 class="card-title">닉네임을 변경하시겠습니까?</h3>
                <form action="<c:url value='/changeNickname'/>" method="post">
                    <input type="hidden" name="userId" value="${dto.userId}">
                    <input type="hidden" id="check" value="${dto.nickname}">
                    <input type="text" id="nickname" name="nickname" value="${dto.nickname}" minlength="2" required autofocus="autofocus" class="form-control mb-3">
                    <span id="confirmNicknameError" style="color: red;"></span>
                    <div class="d-grid gap-2 d-md-flex justify-content-md-end mt-3">
                        <input type="submit" name="clickType" value="변경하기" class="btn btn-primary me-md-2">
                        <input type="submit" name="clickType" value="그대로 사용하기" class="btn btn-secondary">
                    </div>
                </form>
            </div>
        </div>
    </div>

    <script type="text/javascript">
        $(function(){
            //닉네임 중복 확인
            var prevNickname = ""; 
            $("#nickname").on("focusout", function() {
                var nickname = $("#nickname").val();
                var errorSpan = $("#confirmNicknameError");
                var check = $("#check").val()
                
                if(check !== nickname){
                    if (nickname !== prevNickname) {
                        $.ajax({
                            type: "POST",
                            url: "<c:url value='/AjaxNicknameDuplicate'/>", 
                            data: { nickname: nickname },
                            success: function (response) {
                                //닉네임이 DB에 저장된 닉네임과 일치하는 데이터가 있을 경우, ajax 출력
                                if (response === "duplicate") {
                                    errorSpan.text("이미 사용 중인 닉네임입니다.");
                                } else {
                                    errorSpan.text("");
                                } 
                            },
                            error: function (error) {
                                console.error("닉네임 중복 검사 에러:", error);
                            }
                        })
                        prevNickname = nickname;
                    };
                };
            });
            
            //닉네임 중복일 경우, 이벤트 중지
            $("form").on("submit", function(){
                if($("#confirmNicknameError").text() == "이미 사용 중인 닉네임입니다."){
                    event.preventDefault();
                    alert("중복된 닉네임입니다.");
                } else {
                    $("form")[0].submit();
                }
            })
        })
    </script>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</body>
</html>

 

OpenApiController

 

package com.moonBam.controller.member.OpenApi;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;

import com.moonBam.dto.MemberDTO;
import com.moonBam.service.member.OpenApiService;

@Controller
public class OpenApiController {

	@Autowired
	OpenApiService serv;
	
	// 닉네임 변경
	@PostMapping("/changeNickname")
	public String changeNickname(HttpSession session, MemberDTO dto, String nickname, String clickType) {
		if(clickType.equals("변경하기")) {
			MemberDTO nDTO  = serv.selectOneAPIMember(dto.getUserId());
	        
	        Map<String, String> map = new HashMap<>();
	        	map.put("userId", dto.getUserId());
	        	map.put("nickname", nickname);
	        
	        serv.updateAPIMemberNickname(map);
	        MemberDTO nnDTO  = serv.selectOneAPIMember(dto.getUserId());
			session.setAttribute("loginUser", dto);
		} 
		
		return "member/Login/apiRegisterSuccess";
	}

}

 

 

 

이후부터는 클릭하면 바로 로그인되어 메인으로 이동함