본문 바로가기
[ Project ]/Team

[ Team ] OAuth - NAVER API 로그인 구현

by 환이s 2023. 6. 16.


담당 기능

 

 

이전 포스팅까지 담당 기능으로 관리자 페이지 [회원관리] 기능 구현 포스팅을 했습니다.

해당 기능에 대해서 알아보시는 분들은 아래 링크 참고해 보시면 도움이 되실 거 같습니다.

 

 

[ Team ] 관리자 페이지 생성/ CUID 구현 (목록/상세정보)

담당 기능 ADMIN 페이지 회원 관리 기능 구현 담당을 맡아서 책임감 갖고 기능 구현을 해보겠습니다. 프로젝트 구조는 다음과 같습니다. 기능 구현할 때 사용된 객체/메서드/패턴은 이전 포스팅에

drg2524.tistory.com

 

 

[ Team ] 관리자 페이지 생성/ CUID 구현 (수정 페이지)

담당 기능 이전 관리자 페이지 CUID 기능 추가 이어서 수정 페이지 기능 구현을 해보겠습니다. 프로젝트 구조는 다음과 같습니다. 기능 구현할 때 사용된 객체/메서드/패턴은 이전 포스팅에서 자

drg2524.tistory.com

 

오늘은 NAVER 소셜 계정 로그인을 구현할 계획입니다. 목표는 네이버, 카카오 로그인이고 먼저 네아로 오픈 API를 사용해 네이버 로그인을 연동해 봤습니다. API 흐름은 다음과 같습니다.

 

1. LOGIN 페이지에 구현된 NAVER LOGIN 버튼을 클릭합니다.

 

2. 버튼 클릭 시 Service Provider에서 Client에 발급해 준 웹 애플리케이션 Client ID, Client Secret를 통해 네이버 로그인 API를 호출하면 네이버 로그인 폼으로 이동합니다.

 

3. Resource Owner가 Service Provider에 ID, PW를 입력합니다. (Client에 입력 x)

 

4. 계정 액세스 권한 요청 동의를 거친 후에 Access Token이 발급되며, 바로 정보를 꺼내 주는 게 아니라, Service Provider가 Access Token이 저장된 DB에 비교하여 지정된 Scope에 접근하는 게 맞는지 확인 후에 돌려줍니다.

 

5. Client가 Scope에 접근하면서 로그인이 성공하게 됩니다.

 

 

 

프로젝트 구조

 

 

 

 

 

네이버 API 이용 신청

 

소셜 로그인 기능 구현을 하려면 먼저 네이버 Open API 애플리케이션 등록을 해야 합니다.

 

https://developers.naver.com/main/

 

NAVER Developers

네이버 오픈 API들을 활용해 개발자들이 다양한 애플리케이션을 개발할 수 있도록 API 가이드와 SDK를 제공합니다. 제공중인 오픈 API에는 네이버 로그인, 검색, 단축URL, 캡차를 비롯 기계번역, 음

developers.naver.com

 

로그인 후 애플리케이션 등록 페이지로 이동해서 <애플리케이션 이름>, <사용 API>를 선택합니다.

 

<사용 API> 카테고리에서 [네이버 로그인]을 선택하시면

 

 

위 사진처럼 권한 체크 리스트가 나옵니다.

여기서 제공 정보를 선택하시고 환경 추가를 [PC 웹]으로 선택해 줍니다.

 

 

서비스 URL, CALLback URL 설정을 해주는데 경로 설정은 다음과 같습니다.

 

서비스 URL : 네이버 아이디로 로그인 배지가 노출되는 화면입니다. 애플리케이션 등록 시 서비스 URL과 네이버 검색에 노출되는 사이트 URL이 동일하면 네이버 검색 결과에서 네이버 로그인 뱃지가 노출됩니다.

 

CALLback URL : 로그인 성공 시 요청되는 URL입니다.

 

위 작업까지 끝나면 Client ID와 Client Secret을 발급해 줍니다.

 

 

 

라이브러리 설정 

 

 

등록을 마치면 소셜 로그인에 필요한 라이브러리를 pom.xml에 추가해줘야 합니다.

 

필요한 라이브러리는 OAuth2.0 프로토콜과 Controller에서 JSON 데이터 형식인 사용자 정보를 처리하기 위한 Json Parsing 라이브러리도 추가합니다.

 

		<dependency>
			<groupId>com.googlecode.json-simple</groupId>
			<artifactId>json-simple</artifactId>
			<version>1.1.1</version>
		</dependency>
        
        
		<dependency>
			<groupId>com.github.scribejava</groupId>
			<artifactId>scribejava-core</artifactId>
			<version>2.8.1</version>
		</dependency>

 

 

 

NaverLoginApi 

 

NAVER Login 구현체 추가 (provider 설정)

 

package com.example.trakker.oauth.model;

import com.github.scribejava.core.builder.api.DefaultApi20;

