Project/javachip

[2nd Project] 스프링, JSP, Javascript로 네이버 API 로그인 구현하기

ParkYeseul 2024. 10. 4. 20:14

안냐하세요.

네이버 하다가 멘탈 탈탈탈

탈곡기 마냥 탕탈ㄹ타ㅏㄹ 터졌습니다.

성공했는데도 어..엉,...했다..

이런 느낌이랄까

 

정리를 한 번 해보자.......

 

 

일단 네이버가 왜 힘들었냐면요 

구글이랑 카카오는 익숙한 이클립스로 했는데 

네이버는 MVC환경을 접한지 3일 됐는데 해보느라고 아주 애 먹었습니다.

눈이 퀭하네요

주말에는 정청산기 실기 공부만 전념할 수 있겠다 

휴 🍥🍥

 

 


 

 

 

 

 

 

 

 

NaverLoginDAO

package com.human.web.repository;

import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.human.web.vo.NaverLoginVO;

@Repository
public class NaverLoginDAO {
	@Autowired
    private  SqlSession sqlSession;

    public static final String MAPPER = "com.human.web.mapper.NaverLoginMapper";

    // 네이버 로그인 정보 삽입
    public int insertNaverLogin(NaverLoginVO vo) {
        int result = 0; // 입력 실패 시 결과값
        try {
            result = sqlSession.insert(MAPPER + ".insertNaverLogin", vo);
        } catch (Exception e) {
            System.out.println("네이버 로그인 정보 삽입 중 예외 발생");
            e.printStackTrace();
        }
        return result;
    }

    // 네이버 ID로 사용자 정보 조회
    public NaverLoginVO getNaverLoginInfo(String naverId) {
        NaverLoginVO naverLoginVO = null;
        try {
            naverLoginVO = sqlSession.selectOne(MAPPER + ".getNaverLoginInfo", naverId);
        } catch (Exception e) {
            System.out.println("네이버 로그인 정보 조회 중 예외 발생");
            e.printStackTrace();
        }
        return naverLoginVO;
    }
}

우리 DAO는 데이터베이스에 삽입하거나 조회하는 기능을 가지는 클래스다

여기서 중요한 메서드는 바로 <<insertNaverLogin(NaverLoginVO vo)>>

insert로 받아온 사용자 정보를 테이블에 넣을 수 있다.

 

나는 이 코드를 알고싶다.

result = sqlSession.insert(MAPPER + ".insertNaverLogin", vo);

sqlSession는 

MyBatis에서 SQL 실행을 위해 사용되는 객체

sqlSession를 통해 데이터베이스에 대한 삽입(insert), 조회(select), 수정(update), 삭제(delete)와

같은 SQL 쿼리를 실행 가능

 

MAPPER + ".insertNaverLogin"

Mapper는 문자열 상수로 특정 매터의 파일 경로를 나타낸다.

즉  "com.human.web.mapper.NaverLoginMapper.insertNaverLogin"

MyBatis가 실행할 SQL 쿼리를 XML 매퍼 파일에서 찾아서 실행하도록 도와준다.

매터 파일의 id에 있는 "insertNaverLogin"와 연결해준다는 소리

 

 

 

 

NaverLoginVO

package com.human.web.vo;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import java.sql.Timestamp;

@Getter
@Setter
@NoArgsConstructor
@ToString
public class NaverLoginVO {
    
    private String naverId;           // 네이버에서 제공하는 고유 ID
    private int mIdx;                 // m_member 테이블의 idx (외래 키로 사용)
    private String naverName;         // 네이버에서 제공하는 이름
    private String naverNickname;     // 네이버에서 제공하는 별명 (닉네임)
    private Timestamp createAt;       // 가입 날짜 (기본값: 현재 시간)

}

VO는 DTO랑 같은 의미인데. Value Object로 로그인과 관련된 데이터를 저장하는 클래스!
 MyBatis를 사용하고 있는데

자바의 표준 자바 빈 규칙을 따르기 때문에, 필드 이름을 카멜 케이스로 작성하는 것이 좋다라고 한다!

ex)  m_email -> mEmail

 

 

 

 

NaverLoginController

