2024. 3. 23. 21:26ㆍ카테고리 없음
이번에는 "네이버 로그인 api" 를 사용하는 포스팅이다.
https://developers.naver.com/docs/login/api/api.md
여기에 설명되어 있는 것을 참조해서 진행하였다. 그리고, 자바스크립트로 구현하는 방식을 택하였다.
1단계 : 시작은, 위에 보이는 이미지의 네이버 로그인 버튼을 만드는 것에서부터 시작한다.
네이버 로고를 만들고, 버튼을 만드는 작업이 필요한 게 아니라, api documnet 에 제시된 대로,
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>네이버 로그인</title>
<script type="text/javascript" src="https://static.nid.naver.com/js/naverLogin_implicit-1.0.3.js" charset="utf-8"></script>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
</head>
<body>
<!-- 네이버 로그인 버튼 노출 영역 -->
<div id="naver_id_login"></div>
<!-- //네이버 로그인 버튼 노출 영역 -->
<script type="text/javascript">
var naver_id_login = new naver_id_login("YOUR_CLIENT_ID", "YOUR_CALLBACK_URL");
var state = naver_id_login.getUniqState();
naver_id_login.setButton("white", 2,40);
naver_id_login.setDomain("YOUR_SERVICE_URL");
naver_id_login.setState(state);
naver_id_login.setPopup();
naver_id_login.init_naver_id_login();
</script>
</html>
이 부분을 내 jsp 파일에 넣었다. 여기서, 사용자마다 달라야 하는 부분은, 3군데이다.
YOUR_CLIENT_ID , YOUR_CALLBACK_URL, YOUR_SERVICE_URL 이렇게 3군데 이다.
YOUR_CLIENT_ID 에는 네이버에 로그인하고, 내 애플리케이션을 등록하면, 네이버에서 발급하는 Client ID 를 써주면 된다.
아래 이미지에서 빨강부분에 적혀있는 것을 말한다.
그리고, YOUR_CALLBACK_URL 은 뭐냐면, 사용자가 네이버에 로그인하면, 네이버가 사용자에 대한 정보를 다시 돌려주는데, 그 정보를 받을 컨트롤러의 url을 써주면 된다. 나는 "http://localhost:8080/naver-callback" 이라고 써주었다. YOUR_CALLBACK_URL 에 대한 이해가 덜 된다면 포스팅을 읽다보면 이해가 갈거라고 확신한다.
마지막으로, YOUR_SERVICE_URL 에는 내 웹애플리케이션의 도메인을 써주면 된다.
내 프로젝트는 아직 배포하지 않았기 때문에, "http://localhost:8080" 이라고 써주었다.
그래서 위 버튼을 누르면 어떻게 되는가?
아래 페이지가 나오게 된다.
여기서 사용자가 자신의 회원정보를 입력하게 되면, 아까 설정해줬던, YOUR_CALLBACK_URL 에 써줬던 url(나는 "http://localhost:8080/naver-callback" 라고 써줬다고 했지.) 로 네이버 서버가 HTTP 메세지를 보내주게 된다.
그 HTTP 메세지를 받는 컨트롤러를 만들어줘야 한다. 아래와 같이 생겼다.
2단계 : 네이버 서버가 보내준 데이터를 받는 컨트롤러 만들기
@Slf4j
@Controller
@RequiredArgsConstructor
public class NaverLoginController {
@GetMapping("/naver-callback")
public String naverController(){
return "/login/naverLogin";
}
}
이 컨트롤러에서는 아무것도 안해줬고, 바로 naverLogin.jsp 로 이동시켰다.
3단계 : naverLogin.jsp
naverLogin.jsp 는 https://developers.naver.com/docs/login/api/api.md 여기에서 callback.html 이라고 했던 부분을 보고 만들었다. 안내되어 있는 callback.html 은 아래와 같이 되어 있다.
<!doctype html>
<html lang="ko">
<head>
<script type="text/javascript" src="https://static.nid.naver.com/js/naverLogin_implicit-1.0.3.js" charset="utf-8"></script>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
</head>
<body>
<script type="text/javascript">
var naver_id_login = new naver_id_login("YOUR_CLIENT_ID", "YOUR_CALLBACK_URL");
// 접근 토큰 값 출력
alert(naver_id_login.oauthParams.access_token);
// 네이버 사용자 프로필 조회
naver_id_login.get_naver_userprofile("naverSignInCallback()");
// 네이버 사용자 프로필 조회 이후 프로필 정보를 처리할 callback function
function naverSignInCallback() {
alert(naver_id_login.getProfileData('email'));
alert(naver_id_login.getProfileData('nickname'));
alert(naver_id_login.getProfileData('age'));
}
</script>
</body>
</html>
그대로 써놓고, 테스트를 해보니, alert 창이 3개 띄워졌는데, 그게 로그인한 내 네이버계정에 담겨있던, email(wowns590@naver.com), nickname(별명공개는.. 좀.. 그래서 뭐 예를 들어, 바보 라고 표현하겠습니다), age(나이) 가 담긴 alert창 3개였다.
그래서, 나는 아래와 같이, 받을 수 있는 데이터 중에 쓸만한 것들을 받아서 fetch 를 이용해서 /make-account 라는 url 로 post 요청을 보냈다.
naverLogin.jsp 의 전체 코드는 아래와 같다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<c:import url="/jsp/bootstrapconfig/index.jsp"/>
<html>
<head>
<!-- naver 로그인 관련 -->
<script type="text/javascript" src="https://static.nid.naver.com/js/naverLogin_implicit-1.0.3.js" charset="utf-8"></script>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
</head>
<body>
<script type="text/javascript">
var naver_id_login = new naver_id_login("akLY3ixqz148IBJVFcFD", "http://localhost:8080/naver-callback");
// 네이버 사용자 프로필 조회
naver_id_login.get_naver_userprofile("naverSignInCallback()");
// 네이버 사용자 프로필 조회 이후 프로필 정보를 처리할 callback function
function naverSignInCallback() {
let name = naver_id_login.getProfileData('name');
let email = naver_id_login.getProfileData('email');
let id = naver_id_login.getProfileData('id');
fetch('/make-account', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
'name': name,
'email': email,
'id' : id,
}),
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.text();
})
.then(data => {
if(data == 'createAccount' || data == 'alreadyExistsByNaver'){
window.location.href = '/';
} else if (data == 'alreadyExistByWordCharger') {
alert('회원가입 이력이 확인되었습니다. 해당 아이디로 로그인해주세요.');
window.location.href = '/login-form';
}
})
.catch(error => console.error('There has been a problem with your fetch operation:', error));
}
</script>
</body>
</html>
현재 나는 name email id 얻어온 다음, 그 데이터들을 fetch 로 post 요청을 보내고 있는데,
이외에도 다양한 데이터를 받을 수 있다.
그리고 fetch 문의 .then 구문에서 받아온 문자열이 무엇인지에 따라, 다른 처리를 하고 있는데, 이는 아래 있는 fetch 로 보낸 post 요청을 받는 컨트롤러인 /make-account 라는 url 을 매핑하는 컨트롤러에 대해 이해해야 이해가 될 것이다.
4단계. make-account 라는 url 을 매핑하는 컨트롤러
@PostMapping("/make-account")
@ResponseBody
public String makeAccount(@RequestBody NaverServerSendMeDataDTO naverServerSendMeDataDTO, HttpServletRequest request) {
String name = naverServerSendMeDataDTO.getName();
String email = naverServerSendMeDataDTO.getEmail();
String id = naverServerSendMeDataDTO.getId();
// 회원가입한 적이 있는가?
// 이를 판단하기 위해서는 네이버 서버가 준 데이터 중 이메일 부분을 가지고 내 db 테이블 중 email 테이블에 일치하는 행이 있다면
// 가입한 이력이 있는 회원이라고 판단할 것이다.
// 그런데, 밑에서 보면 알겠지만, 네이버로그인을 함으로써 자동으로 회원가입되는 경우에는 email 테이블의 행이 안들어가기 때문에,
// 이메일만으로는 회원가입이력이 있는지 여부를 판단하기에 부족하다.
// 네이버로 로그인함으로써 자동회원가입한 경우를 판단하기 위해서, 네이버로 로그인함으로써 자동회원가입할 때에,
// member 테이블의 user_id 컬럼값을 "nv_" + email의 앞부분 으로 하기로 했기 때문에,
// member 테이블에서 user_id 컬럼값이 "nv_" + email 앞부분인 행이 없는 경우에야
// 비로소, 회원가입이력이 없다고 판단할 것이다.
// naver 서버가 준 데이터 중 email 값은 지금 "wowns590@naver.com" 이렇게 들어와있는데,
// 내 email 테이블에는 email 컬럼으로 wowns590 이렇게만 들어가도록 설계했기 때문에,
// 지금 email 변수에 들어있는 값을 @ 를 기준으로 앞에 부분인 wowns590 만 잘라서
// email 테이블의 email 컬럼과 비교를 진행해야 한다.
int atIndex = email.indexOf("@"); // wowns590@naver.com 이라는 문자열에서 @가 몇번째 위치해있는지 확인.
String emailFragment = email.substring(0, atIndex); // substring 을 이용해서 잘라냄.
String domain = email.substring(atIndex + 1); // @는 제외한 @ 뒤의 부분을 얻기 위해 atIndex에 1을 더함
String beId = "nv_" + emailFragment; // 밑에 코드에 여러군데에서 사용됨.
// email 테이블에서 email 컬럼값이,
// emailFragment 이라는 변수와 동일한 행을 조회한다.
EmailDTO emailDTO = emailMapper.emailCountByEmail(emailFragment);
boolean flag = false;
if (emailDTO != null) { // 만약 조회된 행이 있다면, 그 행의 domain 컬럼 값이 domain 변수와 동일한지 체크하고,
// domain 컬럼까지 동일하다면, flag 를 true 로 바꾸도록 함.
if(emailDTO.getDomain().equals(domain)){
flag = true;
}
}
MemberJoinDTO member = memberMapper.findMemberById(beId);
if (member != null) { // 만약 네이버로 로그인함으로써 자동으로 회원가입된 경우, flag 가 true가 되도록 함.
flag = true;
}
// 일단, flag 의 값이 true 인지 false 인지에 따라 나눠야 한다.
// flag 값이 여전히 false 라면,
// 우리 웹사이트에 직접 회원가입을 하지도 않았고, 네이버로 로그인해서 자동으로 회원가입된적도 없다는 의미거든?
// 그렇다면, 그냥 네이버서버가 준 데이터로 회원가입을 진행하면 된다.
// 이때, db 중 naver_member 라는 테이블에도 행을 추가해줘야 한다. <- 용도는 뒤에 설명되어 있다.
// 이 테이블의 컬럼 중 핵심은
// 네이버 서버가 네이버 계정마다 부여한 고유한 키인 현재 id 라는 변수에 담겨있는 값을
// 넣은 컬럼인 naver_id 라는 이름의 컬럼이다.
if (flag == false) {
// member 테이블에 행 넣기.
Integer sequence = memberMapper.selectNextSequenceValue(); // id 컬럼 값
String password = id; // password 컬럼 값 : 네이버서버가 네이버계정마다 부여한 고유한 값
String userName = name; // user_name 컬럼 값 : 네이버서버가 준 데이터 중 name 값
memberMapper.insertMember(sequence, beId, id, name); // 삽입.
// naver_member 테이블에도 행을 넣을 것이다.
// naver_member 테이블에는 총 3개의 컬럼이 있다.
// id : pk 로서, 시퀀스값을 넣어줄 컬럼.
// member_id : 방금 member 테이블에 insert 쿼리 실행할 때 썻던 member 테이블의 id 컬럼(pk) 값을 참조하는 외래키로 설정.
// naver_id : 네이버 서버가 네이버계정마다 부여한 고유한 값을 담을 컬럼.
Integer nextSequenceValue = naverMemberMapper.selectNextSequenceValue(); // id 컬럼 값
naverMemberMapper.insertRow(nextSequenceValue, sequence, id);
//세션 설정
MemberJoinDTO findedMember = memberMapper.findMemberById(beId);
HttpSession session = request.getSession(true);
session.setAttribute("loginedMember", findedMember);
return "createAccount";
} else{
// flag 가 true 라면,
// 우리 웹사이트에 직접 회원가입을 했든, 혹은 네이버로 로그인함으로써 자동으로 회원가입을 했든
// 우리 웹사이트에 회원이라는 소리이다.
// 이 경우 2가지로 분류해야 한다.
// 1. 우리 웹사이트에 직접 회원가입을 한 경우.
// 2. 네이버로 로그인함으로써 자동으로 회원가입을 한 경우.
// 1번인지 2번인지는 어떻게 판단할 것인가?
// 네이버가 네이버계정마다 부여한 고유한 값으로 db 테이블 중 naver_member 라는 테이블에서
// 행을 조회한다. 만약, 조회된 행의 개수가 0이면, 우리 웹사이트에 직접 회원가입을 한 경우이고,
// 1이상이면 네이버로 로그인함으로써 자동으로 회원가입된 경우라고 판단할 수 있다.
// 1. 우리 웹사이트에 직접 회원가입을 한 경우 에는 새로운 계정을 만들지 않는다.
// 그리고, alreadyExistByWordCharger 라는 문자열을 되돌려 줄것이다.
// 그러면, fetch 구문에서 alreadyExistByWordCharger 라는 값이 온 경우,
// alert('회원가입한 이력이 확인되었습니다. 그 아이디로 로그인해주세요.') 라고 띄워주고, loginForm.jsp 이 보여지도록 할 것.
// 2. 네이버로 로그인함으로써 자동으로 회원가입을 한 경우 에는
// 세션 설정을 해주고, alreadyExistsByNaver 라는 문자열을 되돌려 줄것이다.
// 그러면, fetch 구문에서 alreadyExistsByNaver 라는 값이 온 경우,
// window.location.href="" 를 이용해서 / 라는 url 을 매핑하는 컨트롤러로 get 요청을 하게 할 것이다.
Integer rowCount = naverMemberMapper.rowCount(id);
if (rowCount == 0) {
// 1. 우리 웹사이트에 직접 회원가입을 한 경우.
return "alreadyExistByWordCharger";
} else{
// 2. 네이버로 로그인함으로써 자동으로 회원가입을 한 경우, 세션 설정.
MemberJoinDTO findedMember = memberMapper.findMemberById(beId); //네이버로 로그인함으로써 자동으로 회원가입할 때, user_id 컬럼값으로 beId 변수를 썻기 때문에 beId를 통해 memeber 테이블의 행을 조회함.
HttpSession session = request.getSession(true);
session.setAttribute("loginedMember", findedMember);
// alreadyExistsByNaver 리턴.
return "alreadyExistsByNaver";
}
}
}
대단히 복잡해보이지만, 큰 흐름만 파악한다면 그리 복잡하지도 않다. 큰 흐름은 아래와 같다.
1. 회원가입한 이력이 있는가?
2. 없다면 -> 네이버서버가 보내준 데이터를 가지고 간단하게 회원가입을 진행해버린다.
있다면 -> 3. 으로
3. 만약 내 홈페이지에서 직접 정보를 입력해서 회원가입한 경우에는 "회원가입한 이력이 있으니 그 아이디로 로그인해달라고 안내한다"
만약 네이버 아이디로 로그인함으로써 회원가입이 자동으로 진행된 경우에는 "세션을 설정해주고 / 으로 get 요청보내서 사용자에게 네이버아이디로 로그인한듯한 효과를 느끼도록 해준다"
회원가입한 이력이 있는 경우, 왜 사용자에게 원래 내 홈페이지에서 필요한 다른 정보(이메일, 주소 등) 을 더 입력받아서 완벽한 계정을 만들도록 하지 않았는가? 그건 https://developers.naver.com/docs/login/devguide/devguide.md 이 글의 의견에 동의했기 때문이다.
이제 다시 naverLogin.jsp 의 fetch 구문 중 then 구문을 보면, 이해가 될 것이다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<c:import url="/jsp/bootstrapconfig/index.jsp"/>
<html>
<head>
<!-- naver 로그인 관련 -->
<script type="text/javascript" src="https://static.nid.naver.com/js/naverLogin_implicit-1.0.3.js" charset="utf-8"></script>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
</head>
<body>
<script type="text/javascript">
var naver_id_login = new naver_id_login("akLY3ixqz148IBJVFcFD", "http://localhost:8080/naver-callback");
// 네이버 사용자 프로필 조회
naver_id_login.get_naver_userprofile("naverSignInCallback()");
// 네이버 사용자 프로필 조회 이후 프로필 정보를 처리할 callback function
function naverSignInCallback() {
let name = naver_id_login.getProfileData('name');
let email = naver_id_login.getProfileData('email');
let id = naver_id_login.getProfileData('id');
fetch('/make-account', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
'name': name,
'email': email,
'id' : id,
}),
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.text();
})
.then(data => {
if(data == 'createAccount' || data == 'alreadyExistsByNaver'){
window.location.href = '/';
} else if (data == 'alreadyExistByWordCharger') {
alert('회원가입 이력이 확인되었습니다. 해당 아이디로 로그인해주세요.');
window.location.href = '/login-form';
}
})
.catch(error => console.error('There has been a problem with your fetch operation:', error));
}
</script>
</body>
</html>
그리고, 로그인 기능 구현 1 에서 나왔었는데,
localhost:8080/ 이라는 url 을 매핑하는 컨트롤러는 HTTP요청을 보낸 그 사용자와 연결된 session 객체에 loginedMember 라는 키를 가진 데이터가 있는 경우에만 loginedHome.jsp 파일로 이동시키는 컨트롤러였다.
그리고 localhost:8080/login-form 이라는 url을 매핑하는 컨트롤러는 단순히 loginForm.jsp 파일로 이동시키는 컨트롤러였다.
참고로, loginedHome.jsp 는 아래와 같이 생겼고,
loginForm.jsp 는 아래와 같이 생겼다.
그런데, 지금 보면, 사용자가 직접 내 프로젝트에서 정보를 입력하여 회원가입한 이력이 있는 경우, 네이버서버가 준 데이터로 회원가입을 진행시키지 않고, 사용자에게 회원가입이력이 있으니 그 아이디로 로그인하라고 안내하고 있는데, 왜 그렇게 했는가?
만약에, 직접 정보를 입력하여 회원가입하고 결제를 진행했다고 가정해보자. 다음에 그 회원이 로그인할 때 회원가입을 진행했었는지 아니면 네이버 로그인으로 대체했는지 기억못하고, 결제했는지도 기억못하는 경우라고 가정해보자.
근데, 이번에는 네이버로그인으로 로그인을 진행했다고 해보자. 만약 이때 "회원가입한 이력이 있으니 그 아이디로 로그인하라" 는 안내를 하지 않고 그냥 새로운 계정을 하나 더 만들게 된다면, 사용자는 자신이 결제한지도 기억못하기 때문에 또 결제를 진행하게 될 위험이 있다.
그래서, 직접 정보를 입력하여 회원가입한 이력이 있는 경우, 네이버 로그인으로 회원가입을 진행시키지 않고 "회원가입한 이력이 있으니 그 아이디로 로그인하라" 라고 안내하도록 설계하였다.
그렇다면, 반대로 네이버로그인으로 회원가입이 된 사용자인데, 직접 정보를 입력하여 회원가입하려는 경우에도 "네이버 로그인으로 회원계정이 생성되었으니, 네이버 로그인으로 로그인해달라" 라고 안내해줘야 하겠지.
그래서 아래와 같이 직접 정보를 입력하여 회원가입하는 컨트롤러에서 이를 처리하였다.
@PostMapping("/Join-form")
public String postJoinFormControllerMethod (@Valid @ModelAttribute MemberJoinDTO memberJoinDTO, BindingResult bindingResult){
//유효성 검사
if (memberJoinDTO.getUserId().equals("") ) {
bindingResult.rejectValue("userId", null,"아이디를 입력 해주세요");
}
if (memberJoinDTO.getPassword().equals("")) {
bindingResult.rejectValue("password", null, "비밀번호를 입력해주세요");
}
if (memberJoinDTO.getUserName().equals("")) {
bindingResult.rejectValue("userName", null, "이름을 입력해주세요");
}
if (memberJoinDTO.getZipCode().equals("")|| memberJoinDTO.getStreetAddress().equals("")|| memberJoinDTO.getAddress().equals("")) {
bindingResult.rejectValue("zipCode", null, "우편번호를 찾기를 통해 주소를 찾아주세요.");
}
if (memberJoinDTO.getPhoneNumberStart().equals("") || memberJoinDTO.getPhoneNumberMiddle().equals("") || memberJoinDTO.getPhoneNumberEnd().equals("")) {
bindingResult.rejectValue("phoneNumberStart", null, "전화번호를 입력해주세요");
}
if (bindingResult.hasErrors()) {
log.info("bindingResult = {}", bindingResult);
return "/login/joinForm";
}
String email = memberJoinDTO.getEmail();
MemberJoinDTO findMember = memberMapper.findMemberById("nv_" + email);
if (findMember != null) {
return "redirect:/login-form?goToNaverLogin=goToNaverLogin";
}
//checkbox value : on => 1 , null => 0 변환
MemberJoinDTO changedMemberJoinDTO = joinService.onAndNullChange(memberJoinDTO);
// 비밀번호 해싱
String rawPassword = changedMemberJoinDTO.getPassword();
String encodedPassword = passwordEncoder.encode(rawPassword);
changedMemberJoinDTO.setPassword(encodedPassword);
//insert 진행 서비스
insertMemberService.insertMember(changedMemberJoinDTO);
return "/home/home";
}
String email = memberJoinDTO.getEmail();
MemberJoinDTO findMember = memberMapper.findMemberById("nv_" + email);
if (findMember != null) {
return "redirect:/login-form?goToNaverLogin=goToNaverLogin";
}
이 부분인데, 네이버로 로그인해서 계정이 생성되는 경우에는 member 테이블의 user_id 컬럼 값으로 "nv_" + 이메일의 앞부분 이 들어가기 때문에, 이를 이용해서 만약 member 테이블에 "nv_" + 사용자가 직접 회원가입을 진행하면서 입력한 이메일 을 user_id 컬럼의 값으로 하는 행이 존재한다면, /login-form 으로 리다이렉트하도록 하면서 쿼리스트링으로 goToNaverLogin=goToNaverLogin 이라는 데이터를 담아주었다.
그리고, /login-form 이라는 get요청(HTTP요청)을 받는 컨트롤러도 아래와 같이 좀 수정했다.
@GetMapping("/login-form")
public String getLoginFormControllerMethod(Model model, @RequestParam(required = false) String goToNaverLogin) {
if (goToNaverLogin != null) {
model.addAttribute("goToNaverLogin", goToNaverLogin);
}
model.addAttribute("loginDTO", new LoginDTO());
return "/login/loginForm";
}
만약, goToNaverLogin 이라는 키로 쿼리스트링이 온 경우, model 에 goToNaverLogin 라는 키로 데이터가 담기도록 해주었다.
그리고 이 컨트롤러로 인해 이동되는 loginForm.jsp 파일에서 아래와 같은 코드를 두어, 만약 model 에 goToNaverLogin 이라는 키를 가진 데이터가 담겨져 있다면, "네이버로그인으로 생성된 계정이 확인되었습니다. 네이버로 로그인 해주세요." 라는 텍스트를 담은 alert창을 띄워주도록 하였다.
<script>
alert('네이버로그인으로 생성된 계정이 확인되었습니다. 네이버로 로그인 해주세요.');
</script>
</c:if>
끝.