public class NaverLoginApi extends DefaultApi20 {

    protected NaverLoginApi(){ }

    private static class InstanceHolder{
        private static final NaverLoginApi INSTANCE = new NaverLoginApi();
    }


    public static NaverLoginApi instance(){
        return InstanceHolder.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";
    }

}

 

 

 

NaverLoginBO

 

네아로 연동 비즈니스 로직을 처리하기 위한 BO 클래스를 생성하여 인증 요청문을 구성해 줍니다.

 

package com.example.trakker.oauth.bo;

import com.example.trakker.oauth.model.NaverLoginApi;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.UUID;

public class NaverLoginBO {

    private final static String CLIENT_ID = "클라이언트ID";
    private final static String CLIENT_SECRET = "클라이언트 시크릿코드";
    private final static String REDIRECT_URI = "http://localhost:9090/trakker/callbackNaver"; //callback url
    private final static String SESSION_STATE = "oauth_state";
    /* 프로필 조회 API URL */
    private final static String PROFILE_API_URL = "https://openapi.naver.com/v1/nid/me";




    /* 네아로 인증  URL 생성  Method */
    public String getAuthorizationUrl(HttpSession session) {

        /* 세션 유효성 검증을 위하여 난수를 생성 */
        String state = generateRandomString();
        /* 생성한 난수 값을 session에 저장 */
        setSession(session,state);

        /* Scribe에서 제공하는 인증 URL 생성 기능을 이용하여 네아로 인증 URL 생성 */
        OAuth20Service oauthService = new ServiceBuilder()
                .apiKey(CLIENT_ID)
                .apiSecret(CLIENT_SECRET)
                .callback(REDIRECT_URI)
                .state(state) //앞서 생성한 난수값을 인증 URL생성시 사용함
                .build(NaverLoginApi.instance());

        return oauthService.getAuthorizationUrl();
    }

    /* 네아로 Callback 처리 및  AccessToken 획득 Method */
    public OAuth2AccessToken getAccessToken(HttpSession session, String code, String state) throws IOException, IOException {

        /* Callback으로 전달받은 세선검증용 난수값과 세션에 저장되어있는 값이 일치하는지 확인 */
        String sessionState = getSession(session);
        if(StringUtils.pathEquals(sessionState, state)){

            OAuth20Service oauthService = new ServiceBuilder()
                    .apiKey(CLIENT_ID)
                    .apiSecret(CLIENT_SECRET)
                    .callback(REDIRECT_URI)
                    .state(state)
                    .build(NaverLoginApi.instance());

            /* Scribe에서 제공하는 AccessToken 획득 기능으로 네아로 Access Token을 획득 */
            OAuth2AccessToken accessToken = oauthService.getAccessToken(code);
            return accessToken;
        }
        return null;
    }

    /* 세션 유효성 검증을 위한 난수 생성기 */
    private String generateRandomString() {
        return UUID.randomUUID().toString();
    }

    /* http session에 데이터 저장 */
    private void setSession(HttpSession session,String state){
        session.setAttribute(SESSION_STATE, state);
    }

    /* http session에서 데이터 가져오기 */
    private String getSession(HttpSession session){
        return (String) session.getAttribute(SESSION_STATE);
    }

    /* Access Token을 이용하여 네이버 사용자 프로필 API를 호출 */
    public String getUserProfile(OAuth2AccessToken oauthToken) throws IOException{

        OAuth20Service oauthService =new ServiceBuilder()
                .apiKey(CLIENT_ID)
                .apiSecret(CLIENT_SECRET)
                .callback(REDIRECT_URI).build(NaverLoginApi.instance());

        OAuthRequest request = new OAuthRequest(Verb.GET, PROFILE_API_URL, oauthService);
        oauthService.signRequest(oauthToken, request);
        Response response = request.send();
        return response.getBody();
    }


}

 

 

servlet-context.xml

 

Controller에서 BO를 이용할 수 있도록 Bean 등록을 해줍니다.

 

<beans:bean id="naverLoginBO" class="com.example.trakker.oauth.bo.NaverLoginBO">
	</beans:bean>

 

 

 

Controller

 

Controller는 View단에서 요청을 받거나, 보내주기 위한 코드를 작성합니다. 

 

@Controller
public class OauthController {


    /* NaverLoginBO */
    private NaverLoginBO naverLoginBO;
	
    /* NaverLoginBO */
    @Autowired
    private void setNaverLoginBO(NaverLoginBO naverLoginBO){
        this.naverLoginBO = naverLoginBO;
    }
     //로그인 첫 화면 요청 메소드
     @RequestMapping(value = "/login", method = { RequestMethod.GET, RequestMethod.POST})
    public String login(HttpSession session , Model model) {
        /* 네아로 인증 URL을 생성하기 위하여 getAuthorizationUrl을 호출 */
        String naverAuthUrl = naverLoginBO.getAuthorizationUrl(session);
        
        //인증요청 확인
        System.out.println("naverAuthUrl = " + naverAuthUrl);
        
        // 객체 바인딩
        model.addAttribute("urlNaver", naverAuthUrl);
        
         /* 생성한 인증 URL을 View로 전달 */
        return "login";
    }
    
