today_is
[ spring 프로젝트 ] 네이버 로그인 api (1) 본문
사실, 기획에는 로그인 api 를 구현할 계획이 없었는데
내 파트를 빠르게 마무리했기 때문에, 프로젝트 기능성을 높이기 위해서 추가할 예정이다 !
네이버 로그인을 이용하면 사용자 입장에서 장점이 몇가지 있다.
1) 네이버에 로그인된 정보를 통해, 우리 사이트에 수월하게 회원가입할 수 있다.
2) 우리 사이트의 회원과 네이버 계정을 처음 한번만 연동해두면,
네이버 계정의 세션이 로그인 되어있다면, 우리 사이트에서도 로그인이 된다.
3) 복잡한 인증이 필요없다
: 물론 우리 사이트는 인증을 크게 다루지 않아서,, 적용되지 않는 항목이지만
만약 회원가입시 복잡한 인증이 필요한 사이트라면
네이버 로그인만으로도 사용자 인증이 가능할 것으로 예상된다
로그인 API 구현을 통해, 추가할 3가지 기능
1 ) 간편 회원가입
2 ) 간편 로그인
3 ) 계정연동
결과
login 페이지에서
네이버 아이디로 로그인 / 회원가입하기 버튼을 누르면 네이버 로그인 창이 뜬다
간편회원가입 기능
네이버 로그인된 계정과 일치하는 아이디 , 이메일이 없다면 회원가입 폼으로 넘어옴
이때, 이메일과 닉네임은 네이버에서 받아온 값이다
대신 수정을 가능하게 하였음 (value 를 이용하여 값만 받아옴. readonly 사용 X)
이메일이 일치하는 계정이 있으면
계정 연동 시킬지 제안함.
네, 지금 연결할게요 -> DB 에 naver_id 컬럼을 update
다음에 연결할게요 -> 대문페이지로 이동
계정 연동후 세션을 이용한 로그인 저장하는 부분은 아직 미구현 !!
현재는 개발용으로 api 를 사용하는것이기 때문에
내 계정이 아닌, 다른 계정으로 로그인을 시도해보려면
내가 신청한 api에 아이디를 추가해줘야한다.
개발용은 최대 5개까지 가능하다 (api 신청한 본인계정은 제외)
우리 조원한테 로그인해달라고 부탁했는데
드디어 200이 떴다 !! 성공 ㅎㅎ
DB에도 security 를 이용하여 BCryptPasswordEncoder 형식의 비밀번호가 들어가는것을 확인할 수 있다
: $ 가 들어갔으니, 변경완료
준비 단계
1) 네이버 개발자 센터로 가서 ( https://developers.naver.com/main/ )
2) 서비스 api 누르세요
3) (스크롤 조금 내리면) 오픈 api 이용 신청
4) application 등록
(1) 애플리케이션 이름은 네이버 로그인창이 뜰때 사용될 이름입니다
(2) 사용 api 에는 "네이버 로그인" 을 선택해주세요.
사용자에게서 어떤 정보를 받아올지 정하시면 됩니다
저는 회원이름, 연락처 이메일주소, 별명만 받아왔어요.
왜냐하면 저희 사이트에 member 테이블의 컬럼 내용중에서 필요한 데이터만 받아왔기 때문입니다.
사용자의 정보를 받아와서 어떠한 기능을 추가로 제공해주는게 아니라면,
굳이 의미없는 정보는 안받아와도 될 것 같아요.
예시로 성별에 따라서 다른 정보를 준다던지..? 그런 부가적인 기능이 없다면
회원을 구분지을 수 있는 데이터만 받아왔어요.
(3) 다음으로 환경은 "PC 웹" 으로 해주세요.
저희 사이트는 웹이기 때문입니다 !
이 부분 굉장히 중요합니다
(4) 서비스 URL, 콜백 URL
일단 저는 서비스 url 은
localhost:8080 을 사용하고 있기 때문에
http://localhost:8080 으로 해뒀습니다.
callback URL 이 제일 중요해요
사용자가 로그인하고 나서 돌아갈 페이지 링크를 적어야합니다
http://localhost:8080/프로젝트명/돌아갈 페이지
저는 이렇게 지정했어요.
네이버에게 요청을 하고,
그 요청에 대한 응답을 받아서
우리가 사용하는 것이기 때문에 콜백주소가 올바르게 되어있지 않으면 계~~ 속 에러를 맞이하게 됩니다 ^^
마치 저처럼요 ㅎㅎ
설정 파일
pom.xml
: 만약, 스프링 시큐리티를 사용하지 않을거면 빼도 됨
... 중략 ...
<!-- 스프링 시큐리티 -->
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-core -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>${spring-security-version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring-security-version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring-security-version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
<version>${spring-security-version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
<version>${spring-security-version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>${spring-security-version}</version>
</dependency>
<!-- 네이버 로그인 oauth 2.0 구현체 -->
<!-- https://mvnrepository.com/artifact/com.github.scribejava/scribejava-apis -->
<dependency>
<groupId>com.github.scribejava</groupId>
<artifactId>scribejava-apis</artifactId>
<version>8.3.3</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.scribejava/scribejava-core -->
<dependency>
<groupId>com.github.scribejava</groupId>
<artifactId>scribejava-core</artifactId>
<version>8.3.3</version>
</dependency>
<!-- JSON 파싱 -->
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
</dependency>
<!-- slf4j -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.4</version>
</dependency>
<!-- log4j(2)-core -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.4</version>
<exclusions>
<exclusion>
<artifactId>log4j-api</artifactId>
<groupId>org.apache.logging.log4j</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- log4jdbc-log4j2 -->
<dependency>
<groupId>org.bgee.log4jdbc-log4j2</groupId>
<artifactId>log4jdbc-log4j2-jdbc4</artifactId>
<version>1.16</version>
</dependency>
</dependencies>
security-context.xml 만들기 (필수 X)
: src -> main -> webapp -> WEB-INF -> spring 안에 만들면 됩니다.
spring 폴더를 오른쪽 마우스로 클릭
new -> Spring Bean Configuration File -> security-context.xml 파일 만들기
-> 파일에 Namespaces 에 들어가서 beans 와 security 가 체크박스로 체크 되어있는지 확인
web.xml
: security-context.xml 의 경로도 지정해주어야함 !!
(param-value 확인하기)
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="4.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_4_0.xsd">
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/root-context.xml
/WEB-INF/spring/security-context.xml
</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- encoding Filter -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- spring-security filter -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Processes application requests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<error-page>
<error-code>403</error-code>
<location>/WEB-INF/views/error-403.jsp</location>
</error-page>
</web-app>
LoginController
package com.itbank.controller;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
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.PostMapping;
import org.springframework.web.servlet.ModelAndView;
import com.itbank.model.MemberDTO;
import com.itbank.model.OauthUserDTO;
import com.itbank.oauth2.NaverLogin;
import com.itbank.oauth2.OauthLogin;
import com.itbank.service.MemberService;
@Controller
public class LoginController {
@Autowired private NaverLogin naverLogin;
@Autowired private MemberService memberService;
@GetMapping("/oauth2_intergrated")
public ModelAndView oauth2_intergrated(String code, String state, HttpSession session) throws IOException, InterruptedException, ExecutionException {
ModelAndView mav = new ModelAndView("oauth2");
OauthLogin oauthLogin = null;
String provider = (String) session.getAttribute("provider");
switch(provider) {
case "NAVER": oauthLogin = naverLogin; break;
}
// 2) 로그인 정보로 액세스 토큰을 받아온다
String accessToken = oauthLogin.getAccessToken(session, code, state);
System.out.println("accessToken : " + accessToken);
// 3) 액세스 토큰을 이용하여 사용자의 프로필 정보를 받아온다
String apiResult = oauthLogin.getUserProfile(accessToken);
System.out.println("apiResult : " + apiResult);
// 4) ObjectMapper 를 이용하여 자바 객체 타입으로 변환한다
OauthUserDTO oauthUser = oauthLogin.getOauthUser(apiResult);
oauthUser.setProvider(provider);
session.setAttribute("oauthUser", oauthUser); // 사용자 정보를 세션에 저장
// 5) 인증 정보를 이용하여 DB에서 먼저 조회한다
MemberDTO dtoById = memberService.getMemberById(oauthUser);
MemberDTO dtoByEmail = memberService.getMemberByEmail(oauthUser);
// 6) DB조회 여부에 따라, 회원가입, 기존 계정 연동, 로그인으로 분기하여 진행한다
if(dtoById == null) { // 연동된 계정이 없음
// 이메일이 일치하는 계정이 있음 -> 계정 연동 제안
// 이메일이 일치하는 계정도 없음 -> 회원가입
mav.addObject("location", dtoByEmail != null ? "/updateId" : "/member/joinBasicMember");
} else { // 연동된 계정 정보가 있음 -> 사용자 세션 설정
mav.addObject("location", "/"); // 메인 페이지로 리다이렉트
}
return mav;
}
@GetMapping("/oauth2_naver")
public ModelAndView oauth2_naver(String code, String state, HttpSession session) throws IOException, InterruptedException, ExecutionException {
session.setAttribute("provider", "NAVER");
return oauth2_intergrated(code, state, session);
}
@GetMapping("/updateId")
public void updateNaverId() {}
@PostMapping("/updateId")
public String updateNaverId(HttpSession session) {
OauthUserDTO oauthUser = (OauthUserDTO)session.getAttribute("oauthUser");
int row = memberService.updateId(oauthUser);
System.out.println(row != 0 ? "성공" : "실패");
return "redirect:/";
}
}
NaverLogin.java
: 로그인 api 와 관련된 정보를 기입
원래는 dto = objectMapper.treeToValue(response, OauthUserDTO.class); 을 작성하여
응답 받은 값들을 전부 받아왔었는데
네이버 개발자센터에서 api 에서 받아올 값을 일부 수정한뒤에 프로젝트를 바로 실행했더니
체크 해제했던 gender 값을 계속 받아와서 에러가 났다
(응답값을 저장해둘 OauthDTO에는 gender 필드가 없기 때문)
결국 DB 에 있는 꼭 필요한 내용들만 받아오고자,
응답 값을 get 하여, DTO setter 에 저장하였다.
이 과정을 통하면 필드명이 일치하는 값들만 받아오기 때문에
에러가 날 일이 없다
아마도, 네이버 개발자센터에서 처리한 내용들이 즉각 반영되지 않아서 생긴 문제인 것 같다.
package com.itbank.oauth2;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.scribejava.core.builder.ServiceBuilder;
import com.github.scribejava.core.model.OAuth2AccessToken;
import com.github.scribejava.core.model.OAuthRequest;
import com.github.scribejava.core.model.Response;
import com.github.scribejava.core.model.Verb;
import com.github.scribejava.core.oauth.OAuth20Service;
import com.itbank.model.OauthUserDTO;
@Component
public class NaverLogin implements OauthLogin {
private static final String CLIENT_ID = "";
private static final String CLIENT_SECRET = "";
private static final String REDIRECT_URL = "";
private static final String SESSION_STATE = "naver_oauth_state";
private static final String PROFILE_API_URL = "https://openapi.naver.com/v1/nid/me";
private ObjectMapper objectMapper = new ObjectMapper();
// 네아로 인증 URL 생성
@Override
public String getAuthorizationUrl(HttpSession session) {
String state = UUID.randomUUID().toString().replace("-", "");
session.setAttribute(SESSION_STATE, state);
OAuth20Service oauthService = new ServiceBuilder(CLIENT_ID)
.apiSecret(CLIENT_SECRET)
.callback(REDIRECT_URL)
.debug()
.build(NaverLoginAPI.getInstance());
return oauthService.getAuthorizationUrl(state);
}
// 네아로 콜백 및 액세스 토큰 획득
@Override
public String getAccessToken(HttpSession session, String code, String state) throws IOException, InterruptedException, ExecutionException {
String sessionState = (String) session.getAttribute(SESSION_STATE);
if(StringUtils.pathEquals(sessionState, state)) {
OAuth20Service oauthService = new ServiceBuilder(CLIENT_ID)
.apiSecret(CLIENT_SECRET)
.callback(REDIRECT_URL)
.debug()
.build(NaverLoginAPI.getInstance());
OAuth2AccessToken accessToken = oauthService.getAccessToken(code);
return accessToken.getAccessToken();
}
return null;
}
@Override
public String getUserProfile(String oauthToken) throws InterruptedException, ExecutionException, IOException {
OAuth20Service oauthService = new ServiceBuilder(CLIENT_ID)
.apiSecret(CLIENT_SECRET)
.callback(REDIRECT_URL)
.debug()
.build(NaverLoginAPI.getInstance());
OAuthRequest request = new OAuthRequest(Verb.GET, PROFILE_API_URL);
request.addQuerystringParameter("CLIENT_ID", CLIENT_ID);
request.addQuerystringParameter("CLIENT_SECRET", CLIENT_SECRET);
request.addQuerystringParameter("ACCESS_TOKEN", oauthToken);
request.addQuerystringParameter("GRANT_TYPE", "GET");
request.addQuerystringParameter("SERVICE_PROVIDER", "NAVER");
oauthService.signRequest(oauthToken, request);
Response response = oauthService.execute(request);
return response.getBody();
}
// getOauthUser : 네이버에서 받은 json 객체 중에서 내가 원하는 값만 저장
@Override
public OauthUserDTO getOauthUser(String apiResult) {
OauthUserDTO dto = null;
try {
JsonNode node = objectMapper.readTree(apiResult);
System.out.println(node);
JsonNode response = node.findValue("response");
System.out.println(response);
// dto = objectMapper.treeToValue(response, OauthUserDTO.class);
dto = new OauthUserDTO();
dto.setId(response.get("id").asText());
dto.setNickname(response.get("nickname").asText());
dto.setEmail(response.get("email").asText());
dto.setName(response.get("name").asText());
return dto;
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return dto;
}
}
NaverLoginAPI.java
package com.itbank.oauth2;
import com.github.scribejava.core.builder.api.DefaultApi20;
public class NaverLoginAPI extends DefaultApi20 {
protected NaverLoginAPI() {}
private static NaverLoginAPI instance = new NaverLoginAPI();
public static NaverLoginAPI getInstance() {
return instance;
}
@Override
public String getAccessTokenEndpoint() { // 사용자 접근 권한
return "https://nid.naver.com/oauth2.0/token?grant_type=authorization_code";
}
@Override
protected String getAuthorizationBaseUrl() { // 사용자 동의란
return "https://nid.naver.com/oauth2.0/authorize";
}
}
Oauth2.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ include file="header.jsp" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:set var="cpath" value="${pageContext.request.contextPath }" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>oauth2</h1>
<hr>
<script>
const cpath = 'http://localhost:8080/convenienceStore'
opener.location.href = '${cpath}${location}'
window.close()
</script>
</body>
</html>
UpdateId.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ include file="header.jsp" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<style>
.UpdatePadding {
padding-top: 100px;
padding-bottom: 300px;
}
h3 {
font-size: 20px;
color : #4E5968;
padding-bottom: 30px;
}
#connectButton1 {
padding: 10px;
background-color: #1E90FF;
border: 1px solid #ddd;
border-radius: 10px;
color: white;
font-size: 15px;
}
#connectButton2 {
padding: 10px;
background-color: #eee;
border: 1px solid #ddd;
border-radius: 10px;
color : #4E5968;
font-size: 15px;
}
p {
font-size: 17px;
}
.ButtonBox {
padding-top: 30px;
padding-bottom: 20px;
}
form {
padding: 0px 20px;
}
</style>
</head>
<body>
<div class="frame">
<div class="UpdatePadding">
<h3>${oauthUser.provider } 계정 연동</h3>
<fieldset>
<p>
${oauthUser.email } 로 이미 가입된 일반 계정이 있습니다<br>
${oauthUser.provider } 계정을 연결하시면 편리하게 이용이 가능합니다
지금 계정을 연결할게요
</p>
<div class="ButtonBox">
<form method="POST" style="display: inline">
<button id="connectButton1">네, 지금 연결할게요</button>
</form>
<a href="${cpath }"><button id="connectButton2">다음에 연결할게요</button></a>
</div>
</fieldset>
</div>
</div>
</body>
</html>
MemberDAO
package com.itbank.repository;
import com.itbank.model.MemberDTO;
import com.itbank.model.OauthUserDTO;
public interface MemberDAO {
MemberDTO selectMemberByEmail(OauthUserDTO oauthUser);
MemberDTO findByOauthId(OauthUserDTO oauthUser);
int updateId(OauthUserDTO naverUser);
MemberDTO selectMemberByUserId(String userid);
MemberDTO loginNaver(OauthUserDTO dto);
}
member-mapper.xml
: 네이버 로그인 관련 부분만 mapper 로 처리함
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC
"-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itbank.repository.MemberDAO">
<select id="selectMemberByUserId" parameterType="string" resultType="member">
select * from member where userid = #{userid}
</select>
<select id="selectMemberByEmail" parameterType="oauth" resultType="member">
select * from member where email = #{email}
</select>
<select id="findByOauthId" parameterType="oauth" resultType="member">
select * from member where ${provider}_id = #{id}
</select>
<update id="updateId" parameterType="oauth">
update member
set
${provider}_id = #{id}
where
email = #{email}
</update>
<select id="loginNaver" parameterType="oauth">
select * from member where naver_id = #{id}
</select>
</mapper>
다음 일정 정리
[ Login API 구현 성공 ]
- 네이버 로그인 하고나면, 우리 사이트에서 해당 아이디 존재 여부에 대해 먼저 파악하고 아이디가 없으면 회원가입으로 보냄.
- 회원가입으로 보낼때, 네이버에 로그인되어있는 정보를 추가적으로 보내서(이메일, 닉네임)
좀 더 간편하게 회원가입 가능.
- 아이디는 존재하지만, 네이버와 연동은 되어있지 않을때는 연동을 제안함
[ Login API 미구현 ]
- session에 api 를 이용한 로그인 값을 저장해두어야함
'project' 카테고리의 다른 글
[ spring 프로젝트 ] 테스트 해보기 (1) | 2024.03.26 |
---|---|
[ spring 프로젝트 ] 네이버 로그인 api (2) (0) | 2024.03.23 |
[ spring 프로젝트 ] 마이페이지 - 회원정보수정(이메일 인증), list 출력 (0) | 2024.03.18 |
[ spring 프로젝트 ] 관리자 모드 - AJAX 이용 (0) | 2024.03.13 |
[ spring 프로젝트 ] 회원가입, 로그인/아웃 - 중복체크(AJAX), hash (0) | 2024.03.09 |