package com.human.web.controller;

import java.io.IOException;

import javax.servlet.http.HttpServletResponse;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.human.web.service.NaverLoginService;

@Controller
@RequestMapping("/naver")
public class NaverLoginController {

    @Autowired
    private NaverLoginService naverLoginService;

    // 네이버 로그인 페이지로 리다이렉트
    @GetMapping("/login")
    public void naverLogin(HttpServletResponse response, HttpSession session) throws IOException {
        String clientId = "hNC1YTLpfwJa8Hc6uBaJ";
        String redirectURI = "http://localhost:9090/web/naver/callback";
        String state = "RANDOM_STATE"; // CSRF 방지를 위한 상태값

        // 상태값을 세션에 저장 (CSRF 방지)
        session.setAttribute("state", state);

        String apiURL = "https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=" + clientId +
                        "&redirect_uri=" + redirectURI + "&state=" + state;

        response.sendRedirect(apiURL);
    }

    // 네이버 로그인 콜백 처리
    @GetMapping("/callback")
    public String naverCallback(@RequestParam("code") String code,
                                @RequestParam("state") String state, HttpSession session) {
        // 세션에 저장된 상태값 가져오기
        String sessionState = (String) session.getAttribute("state");

        // 상태값 검증
        if (sessionState == null || !sessionState.equals(state)) {
            System.out.println("CSRF 검증 실패: state 값 불일치");
            return "redirect:/error/errorPage";
        }

        // 네이버 로그인 처리
        try {
            boolean isLoginSuccessful = naverLoginService.processNaverLogin(code, state);
            return isLoginSuccessful ? "redirect:/SignUp/joinmain" : "redirect:/error/errorPage";
        } catch (Exception e) {
            e.printStackTrace();
            return "redirect:/error/errorPage";
        }
    }
}

로그인 프로세스를 관리!!!!!!!!!!!!

로그인 페이지로 리다이렉트가 되거나, 콜백 URL에서 로그인 프로세스 처리!
사실 컨트롤러가 제일~중요하다고 생각한다.

제일 어렵기도 하고.. 아직도 서비스와 컨트롤러가 조금은 추상적으로 다가온다.

 

여기서는 네이버 로그인을 시도하면 /login 경로에서 로그인 URL로 리다이렉트하고,

인증 성공 후 콜백 URL로 반환되는 code와 state를 사용하여 서버에서 로그인 처리한다.

 

 

NaverLoginService

package com.human.web.service;

import com.human.web.vo.NaverLoginVO;

public interface NaverLoginService {

    boolean processNaverLogin(String code, String state);

    NaverLoginVO getNaverLoginInfo(String naverId); // 사용자 정보 조회 메서드
}

로그인과 관련된 비즈니스 로직을 처리하는 인터페이스

processNaverLogin(String code, String state)

네이버 로그인 과정에서 

액세스 토큰을 받아 사용자 정보를 조회하고 데이터베이스에 저장하는 메서드

 

 

 

 

NaverLoginServiceImpl

package com.human.web.service;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.human.web.repository.M_MemberDAO;
import com.human.web.repository.NaverLoginDAO;
import com.human.web.vo.M_MemberVO;
import com.human.web.vo.NaverLoginVO;

@Service
public class NaverLoginServiceImpl implements NaverLoginService {

    @Autowired
    private M_MemberDAO mMemberDAO;  // M_MemberDAO로 수정

    @Autowired
    private NaverLoginDAO naverLoginDAO;