    @RequestMapping(value = "/callbackNaver" , method = {RequestMethod.GET,RequestMethod.POST})
    public String callbackNaver(Model model,@RequestParam String code, @RequestParam String state, HttpSession session) throws IOException, ParseException {
        System.out.println("로그인 성공 callbackNaver");
        OAuth2AccessToken oauthToken = naverLoginBO.getAccessToken(session, code, state);
        
        // 로그인 사용자 정보를 읽어온다.
        String apiResult = naverLoginBO.getUserProfile(oauthToken);

        JSONParser jsonParser = new JSONParser();
        JSONObject jsonObj;

        jsonObj= (JSONObject) jsonParser.parse(apiResult);
        JSONObject response_obj = (JSONObject) jsonObj.get("response");

        //프로필 조회
        String email = (String) response_obj.get("email");
        String name = (String) response_obj.get("name");



        //세션에 사용자 정보 등록

        session.setAttribute("signIn",apiResult);
        session.setAttribute("email",email);
        session.setAttribute("name",name);
		
        // 로그인 성공 페이지 호출
        return "redirect:/loginSuccess.do";
    }
    
    @RequestMapping("/loginSuccess.do")
    public String loginSuccess(){
        return "loginSuccess";
    }
}

 

 

callback 호출 메서드에서 JSON parsing을 하기 위해 throws Exception으로 예외 처리를 해줘야 합니다.

 

getAccessToken 메서드를 사용해 토큰을 가져와서 Naver에 사용자 정보를 요청합니다.

 

받은 정보는 JSON Parsing 처리하여 세션으로 로그인 성공 페이지에 넘겨줍니다.

Naver에서 보낸 JSON 파일의 형식은 다음과 같습니다.

 

Naver API Response: {"resultcode":"00", "message":"success", "response":{"id":"네이버아이디"
, "email":"네이버이메일", "name":"이름"}}

 

 

 

JSP

 

 

< 로그인 페이지 >

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>NaverLoginTest</title>
</head>
<style>
    .btn_img{
        width: 70px;
        height: 70px;
        border-radius: 70%;
        overflow: hidden;

    }
    .btnimg{
        width: 100%;
        height: 100%;
        object-fit: cover;
    }


</style>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<c:set var="path" value="${pageContext.request.contextPath}" />
<body>
<br>
<div style="text-align:center" class="btn_img">
    <a href="${urlNaver}"><img class="btnimg" alt="사진 적용 안됨" src="${path}/img/btn_naver.png"></a>
</div>
<hr>
<div  class="btn_img">
<a class="p-2" href="${urlKakao}">
    <img src="${path}/img/btn_kakao.png" class="btnimg"></a>
</div>
</body>
</html>

 

 

 

< 로그인 성공 페이지 >

<%--
  Created by IntelliJ IDEA.
  User: drg25
  Date: 2023-06-16
  Time: 오전 2:02
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<jsp:include page="itemfile.jspf"></jsp:include>
<body>
<section class="bg-light">
    <div class="container py-4">
        <div class="row align-items-center justify-content-between">
            <a class="navbar-brand h1 text-center" href="${path}/admin/adminPage.do">
                <span class="text-primary h4">TRAKKER</span>
            </a>
        </div>
        <div>
            <h1 class="text-dark text-center">환영합니다!</h1>
            <p class="text-center">
                <span>${name}</span>님의 로그인 성공<br> 이메일 주소는 <strong>${email}</strong>입니다.
            </p>
        </div>
        <div class="d-grid gap-2">
            <button type="button" class="btn btn-primary btn-lg" onclick="location.href='home.jsp'"></button>
        </div>
    </div>

</section>

</body>
</html>

 

 

구현 결과

 

- MAIN 페이지 /login 경로 입력

 

 

 

- login 페이지

 

 

 

 

- 로그인 성공 페이지


마치며

 

오늘은 네이버 소셜 로그인 api 기능 구현에 대해서 알아봤습니다.

소셜 로그인을 공부할 때 Spring Boot로 진행하면서 Security OAuth 프로토콜 코드 형식으로 진행했었는데..

팀 프로젝트에서는 Spring 프레임워크. 즉,  Legacy 프로젝트로 진행하다보니, 라이브러리 충돌도 많이 발생합니다.

솔직히 구현하면서 느낀 점은 어렵습니다.

그만큼 코드를 짜면서 에러도 많이 발생했고 생각하는 시간도 많이 발생했었는데, 그만큼 공부도 많이 되는 기능이라고 생각합니다. 

 

다음 포스팅에서 뵙겠습니다.

728x90