[TIL] 230612 <Spring> 카카오 로그인
- -
[카카오 로그인]
소셜 로그인
- 모든 웹 사이트에서 회원가입 과정을 거치는 것은 매번 번거로운 회원가입 과정을 수행해야 할 뿐 아니라, 웹 사이트마다 다른 아이디와 비밀번호를 기억해야 하기 때문에 사용자에게 부담!
- 웹 사이트를 운영하는 측에서도 회원들의 개인정보를 지켜야하는 역할 부담!
바이러스와 백신의 관계 처럼, 발전하는 해킹 기술을 막기 위해 보안을 강화하는 노력이 지속적으로 필요하기 때문 - 이런 문제를 해결하기 위해 OAuth를 사용한 소셜 로그인이 등장.
OAuth
- 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준
- 사용자가 애플리케이션에게 모든 권한을 넘기지 않고 사용자 대신 서비스를 이용할 수 있게 해주는 HTTP 기반의 보안 프로토콜
- OAuth를 사용하는 서비스 제공자는 대표적으로 구글,페이스북,네이버,카카오가 있음
[카카오 사용자 정보 가져오기]
1. 카카오 서버에서 인가 코드 받기
myselectshop>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;
}
myselectshop>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>
🔺 인가코드 요청 방법
https://kauth.kakao.com/oauth/authorize?client_id={REST_API_KEY}&redirect_uri={REDIRECT_URI}&response_type=code
controller>UserController
- 카카오에서 보내주는 '인가코드' 처리
- 카카오에서 보내주는 '인가코드'를 받음 ⇒ Controller
💡 http://localhost:8080/api/user/kakao/callback?code=zAGhy36K0... - '인가코드'를 가지고 카카오 로그인 처리 ⇒ Service
- 로그인 성공 시 "/" 으로 redirect ⇒ Controller
- 카카오에서 보내주는 '인가코드'를 받음 ⇒ Controller
private final KakaoService kakaoService;
@GetMapping("/user/kakao/callback")
public String kakaoLogin(@RequestParam String code, HttpServletResponse response) throws JsonProcessingException {
String token = kakaoService.kakaoLogin(code); //JWT 반환되어 옴 -> 이걸 쿠키에 넣어주고 -> 이걸 response 객체에 넣어주면 됨
Cookie cookie = new Cookie(JwtUtil.AUTHORIZATION_HEADER, token.substring(7)); //쿠키 생성
cookie.setPath("/"); //메인페이지 경로
response.addCookie(cookie); //넣어주면 자동으로 브라우저의 JWT값이 set됨
return "redirect:/"; //메인페이지로 redirect 시킴
}
2. 카카오 사용자 정보 가져오기
service>KakaoService
@Slf4j(topic = "KAKAO Login")
@Service
@RequiredArgsConstructor
public class KakaoService {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
// 스프링 부트에서는 RestTemplate 바로 빈으로 등록하는게 아니라 RestTemplate빌더를 통해 생성할 수 있게 유도함
// 생성자에서 만드는게 아니라 따로 빈 수동으로 등록해서 관리
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;
}
1. "인가 코드"로 "액세스 토큰" 요청 ⇒ private String getToken(String code)
- KakaoOAuth2 에 발급 받은 본인의 REST API 키 입력
ㄴ body.add("client_id", "본인의 REST API키");
// 1. "인가 코드"로 "액세스 토큰" 요청
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", "본인의 REST API 키");
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
); //반환되는 String이 토큰 형태로 되어 있음
// HTTP 응답 (JSON) -> 액세스 토큰 파싱
JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
return jsonNode.get("access_token").asText();
}
2. "액세스 토큰"으로 "카카오 사용자 정보" 가져오기
⇒ private KakaoUserInfoDto getKakaoUserInfo(String accessToken)
// 2. 토큰으로 카카오 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<>()); //body에 따로 보내줄 필요 없어서 이렇게!
// 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") //properties에서 nickname 값 가져옴
.get("nickname").asText();
String email = jsonNode.get("kakao_account") //kakao_account에서 email 값 가져옴
.get("email").asText();
// {
// "id": 1632335751,
// "properties": {
// "nickname": "르탄이",
// "profile_image": "http://k.kakaocdn.net/...jpg",
// "thumbnail_image": "http://k.kakaocdn.net/...jpg"
// },
// "kakao_account": {
// "profile_needs_agreement": false,
// "profile": {
// "nickname": "르탄이",
// "thumbnail_image_url": "http://k.kakaocdn.net/...jpg",
// "profile_image_url": "http://k.kakaocdn.net/...jpg"
// },
// "has_email": true,
// "email_needs_agreement": false,
// "is_email_valid": true,
// "is_email_verified": true,
// "email": "letan@sparta.com"
// }
// }
log.info("카카오 사용자 정보: " + id + ", " + nickname + ", " + email);
return new KakaoUserInfoDto(id, nickname, email);
}
}
dto>KakaoUserInfoDto
@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;
}
}
config>RestTemplateConfig
- Spring Boot에서는 RestTemplate을 바로 Bean으로 등록하는 게 아니라 RestTemplate 빌더를 통해 생성할 수 있도록 유도함
- 생성자에서 만드는 것이 아니라 따로 Bean 수동으로 등록해서 관리
@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();
}
}
[카카오 사용자 정보로 회원가입 구현]
- 카카오로 부터 받은 사용자 정보
- kakaoId
- nickname
- email
- 테이블 설계 옵션
- 카카오 User 를 위한 테이블 (ex. KakaoUser) 을 하나 더 만든다.
- 장점: 결합도가 낮아짐
- 성격이 다른 유저 별로 분리 → 차후 각 테이블의 변화에 서로 영향을 주지 않음
- 예) 카카오 사용자들만 profile_image 컬럼 추가해서 사용 가능
- 단점: 구현 난이도가 올라감
- 예) 관심상품 등록 시, 회원별로 다른 테이블을 참조해야 함
- 일반 회원: User - Product
- 카카오 회원: KakaoUser - Product
- 예) 관심상품 등록 시, 회원별로 다른 테이블을 참조해야 함
- 장점: 결합도가 낮아짐
- 기존 회원 (User) 테이블에 카카오 User 추가 →사용하기로 결정!
- 장점: 구현이 단순해짐
- 단점: 결합도가 높아짐
- 폼 로그인을 통해 카카오 로그인 사용자의 username, password 를 입력해서 로그인한다면??
- 폼 로그인을 통해 카카오 로그인 사용자의 username, password 를 입력해서 로그인한다면??
- 카카오 User 를 위한 테이블 (ex. KakaoUser) 을 하나 더 만든다.
entity>User (기존 User 테이블에 'kakaoId' 추가)
@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;
}
}
service>KakaoService
@Slf4j(topic = "KAKAO Login")
@Service
@RequiredArgsConstructor
public class KakaoService {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
// 스프링 부트에서는 RestTemplate 바로 빈으로 등록하는게 아니라 RestTemplate빌더를 통해 생성할 수 있게 유도함
// 생성자에서 만드는게 아니라 따로 빈 수동으로 등록해서 관리
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);
// 3. 필요 시에 회원가입
User kakaoUser = registerKakaoUserIfNeeded(kakaoUserInfo);
// 4. JWT 반환
String createToken = jwtUtil.createToken(kakaoUser.getUsername(), kakaoUser.getRole());
return createToken;
}
- 패스워드를 UUID 로 설정한 이유 : 폼 로그인을 통해서 로그인되지 않도록!
// 3. 필요 시에 회원가입
private User registerKakaoUserIfNeeded(KakaoUserInfoDto kakaoUserInfo) {
// DB 에 중복된 Kakao Id 가 있는지 확인
Long kakaoId = kakaoUserInfo.getId();
User kakaoUser = userRepository.findByKakaoId(kakaoId).orElse(null);
if (kakaoUser == null) { //DB에 해당 카카오 아이디 없다면 회원가입 진행
// 카카오 사용자 email 동일한 email 가진 회원이 있는지 확인
String kakaoEmail = kakaoUserInfo.getEmail();
User sameEmailUser = userRepository.findByEmail(kakaoEmail).orElse(null);
if (sameEmailUser != null) { //DB에 이미 존재하는 이메일을 가진 회원이 있다면
kakaoUser = sameEmailUser; //같은 회원이라고 덮어씌우기
kakaoUser = kakaoUser.kakaoIdUpdate(kakaoId); //기존 회원정보에 카카오 Id 추가
} else { // 신규 회원가입
// password: random UUID
String password = UUID.randomUUID().toString(); //password는 UUID를 사용해서 랜덤으로 생성
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;
}
}
'TIL' 카테고리의 다른 글
[TIL] 230614 <Spring> Spring AOP (1) | 2024.06.14 |
---|---|
[TIL] 230613 <Spring> 테스트 (3) | 2024.06.13 |
[TIL] 230611 <Spring> 팀프로젝트 14조 - E거 I4아이가 KPT 회고 (2) | 2024.06.11 |
[TIL] 230610 <Spring> Swagger 연결 시 HTTP ERROR 403 해결 (0) | 2024.06.10 |
[TIL] 230607 <Spring> WebSecurityConfig, JwtAuthorizationFilter, JwtAuthenticationFilter, JwtUtil, UserDetailsServiceImpl (1) | 2024.06.07 |
소중한 공감 감사합니다