    @Override
    public boolean processNaverLogin(String code, String state) {
        try {
            String clientId = "hNC1YTLpfwJa8Hc6uBaJ";
            String clientSecret = "zoh620bPc0";
            String redirectURI = "http://localhost:9090/web/naver/callback";

            String apiURL = "https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&client_id=" 
                          + clientId + "&client_secret=" + clientSecret + "&code=" + code + "&state=" + state;

            URL url = new URL(apiURL);
            HttpURLConnection con = (HttpURLConnection) url.openConnection();
            con.setRequestMethod("GET");
            int responseCode = con.getResponseCode();

            if (responseCode == 200) {
                BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()));
                String inputLine;
                StringBuffer response = new StringBuffer();
                while ((inputLine = br.readLine()) != null) {
                    response.append(inputLine);
                }
                br.close();

                // JSON 파싱 후 액세스 토큰 가져오기
                JSONObject jsonResponse = new JSONObject(response.toString());
                String accessToken = jsonResponse.getString("access_token");

                // 사용자 정보 요청
                return requestUserInfo(accessToken);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    private boolean requestUserInfo(String accessToken) {
        try {
            String userApiURL = "https://openapi.naver.com/v1/nid/me";
            URL userUrl = new URL(userApiURL);
            HttpURLConnection userCon = (HttpURLConnection) userUrl.openConnection();
            userCon.setRequestMethod("GET");
            userCon.setRequestProperty("Authorization", "Bearer " + accessToken);

            int userResponseCode = userCon.getResponseCode();
            if (userResponseCode == 200) {
                BufferedReader userBr = new BufferedReader(new InputStreamReader(userCon.getInputStream()));
                StringBuilder userResponse = new StringBuilder();
                String userInputLine;
                while ((userInputLine = userBr.readLine()) != null) {
                    userResponse.append(userInputLine);
                }
                userBr.close();

                // 사용자 정보 파싱
                JSONObject userJson = new JSONObject(userResponse.toString());
                JSONObject responseObject = userJson.getJSONObject("response");
                String naverId = responseObject.getString("id");
                String naverName = responseObject.getString("name");
                String naverNickname = responseObject.getString("nickname");

                // m_member 테이블에 회원 정보 삽입
                M_MemberVO member = new M_MemberVO();
                member.setMEmail(naverId + "@naver.com");  // 카멜 케이스로 변경된 필드명에 맞춘 세터 호출
                member.setMNickname(naverNickname);         // 카멜 케이스로 변경된 필드명에 맞춘 세터 호출
                member.setMRegistrationType("naver");       // 카멜 케이스로 변경된 필드명에 맞춘 세터 호출

                int mIdx = mMemberDAO.insertM_Member(member);  // M_MemberDAO 사용하여 회원 정보 삽입

                if (mIdx > 0) {
                    // naver_login 테이블에 네이버 로그인 정보 삽입
                    NaverLoginVO vo = new NaverLoginVO();
                    vo.setNaverId(naverId);
                    vo.setMIdx(mIdx);
                    vo.setNaverName(naverName);
                    vo.setNaverNickname(naverNickname);

                    int result = naverLoginDAO.insertNaverLogin(vo);
                    return result > 0;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

	@Override
	public NaverLoginVO getNaverLoginInfo(String naverId) {
		// TODO Auto-generated method stub
		return null;
	}
}

NaverLoginService 인터페이스를 구현하여 네이버 로그인 과정의 실제 비즈니스 로직을 처리하는 클래스

 

processNaverLogin(String code, String state) -> 위에 NaverLoginService에 있었죠!

네이버에서 발급한 code와 state로 액세스 토큰을 받아 사용자 정보를 조회할 수 있다.

네이버에서 반환한 code로 액세스 토큰을 받고, 그 토큰을 사용해 사용자 정보를 조회하고 데이터베이스에 저장한다.

 

여기 코드가 가장 어렵고 길당 .. 어려운 부분을 공부해보잣

URL url = new URL(apiURL);
            HttpURLConnection con = (HttpURLConnection) url.openConnection();
            con.setRequestMethod("GET");
            int responseCode = con.getResponseCode();

            if (responseCode == 200) {
                BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()));
                String inputLine;
                StringBuffer response = new StringBuffer();
                while ((inputLine = br.readLine()) != null) {
                    response.append(inputLine);
                }
                br.close();

 

URL url = new URL(apiURL);

URL 

java.net.URL 클래스는 URL을 표현하는 클래스로,

네트워크 리소스에 접근할 수 있도록 한다.

 

new URL(apiURL)

apiURL이라는 문자열을 사용해 URL 객체 생성

apiURL은 네이버 API에 요청을 보낼 주소

 

HttpURLConnection con = (HttpURLConnection) url.openConnection();

 

openConnection() 

URL 객체의 openConnection() 메서드를 호출하여 네트워크 연결, URL에 대한 연결을 반환

 

HttpURLConnection

HTTP 프로토콜로 통신하기 위한 클래스

 

con.setRequestMethod("GET")

네이버 API로부터 데이터를 가져오기 위한 GET 요청을 설정

 

 

con.getInputStream()

서버의 응답 데이터를 읽기 위해 InputStream을 가져온다.

 

InputStreamReader

네트워크를 통해 받은 데이터는 바이트 단위라서 문자로 읽기 위해 InputStreamReader 사용

 

BufferedReader

BufferedReader는 내부 버퍼를 사용하여 입력 스트림을 읽기 때문에 한 줄씩 읽는 등의 작업을 쉽게 할 수 있다.

 

 

StringBuffer

문자열을 다룰 때 문자열을 수정할 수 있는 버퍼를 제공

String은 불변(immutable) 속성을 가지므로 문자열을 연결할 때마다 새로운 객체가 생성

반면 StringBuffer는 가변이기 때문에 기존 객체를 수정하여 효율성을 높인다.

 

while ((inputLine = br.readLine()) != null) { response.append(inputLine); }

br.readLine()
BufferedReader 객체를 사용해 서버로부터 한 줄씩 데이터를 읽는다.

읽은 데이터는 inputLine에 저장


!= null 조건은 읽어들인 줄이 null이 아닐 때까지, 즉 더 이상 읽을 데이터가 없을 때까지 반복


response.append(inputLine);
StringBuffer 객체인 response에 읽어들인 줄을 추가

이렇게 하면 전체 응답 데이터를 하나의 문자열로 연결할 수 있다.

 

 

 

NaverLoginMapper

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.human.web.mapper.NaverLoginMapper">

    <!-- 네이버 로그인 정보 삽입 -->
    <insert id="insertNaverLogin">
        INSERT INTO NAVER_LOGIN (naver_id, m_idx, naver_name, naver_nickname, create_at)
        VALUES (#{naverId}, #{mIdx}, #{naverName}, #{naverNickname}, #{createAt})
    </insert>

    <!-- 네이버 ID로 사용자 정보 조회 -->
    <select id="getNaverLoginInfo" parameterType="string" resultType="com.human.web.vo.NaverLoginVO">
        SELECT * FROM NAVER_LOGIN WHERE naver_id = #{naverId}
    </select>

    <!-- 네이버 ID 중복 검사 -->
    <select id="checkNaverId" parameterType="string" resultType="int">
        SELECT COUNT(*) FROM NAVER_LOGIN WHERE naver_id = #{naverId}
    </select>
</mapper>

MyBatis 매퍼로, 네이버 로그인 관련 SQL을 정의하고 데이터베이스와의 매핑을 담당
이 부분이 조금 낯설었은데 DAO에서 하던 걸 Mapper에서 하니까 오히려 가독성이 좋아지는 것 같다.

 

parameterType

매퍼 인터페이스나 매퍼 XML에서 사용되는 SQL 문에 전달되는 파라미터의 데이터 타입을

특정 사용자 정보를 조회할 때 사용자 ID를 인자로 받는다면 parameterType은 그 ID의 타입

String, int, 또는 특정 객체(UserVO)를 의미

 

 

resultType

SQL쿼리 실행 후 반환되는 결과의 데이터 타입 정의한다.

 

resultType="UserVO"는 쿼리 실행 결과가 UserVO 클래스의 객체에 매핑된다는 의미

즉, SQL에서 조회한 데이터를 UserVO 객체에 담아 반환

 

NaverLoginVO 객체가 전달

(#{}: #{naverId}, #{mIdx} 등은 MyBatis의 동적 바인딩 기법으로, Java 객체의 필드를 SQL에 매핑하여 사용할 수 있도록 한다.)

 

 

 

HomeController

 // 에러 페이지 매핑
    @GetMapping("/error/errorPage")
    public String errorPage() {
        return "error/errorPage";  // "WEB-INF/views/error/errorPage.jsp"로 이동
    }
	
	 @GetMapping("/") 
	 public String home() {
		 
		 return "SignUp/joinmain";
	  
	  }

홈 컨트롤러라는 것도 MVC에서 처음 보는 설정인데, 기본 페이지를 따로 설정을 해줘야 서버를 켰을 때 

내가 원하는 페이지로 간다! 
이클립스와 달리 WEB-INF에서 페이지를 열 수가 없다.

 

 

 

 

joinmain.jsp

// 네이버 로그인 버튼 클릭 시 처리
        document.getElementById("naver-login-btn").addEventListener("click", function () {
            console.log("네이버 로그인 버튼이 클릭되었습니다.");

            // 네이버 로그인 요청 경로로 리다이렉트
            window.location.href = '${pageContext.request.contextPath}/naver/login';
            
            try {
                let redirectUri = new URL(document.referrer).searchParams.get("redirect_uri");
                console.log("리다이렉트 URI:", redirectUri);
            } catch (e) {
                console.error("이전 페이지가 존재하지 않거나 URL 형식이 잘못되었습니다.", e);
            }

            
        });

네이버 로그인 버튼이 있는 페이지

 

 

 

 

 

errorPage

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>로그인 오류</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            text-align: center;
            margin-top: 50px;
        }
        .error-container {
            color: red;
        }
    </style>
</head>
<body>
    <div class="error-container">
        <h2>로그인 실패</h2>
        <p>로그인에 실패했습니다. 다시 시도해 주세요.</p>
        <button onclick="location.href='/'">홈으로 돌아가기</button>
    </div>
</body>
</html>

오류가 발생했을 때 사용자에게 오류 메시지를 보여주는 페이지로  NaverLoginController에서 

로그인 실패 하면 리다이렉트 되는 페이지다

 

 

MVC에서 작성하는 순서는

NaverLoginController -> NaverLoginService -> NaverLoginServiceImpl(아래 메소드 재정의) -> NaverLoginDAO
-> NaverLoginMapper -> 이제 그 값들이 NaverLoginController  넘어옴

이렇다. 나는 이게 왜 이렇게 헷갈리는지 매일 정리해도 매일 까먹으니까 매일 봐야지

 

 

🍝전체 흐름

  1.  joinmain.jsp에서 네이버 로그인 버튼을 클릭하면 NaverLoginController의 /login으로 요청이 리다이렉트
  2. NaverLoginController는 네이버 로그인 API URL로 사용자를 리다이렉트하여 네이버에서 로그인 인증을 수행
  3. 인증이 성공하면 네이버는 콜백 URL로 code와 state를 반환
  4. NaverLoginController의 /callback 메서드에서 code를 받아 NaverLoginService를 통해 액세스 토큰을 발급받고, 사용자 정보 조회
  5. NaverLoginDAO를 이용해 조회한 사용자 정보를 데이터베이스에 저장
  6. 로그인 성공 시 메인 페이지로 리다이렉트하고, 실패 시 errorPage.jsp로 이동

 

 

 

 

 

🥃오늘의 오류 이야기

나는 줄곧 callURL 오류였다.

사실 모든 sns로그인 할 때 리다이렉트 URL로 오류가 있었음에도

다 각기 다른 방법으로 오류를 내버렸다.

 

오늘은 어떤 오류였냐면, 내가 설정한 callback url은

http://www.localhost:9090/myapp/naver/callback

으로 설정을 했는데 자꾸~~~~~~~파일엔 문제가 없는데

가용할 수 없는 페이지라고 404가 떴다.

 

콘솔에 뜨는 것도 없고 그냥 막막해서 홈 컨트롤러 건들여보고 뭐가 문제인지 보다가

server.xml을 가서 내 경로를 확인해봤다.

path="/web"

ㅋㅋㅋㅋㅋㅋmyapp->web으로 바꿨다.

그런데 오늘은 2시반에 시작해서 7시에 끝났다. 

점점 시간이 빨라지는 거에 의의를 두고 
마무리를 잘 해야겠다.

 

 

 


 

드디어 3개의 SNS API 끝!!!!!!

이제 버튼 연결 잘하고 MVC에 옮기면 되겠다 헤셓세헤

꼬르륵🥩🍗