OAuth를 사용한 소셜 로그인
- OAuth는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준
- 사용자가 애플리케이션에게 모든 권한을 넘기지 않고 사용자 대신 서비스를 이용할 수 있게 해주는 HTTP 기반의 보안 프로토콜
카카오 로그인의 큰 흐름
카카오 로그인을 사용하기 위해서는 카카오 개발 사이트에서 애플리케이션 등록이 필요
'카카오 개발자' 사이트로 이동하여 회원가입을 진행, 본인만의 애플리케이션을 등록
1. 회원가입
2. 내 애플리케이션 메뉴 선택-> 애플리케이션 추가하기
3. 앱 아이콘, 앱 이름, 사업자명 저장
4. 사이트 도메인 등록하기 (개발중인 로컬환경의 서버 주소) -> http://localhost:8080
5. 카카오로 로그인 했을때 인가토큰을 받게 될 Redirect URI (callback) 를 설정하기
-> http://localhost:8080/api/user/kakao/callback 활성화
동의항목 설정하기
닉네임과 이메일을 '필수 동의'로 받기
카카오 인가코드받기
login.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link rel="stylesheet" type="text/css" href="/css/style.css">
<script src="https://code.jquery.com/jquery-3.7.0.min.js"
integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/dist/js.cookie.min.js"></script>
<meta charset="UTF-8">
<title>로그인 페이지</title>
</head>
<body>
<div id="login-form">
<div id="login-title">Log into Select Shop</div>
<button id="login-kakao-btn" onclick="location.href='https://kauth.kakao.com/oauth/authorize?client_id={REST_API_KEY}&redirect_uri=http://localhost:8080/api/user/kakao/callback&response_type=code'">
카카오로 로그인하기
</button>
<button id="login-id-btn" onclick="location.href='/api/user/signup'">
회원 가입하기
</button>
<div>
<div class="login-id-label">아이디</div>
<input type="text" name="username" id="username" class="login-input-box">
<div class="login-id-label">비밀번호</div>
<input type="password" name="password" id="password" class="login-input-box">
<button id="login-id-submit" onclick="onLogin()">로그인</button>
</div>
<div id="login-failed" style="display:none" class="alert alert-danger" role="alert">로그인에 실패하였습니다.</div>
</div>
</body>
<script>
$(document).ready(function () {
// 토큰 삭제
Cookies.remove('Authorization', {path: '/'});
});
const href = location.href;
const queryString = href.substring(href.indexOf("?") + 1)
if (queryString === 'error') {
const errorDiv = document.getElementById('login-failed');
errorDiv.style.display = 'block';
}
const host = 'http://' + window.location.host;
function onLogin() {
let username = $('#username').val();
let password = $('#password').val();
$.ajax({
type: "POST",
url: `/api/user/login`,
contentType: "application/json",
data: JSON.stringify({username: username, password: password}),
})
.done(function (res, status, xhr) {
const token = xhr.getResponseHeader('Authorization');
Cookies.set('Authorization', token, {path: '/'})
$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
jqXHR.setRequestHeader('Authorization', token);
});
window.location.href = host;
})
.fail(function (jqXHR, textStatus) {
alert("Login Fail");
window.location.href = host + '/api/user/login-page?error'
});
}
</script>
</html>
basic.js
const host = 'http://' + window.location.host;
let targetId;
let folderTargetId;
$(document).ready(function () {
const auth = getToken();
if (auth !== undefined && auth !== '') {
$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
jqXHR.setRequestHeader('Authorization', auth);
});
} else {
window.location.href = host + '/api/user/login-page';
return;
}
$.ajax({
type: 'GET',
url: `/api/user-info`,
contentType: 'application/json',
})
.done(function (res, status, xhr) {
const username = res.username;
const isAdmin = !!res.admin;
if (!username) {
window.location.href = '/api/user/login-page';
return;
}
$('#username').text(username);
if (isAdmin) {
$('#admin').text(true);
showProduct();
} else {
showProduct();
}
// 로그인한 유저의 폴더
$.ajax({
type: 'GET',
url: `/api/user-folder`,
error(error) {
logout();
}
}).done(function (fragment) {
$('#fragment').replaceWith(fragment);
});
})
.fail(function (jqXHR, textStatus) {
logout();
});
// id 가 query 인 녀석 위에서 엔터를 누르면 execSearch() 함수를 실행하라는 뜻입니다.
$('#query').on('keypress', function (e) {
if (e.key == 'Enter') {
execSearch();
}
});
$('#close').on('click', function () {
$('#container').removeClass('active');
})
$('#close2').on('click', function () {
$('#container2').removeClass('active');
})
$('.nav div.nav-see').on('click', function () {
$('div.nav-see').addClass('active');
$('div.nav-search').removeClass('active');
$('#see-area').show();
$('#search-area').hide();
})
$('.nav div.nav-search').on('click', function () {
$('div.nav-see').removeClass('active');
$('div.nav-search').addClass('active');
$('#see-area').hide();
$('#search-area').show();
})
$('#see-area').show();
$('#search-area').hide();
})
function numberWithCommas(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
function execSearch() {
/**
* 검색어 input id: query
* 검색결과 목록: #search-result-box
* 검색결과 HTML 만드는 함수: addHTML
*/
// 1. 검색창의 입력값을 가져온다.
let query = $('#query').val();
// 2. 검색창 입력값을 검사하고, 입력하지 않았을 경우 focus.
if (query == '') {
alert('검색어를 입력해주세요');
$('#query').focus();
return;
}
// 3. GET /api/search?query=${query} 요청
$.ajax({
type: 'GET',
url: `/api/search?query=${query}`,
success: function (response) {
$('#search-result-box').empty();
// 4. for 문마다 itemDto를 꺼내서 HTML 만들고 검색결과 목록에 붙이기!
for (let i = 0; i < response.length; i++) {
let itemDto = response[i];
let tempHtml = addHTML(itemDto);
$('#search-result-box').append(tempHtml);
}
},
error(error, status, request) {
logout();
}
})
}
function addHTML(itemDto) {
/**
* class="search-itemDto" 인 녀석에서
* image, title, lprice, addProduct 활용하기
* 참고) onclick='addProduct(${JSON.stringify(itemDto)})'
*/
return `<div class="search-itemDto">
<div class="search-itemDto-left">
<img src="${itemDto.image}" alt="">
</div>
<div class="search-itemDto-center">
<div>${itemDto.title}</div>
<div class="price">
${numberWithCommas(itemDto.lprice)}
<span class="unit">원</span>
</div>
</div>
<div class="search-itemDto-right">
<img src="../images/icon-save.png" alt="" onclick='addProduct(${JSON.stringify(itemDto)})'>
</div>
</div>`
}
function addProduct(itemDto) {
/**
* modal 뜨게 하는 법: $('#container').addClass('active');
* data를 ajax로 전달할 때는 두 가지가 매우 중요
* 1. contentType: "application/json",
* 2. data: JSON.stringify(itemDto),
*/
// 1. POST /api/products 에 관심 상품 생성 요청
$.ajax({
type: 'POST',
url: '/api/products',
contentType: 'application/json',
data: JSON.stringify(itemDto),
success: function (response) {
// 2. 응답 함수에서 modal을 뜨게 하고, targetId 를 reponse.id 로 설정
$('#container').addClass('active');
targetId = response.id;
},
error(error, status, request) {
logout();
}
});
}
function showProduct(folderId = null) {
/**
* 관심상품 목록: #product-container
* 검색결과 목록: #search-result-box
* 관심상품 HTML 만드는 함수: addProductItem
*/
let dataSource = null;
var sorting = $("#sorting option:selected").val();
var isAsc = $(':radio[name="isAsc"]:checked').val();
if (folderId) {
dataSource = `/api/folders/${folderId}/products?sortBy=${sorting}&isAsc=${isAsc}`;
} else if(folderTargetId === undefined) {
dataSource = `/api/products?sortBy=${sorting}&isAsc=${isAsc}&folderId=${folderId}`;
} else {
dataSource = `/api/folders/${folderTargetId}/products?sortBy=${sorting}&isAsc=${isAsc}`;
}
$('#product-container').empty();
$('#search-result-box').empty();
$('#pagination').pagination({
dataSource,
locator: 'content',
alias: {
pageNumber: 'page',
pageSize: 'size'
},
totalNumberLocator: (response) => {
return response.totalElements;
},
pageSize: 10,
showPrevious: true,
showNext: true,
ajax: {
beforeSend: function () {
$('#product-container').html('상품 불러오는 중...');
},
error(error, status, request) {
if (error.status === 403) {
$('html').html(error.responseText);
return;
}
logout();
}
},
callback: function (response, pagination) {
$('#product-container').empty();
for (let i = 0; i < response.length; i++) {
let product = response[i];
let tempHtml = addProductItem(product);
$('#product-container').append(tempHtml);
}
}
});
}
// Folder 관련 기능
function openFolder(folderId) {
folderTargetId = folderId;
$("button.product-folder").removeClass("folder-active");
if (!folderId) {
$("button#folder-all").addClass('folder-active');
} else {
$(`button[value='${folderId}']`).addClass('folder-active');
}
showProduct(folderId);
}
// 폴더 추가 팝업
function openAddFolderPopup() {
$('#container2').addClass('active');
}
// 폴더 Input 추가
function addFolderInput() {
$('#folders-input').append(
`<input type="text" class="folderToAdd" placeholder="추가할 폴더명">
<span onclick="closeFolderInput(this)" style="margin-right:5px">
<svg xmlns="http://www.w3.org/2000/svg" width="30px" fill="red" class="bi bi-x-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
</svg>
</span>
`
);
}
function closeFolderInput(folder) {
$(folder).prev().remove();
$(folder).next().remove();
$(folder).remove();
}
function addFolder() {
const folderNames = $('.folderToAdd').toArray().map(input => input.value);
try {
folderNames.forEach(name => {
if (name === '') {
alert('올바른 폴더명을 입력해주세요');
throw new Error("stop loop");
}
});
} catch (e) {
console.log(e);
return;
}
$.ajax({
type: "POST",
url: `/api/folders`,
contentType: "application/json",
data: JSON.stringify({
folderNames
})
}).done(function (data, textStatus, xhr) {
if(data !== '') {
alert("중복된 폴더입니다.");
return;
}
$('#container2').removeClass('active');
alert('성공적으로 등록되었습니다.');
window.location.reload();
})
.fail(function(xhr, textStatus, errorThrown) {
alert("중복된 폴더입니다.");
});
}
function addProductItem(product) {
const folders = product.productFolderList.map(folder =>
`
<span onclick="openFolder(${folder.id})">
#${folder.name}
</span>
`
);
return `<div class="product-card">
<div onclick="window.location.href='${product.link}'">
<div class="card-header">
<img src="${product.image}"
alt="">
</div>
<div class="card-body">
<div class="title">
${product.title}
</div>
<div class="lprice">
<span>${numberWithCommas(product.lprice)}</span>원
</div>
<div class="isgood ${product.lprice > product.myprice ? 'none' : ''}">
최저가
</div>
</div>
</div>
<div class="product-tags" style="margin-bottom: 20px;">
${folders}
<span onclick="addInputForProductToFolder(${product.id}, this)">
<svg xmlns="http://www.w3.org/2000/svg" width="30px" fill="currentColor" class="bi bi-folder-plus" viewBox="0 0 16 16">
<path d="M.5 3l.04.87a1.99 1.99 0 0 0-.342 1.311l.637 7A2 2 0 0 0 2.826 14H9v-1H2.826a1 1 0 0 1-.995-.91l-.637-7A1 1 0 0 1 2.19 4h11.62a1 1 0 0 1 .996 1.09L14.54 8h1.005l.256-2.819A2 2 0 0 0 13.81 3H9.828a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 6.172 1H2.5a2 2 0 0 0-2 2zm5.672-1a1 1 0 0 1 .707.293L7.586 3H2.19c-.24 0-.47.042-.684.12L1.5 2.98a1 1 0 0 1 1-.98h3.672z"/>
<path d="M13.5 10a.5.5 0 0 1 .5.5V12h1.5a.5.5 0 0 1 0 1H14v1.5a.5.5 0 0 1-1 0V13h-1.5a.5.5 0 0 1 0-1H13v-1.5a.5.5 0 0 1 .5-.5z"/>
</svg>
</span>
</div>
</div>`;
}
function addInputForProductToFolder(productId, button) {
$.ajax({
type: 'GET',
url: `/api/folders`,
success: function (folders) {
const options = folders.map(folder => `<option value="${folder.id}">${folder.name}</option>`)
const form = `
<span>
<form id="folder-select" method="post" autocomplete="off" action="/api/products/${productId}/folder">
<select name="folderId" form="folder-select">
${options}
</select>
<input type="submit" value="추가" style="padding: 5px; font-size: 12px; margin-left: 5px;">
</form>
</span>
`;
$(form).insertBefore(button);
$(button).remove();
$("#folder-select").on('submit', function (e) {
e.preventDefault();
$.ajax({
type: $(this).prop('method'),
url: $(this).prop('action'),
data: $(this).serialize(),
}).done(function (data, textStatus, xhr) {
if(data !== '') {
alert("중복된 폴더입니다.");
return;
}
alert('성공적으로 등록되었습니다.');
window.location.reload();
})
.fail(function(xhr, textStatus, errorThrown) {
alert("중복된 폴더입니다.");
});
});
},
error(error, status, request) {
logout();
}
});
}
function setMyprice() {
/**
* 1. id가 myprice 인 input 태그에서 값을 가져온다.
* 2. 만약 값을 입력하지 않았으면 alert를 띄우고 중단한다.
* 3. PUT /api/product/${targetId} 에 data를 전달한다.
* 주의) contentType: "application/json",
* data: JSON.stringify({myprice: myprice}),
* 빠뜨리지 말 것!
* 4. 모달을 종료한다. $('#container').removeClass('active');
* 5, 성공적으로 등록되었음을 알리는 alert를 띄운다.
* 6. 창을 새로고침한다. window.location.reload();
*/
// 1. id가 myprice 인 input 태그에서 값을 가져온다.
let myprice = $('#myprice').val();
// 2. 만약 값을 입력하지 않았으면 alert를 띄우고 중단한다.
if (myprice == '') {
alert('올바른 가격을 입력해주세요');
return;
}
// 3. PUT /api/product/${targetId} 에 data를 전달한다.
$.ajax({
type: 'PUT',
url: `/api/products/${targetId}`,
contentType: 'application/json',
data: JSON.stringify({myprice: myprice}),
success: function (response) {
// 4. 모달을 종료한다. $('#container').removeClass('active');
$('#container').removeClass('active');
// 5. 성공적으로 등록되었음을 알리는 alert를 띄운다.
alert('성공적으로 등록되었습니다.');
// 6. 창을 새로고침한다. window.location.reload();
window.location.reload();
},
error(error, status, request) {
logout();
}
})
}
function logout() {
// 토큰 삭제
Cookies.remove('Authorization', {path: '/'});
window.location.href = host + '/api/user/login-page';
}
function getToken() {
let auth = Cookies.get('Authorization');
if(auth === undefined) {
return '';
}
// kakao 로그인 사용한 경우 Bearer 추가
if(auth.indexOf('Bearer') === -1 && auth !== ''){
auth = 'Bearer ' + auth;
}
return auth;
}
인가코드 요청방법
https://kauth.kakao.com/oauth/authorize?client_id={REST_API_KEY}&redir
ect_uri={REDIRECT_URI}&response_type=code
{REDIRECT_URI} 이부분에 본인의 API 키 추가 -> 본인의 API키는 KAKAO developers 앱 키에 있음
카카오에서 보내주는 '인가코드' 처리
UserController
@GetMapping("/user/kakao/callback")
public String kakaoLogin(@RequestParam String code, HttpServletResponse response) throws JsonProcessingException {
String token = kakaoService.kakaoLogin(code);
Cookie cookie = new Cookie(JwtUtil.AUTHORIZATION_HEADER, token.substring(7));
cookie.setPath("/");
response.addCookie(cookie);
return "redirect:/";
}
1. 카카오에서 보내주는 '인가코드'를 받음 -> Controller
2. '인가코드'를 가지고 카카오 로그인 처리 -> Service
3. 로그인 성공 시 "/" 으로 redirect -> Controller
카카오 사용자 정보 가져오기
KaKaoService
import java.net.URI;
@Slf4j(topic = "KAKAO Login")
@Service
@RequiredArgsConstructor
public class KakaoService {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
private final RestTemplate restTemplate;
private final JwtUtil jwtUtil;
public String kakaoLogin(String code) throws JsonProcessingException {
// 1. "인가 코드"로 "액세스 토큰" 요청
String accessToken = getToken(code);
// 2. 토큰으로 카카오 API 호출 : "액세스 토큰"으로 "카카오 사용자 정보" 가져오기
KakaoUserInfoDto kakaoUserInfo = getKakaoUserInfo(accessToken);
return null;
}
RestTemplateConfig
package com.sparta.myselectshop.config;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
// RestTemplate 으로 외부 API 호출 시 일정 시간이 지나도 응답이 없을 때
// 무한 대기 상태 방지를 위해 강제 종료 설정
.setConnectTimeout(Duration.ofSeconds(5)) // 5초
.setReadTimeout(Duration.ofSeconds(5)) // 5초
.build();
}
}
KakaoUserInfoDto
package com.sparta.myselectshop.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class KakaoUserInfoDto {
private Long id;
private String nickname;
private String email;
public KakaoUserInfoDto(Long id, String nickname, String email) {
this.id = id;
this.nickname = nickname;
this.email = email;
}
}
"인가 코드"로 "액세스 토큰" 요청
private String getToken(String code) throws JsonProcessingException {
// 요청 URL 만들기
URI uri = UriComponentsBuilder
.fromUriString("https://kauth.kakao.com")
.path("/oauth/token")
.encode()
.build()
.toUri();
// HTTP Header 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// HTTP Body 생성
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", "c4210b8991869d817319c27bda94cd88");
body.add("redirect_uri", "http://localhost:8080/api/user/kakao/callback");
body.add("code", code);
RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
.post(uri)
.headers(headers)
.body(body);
// HTTP 요청 보내기
ResponseEntity<String> response = restTemplate.exchange(
requestEntity,
String.class
);
// HTTP 응답 (JSON) -> 액세스 토큰 파싱
JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
return jsonNode.get("access_token").asText();
}
KakaoOAuth2에 발급 받은 본인의 REST API 키 입력
body.add("client_id", "본인의 REST API키");
"액세스 토큰"으로 "카카오 사용자 정보" 가져오기
private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
// 요청 URL 만들기
URI uri = UriComponentsBuilder
.fromUriString("https://kapi.kakao.com")
.path("/v2/user/me")
.encode()
.build()
.toUri();
// HTTP Header 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + accessToken);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
.post(uri)
.headers(headers)
.body(new LinkedMultiValueMap<>());
// HTTP 요청 보내기
ResponseEntity<String> response = restTemplate.exchange(
requestEntity,
String.class
);
JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
Long id = jsonNode.get("id").asLong();
String nickname = jsonNode.get("properties")
.get("nickname").asText();
String email = jsonNode.get("kakao_account")
.get("email").asText();
log.info("카카오 사용자 정보: " + id + ", " + nickname + ", " + email);
return new KakaoUserInfoDto(id, nickname, email);
}
카카오 사용자 정보로 회원가입 구현
카카오로 부터 받은 사용자 정보
1. kakaoId
2. nickname
3. email
- 테이블 설계 옵션
- 카카오 User 를 위한 테이블 (ex. KakaoUser) 을 하나 더 만든다.
- 장점: 결합도가 낮아짐
- 성격이 다른 유저 별로 분리 → 차후 각 테이블의 변화에 서로 영향을 주지 않음
- 예) 카카오 사용자들만 profile_image 컬럼 추가해서 사용 가능
- 단점: 구현 난이도가 올라감
- 예) 관심상품 등록 시, 회원별로 다른 테이블을 참조해야 함
- 일반 회원: User - Product
- 카카오 회원: KakaoUser - Product
- 예) 관심상품 등록 시, 회원별로 다른 테이블을 참조해야 함
- 장점: 결합도가 낮아짐
- 카카오 User 를 위한 테이블 (ex. KakaoUser) 을 하나 더 만든다.
- 기존 회원 (User) 테이블에 카카오 User 추가
- 장점: 구현이 단순해짐
- 단점: 결합도가 높아짐
카카오 사용자 정보로 회원가입
User 테이블에 'kakaoId' 추가
entity -> User
package com.sparta.myselectshop.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
@Enumerated(value = EnumType.STRING)
private UserRoleEnum role;
private Long kakaoId;
public User(String username, String password, String email, UserRoleEnum role) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
}
public User(String username, String password, String email, UserRoleEnum role, Long kakaoId) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
this.kakaoId =kakaoId;
}
public User kakaoIdUpdate(Long kakaoId) {
this.kakaoId = kakaoId;
return this;
}
}
회원가입 처리
DB에 kakaoId를 가진 회원이 없을 경우에만 신규로 회원가입을 진행
public String kakaoLogin(String code, HttpServletResponse response) throws JsonProcessingException {
// 1. "인가 코드"로 "액세스 토큰" 요청
String accessToken = getToken(code);
// 2. 토큰으로 카카오 API 호출 : "액세스 토큰"으로 "카카오 사용자 정보" 가져오기
KakaoUserInfoDto kakaoUserInfo = getKakaoUserInfo(accessToken);
// 3. 필요시에 회원가입
User kakaoUser = registerKakaoUserIfNeeded(kakaoUserInfo);
// 4. JWT 토큰 반환
String createToken = jwtUtil.createToken(kakaoUser.getUsername(), kakaoUser.getRole());
return createToken;
}
회원가입 처리
private User registerKakaoUserIfNeeded(KakaoUserInfoDto kakaoUserInfo) {
// DB 에 중복된 Kakao Id 가 있는지 확인
Long kakaoId = kakaoUserInfo.getId();
User kakaoUser = userRepository.findByKakaoId(kakaoId).orElse(null);
if (kakaoUser == null) {
// 카카오 사용자 email 동일한 email 가진 회원이 있는지 확인
String kakaoEmail = kakaoUserInfo.getEmail();
User sameEmailUser = userRepository.findByEmail(kakaoEmail).orElse(null);
if (sameEmailUser != null) {
kakaoUser = sameEmailUser;
// 기존 회원정보에 카카오 Id 추가
kakaoUser = kakaoUser.kakaoIdUpdate(kakaoId);
} else {
// 신규 회원가입
// password: random UUID
String password = UUID.randomUUID().toString();
String encodedPassword = passwordEncoder.encode(password);
// email: kakao email
String email = kakaoUserInfo.getEmail();
kakaoUser = new User(kakaoUserInfo.getNickname(), encodedPassword, email, UserRoleEnum.USER, kakaoId);
}
userRepository.save(kakaoUser);
}
return kakaoUser;
}
UserRepository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
Optional<User> findByKakaoId(Long kakaoId);
}
'TIL' 카테고리의 다른 글
TIL 240614 Mockito, 통합테스트 (0) | 2024.06.17 |
---|---|
TIL 240613 단위 테스트 (1) | 2024.06.14 |
TIL 240611 3조 KPT 회고 (0) | 2024.06.12 |
TIL 240610 예외 처리 변경 (0) | 2024.06.11 |
TIL 240607 Spring Request Mapping (0) | 2024.06.10 |