그저 내가 되었고

🎯Node.js + Oauth:: 네이버 로그인 구현하기(w/o passport) 본문

개발/Node.js

🎯Node.js + Oauth:: 네이버 로그인 구현하기(w/o passport)

hyuunii 2022. 12. 2. 20:05

서버단에서는 액세스토큰 발급과, 사용자정보 받아오는 로직만 짜면 된다.


네이버 로그인 API

: 로그인 인증 요청 API + 접근 토큰 발급/갱신/삭제 요청 API

(※ GET과 POST 메소드를 사용. 따라서 간편로그인 오픈 API는 정보 입력, 정보 요청의 기능을 수행하며 삭제와 업데이트는 기능적으로 지원하지 않음. 간편로그인 특성상 사용자가 직접 로그아웃 작업을 직접 수행해야 함)

 

* 네이버 로그인 인증 요청 API?

- 내 웹 또는 앱에 네이버 로그인 화면을 띄우는 API(인증에 성공하게 되면 해당 API에서 받은 code값을 통해 발급 API을 호출)

- GET/POST 메소드 사용, URL(https://nid.naver.com/oauth2.0/authorize)

 

* 접근 토큰 발급/갱신/삭제 요청 API?

- 이용자가 네이버 회원 인증에 성공하면 API로부터 받은 code값을 이용해서 접근 토큰 발급 요청 API를 호출. 이를 통해 받은 접근 토큰(access token)값은 다음과 같이 회원 프로필 조회를 비롯하여 여러가지 로그인 오픈 API 호출에 사용 가능.

- GET/POST 메소드 사용, URL(https://nid.naver.com/oauth2.0/token)

 

* 로그인 오픈 API?

- 접근 토큰값을 이용해 호출 가능한 API 들로서 HTTP로 호출할 때 Header에 접근 토큰(access token)과 함께 애플리케이션 등록 시 발급받은 Client ID와 Client Secret 값을 같이 전송하면 다음의 기능 활용 가능.

  • 회원 프로필 조회
  • 카페 가입 / 글쓰기
  • 캘린더 일정 생성

네이버 API 이용 신청

1. 네이버 개발자센터에서 애플리케이션 등록후 ClientId, Secret키 발급

https://developers.naver.com/products/login/api/api.md

1) 내 애플리케이션 등록

위와 같이 입력을 완료하면  client ID와 client secret을 발급해준다.

 

2. 네아로 연동 URL 생성

 

url 창에 입력

client_id와 redirect_uri는 해당 값에 맞게 입력 

https://nid.naver.com/oauth2.0/authorize?client_id=발급받은clientid&response_type=code&redirect_uri=http://127.0.0.1:9090/navercallback 

 

위 그림처럼 나온다면 잘 순조롭게 되고 있는 것이다. 

('네이버 로그인으로 {본인이 만든 애플리케이션 이름} 서비스를 이용하실 수 있습니다.' )

 

동의하기 클릭하면 파라미터인 redirect_uri에 인증코드 (code=~) 를 발급해준다. 

이 말은 즉 동의하기 클릭 시 네이버 서버로 로그인을 요청한 것이다.

이에 네이버 서버는 요청에 응답하기 위해 반환해주어야하는데 이를 redirect_uri의 값으로 리턴해주는 것이다.

이 때 네이버 서버에서는 리턴될 redirect_uri에 인증코드(authorization code)를 내려주게 된다.

그러므로 redirect_uri은 꼭 리턴되어 처리가 이루어질 URL로 설정해주어야한다.

 

여기까지가 로그인 API를 이용한 것이다.

그렇다면 이제 로그인한 계정의 프로필 정보를 가져와보자.

요청 URL(접근 토큰 발급/갱신/삭제 요청) 을 이용하면 된다.(https://nid.naver.com/oauth2.0/toekn)

 

여기서 이제 access_token을 받아오게 된다(얘로 다른 api들을 이용할 수 있게 되니까 중요~!)

똑같이 요청 변수 명세에 따라 파라미터를 추가한 후 보내주면 된다. 

개중에서 중요한 건 code이다. code 파라미터가 바로 로그인 API를 통해 얻어온 인증코드(authorization code)이다.

파라미터를 넣고 요청 URL로 보낼 시 제대로 보내졌다면 access_token을 받아올 수 있을 것이다.

 

이제 받아온 access_token을 가지고 프로필 정보를 얻어올 것이다.

https://openapi.naver.com/v1/nid/me?access_token="받아온 액세스 토큰"

위 URL로 요청하면 끝~. 프로필 정보들이 JSON 형태로 받아왔는 걸 확인할 수 있다.

 

다른 API를 이용할 때에도 access_token을 가지고 이용한다고 보면된다.

명세에 따라 요청 URL과 요청 파라미터가 다를 뿐이다.

 

 

 

3. 액세스토큰 발급 

 

client_id, client_secret, code 값 채워서 url 만들고 입력 

 

https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&client_id=발급받은clientid&client_secret=발급받은clientsecret&code=위에서받은code

 

결과


요청 변수

로그인을 하기 위해서 네이버 로그인 페이지로 이동을 위하여 네이버 아이디로 로그인 인증 요청'의 요청 URL을 이용한다.

요청 URL의 요청 변수 명세에 따라 파라미터를 붙여주어 페이지 이동을 하게 한다.

그렇다면 https://nid.naver.com/oauth2.0/authorize?client_id=ZtFYUWVDrtzPEzaHMGaH&response_type=code&redirect_uri=http%3A%2F%2Fhotehrud.cafe24.com%2FGiftcon%2Ftest.html&state=1465903844635 이처럼 이동한 페이지의 URL이 될 것이다.

페이지 이동에 오류가 뜬다면 빠진 파라미터가 없는 지 확인하여 요청하면 된다.

 

 

1. 로그인 인증 요청 API

요청 변수:

 

요청 예시(요청 URL의 요청 변수 명세에 따라 파라미터를 붙여주어 페이지 이동을 하게 한다.)

(https)://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=jyvqXeaVOVmV&

redirect_uri=http%3A%2F%2Fservice.redirect.url%2Fredirect&state=hLiDdL2uhPtsftcU

 
· 인증 요청 기본 URL: tps://nid.naver.com/oauth2.0/authorize?

· 요청 변수 1. response_type=code&

· 요청 변수 2. client_id=jyvqXeaVOVmV&

· 요청 변수 3. redirect_uri=http%3A%2F%2Fservice.redirect.url%2Fredirect&

· 요청 변수 4. state=hLiDdL2uhPtsftcU

 

변수를 제외한 모든 내용은 예시로 이루어져있다.

 

 

응답 예시

· (http)://콜백URL/redirect?code={code값}&state={state값}

 

 

 

 

2. 접근 토큰 발급/갱신/삭제 요청 API

요청 예시(요청 URL의 요청 변수 명세에 따라 파라미터를 붙여주어 페이지 이동을 하게 한다.)

 

1. 발급

(https)://nid.naver.com/oauth2.0/token?grant_type=authorization_code&

client_id=jyvqXeaVOVmV&client_secret=527300A0_COq1_XV33cf&

code=EIc5bFrl4RibFls1&state=9kgsGTfH4j7IyAkg

 

2. 갱신

(https)://nid.naver.com/oauth2.0/token?grant_type=refresh_token&client_id=jyvqXeaVOVmV&

client_secret=527300A0_COq1_XV33cf&refresh_token=c8ceMEJisO4Se7uGCEYKK1p52L93bHXLn

 

3. 삭제

(https)://nid.naver.com/oauth2.0/token?grant_type=delete&client_id=jyvqXeaVOVmV&

client_secret=527300A0_COq1_XV33cf&access_token=c8ceMEJisO4Se7uGCEYKK1p52L93bHXLnaoETis9YzjfnorlQwEisqemfpKHUq2gY&service_provider=NAVER

 

 

· 요청 기본 URL: (https)://nid.naver.com/oauth2.0/token?

· 기본 요청 변수 1. grant_type=authorization_code&

· 기본 요청 변수 2. client_id=jyvqXeaVOVmV&

· 기본 요청 변수 3. client_secret=527300A0_COq1_XV33cf&

 

· 발급 요청 변수 1. code=EIc5bFrl4RibFls1&

· 발급 요청 변수 2. state=9kgsGTfH4j7IyAkg

 

· 갱신 요청 변수 1. refresh_token=c8ceMEJisO4Se7uGCEYKK1p52L93bHXLn

 

· 삭제 요청 변수 1. access_token=c8ceMEJisO4Se7uGCEYKK1p52L93bHXLn

· 삭제 요청 변수 2. service_provider=NAVER

 

변수를 제외한 모든 내용은 예시이며, 접근 토큰의 발급/갱신/삭제에 따라 요청 변수가 달라지는 것을 확인할 수 있다.

 

 

 

응답 예시

1. 발급

{

"access_token":"AAAAQosjWDJieBiQZc3to9YQp6HDLvrmyKC+6+iZ3gq7qrkqf50ljZC+Lgoqrg",    "refresh_token":"c8ceMEJisO4Se7uGisHoX0f5JEii7JnipglQipkOn5Zp3tyP7dHQoP0zNKHUq2gY",    "token_type":"bearer",     

"expires_in":"3600"

}

 

2. 갱신

{

 "access_token":"AAAAQjbRkysCNmMdQ7kmowPrjyRNIRYKG2iGHhbGawP0xfuYwjrE2WTI3p44SNepkFXME/NlxfamcJKPmUU4dSUhz+R2CmUqnN0lGuOcbEw6iexg",     

"token_type":"bearer",    

 "expires_in":"3600"

}

 

3. 삭제

{

"access_token":"c8ceMEjfnorlQwEisqemfpM1Wzw7aGp7JnipglQipkOn5Zp3tyP7dHQoP0zNKHUq2gY",     

"result":"success"

}


전체 코드

.env

더보기
NAVER_CLEINT_ID = 
NAVER_CLIENT_SECRET = 
NAVER_REDIRECT_URI = 
NAVER_STATE =

 

social.router.js

더보기
require("dotenv").config();
const express = require('express');
const router = express.Router();

const SoicalController = require('../controllers/social');
const soicalController = new SoicalController();

router.post('/naver/isNaver', soicalController.isNaver);

router.post('/naver/callback', soicalController.naver_callback);

module.exports = router;

 

controllers/social.js

더보기
// const Acess_ = require('../models/access_token');
require("dotenv").config();
const SocialService = require("../services/social");

// 순서
// 1. 프론트에게 인가코드 받기
// 2. 받은 인가코드를 백이 kakao쪽에 token요청
// 3. token받은 걸로 유저 정보 체크 후 DB에 저장
// 4. DB에 저장 후 token을 다시 만들어서 프론트에게 보내기

class SoicalController {
  socialService = new SocialService();

  isNaver = async (req, res, next) => {
    try {
      // 프론에게 인가코드 받기
      const { code } = req.body;

      // console.log('인가 코드' + code);
      try {
        const isNaver = await this.socialService.isNaver(code)

        const findNaverUser = await this.socialService.findUser(
            isNaver
        );
        if (!findNaverUser) {
          res.status(200).json({ userId: isNaver });
        } else {
          const accessToken = await this.socialService.accessToken(isNaver);
          const refresh_token = await this.socialService.refreshToken();

          // refreshtoken DB에 업데이트
          await this.socialService.updateRefresh(isNaver, refresh_token);

          res.status(201).json({
            accessToken: `Bearer ${accessToken}`,
            refresh_token: `Bearer ${refresh_token}`,
          });
        }
      } catch (error) {
        console.log(error);
        res.send(error);
      }
    } catch (err) {
      res.status(400).send({
        success: false,
        errorMessage: err.message,
        message: "에러가 발생했습니다.",
      });
    }
  };

  naver_callback = async (req, res, next) => {
    try {
      // 프론에게 인가코드 받기
      const { userId, nickName, myPlace, age, gender, likeGame } = req.body;

      // 회원가입에 필요한 내용 싹다 넣기 -> kakao에 있는 schema를 users로 변경
      // console.log(nickName)
      // console.log(address)
      try {
        await this.socialService.createUser(
            userId,
            nickName,
            myPlace,
            age,
            gender,
            likeGame
        );

        const accessToken = await this.socialService.accessToken(userId);
        const refresh_token = await this.socialService.refreshToken();

        await this.socialService.updateRefresh(userId, refresh_token);

        res.status(201).json({
          accessToken: `Bearer ${accessToken}`,
          refresh_token: `Bearer ${refresh_token}`,
        });
      } catch (error) {
        console.log(error);
        res.status(409).json({message : error.message, statusCode : error.status});
      }
    } catch (err) {
      res.status(400).send({
        success: false,
        errorMessage: err.message,
        message: "에러가 발생했습니다.",
      });
    }
  };
}

module.exports = SoicalController;

 

services/social.js

더보기
require("dotenv").config();
const axios = require("axios");
const jwt = require("jsonwebtoken");
const SocialRepository = require('../repositories/social')

const NAVER_CLIENT_ID = process.env.NAVER_CLIENT_ID;
const NAVER_REDIRECT_URL = process.env.NAVER_REDIRECT_URL;
const NAVER_CLIENT_SECRET = process.env.NAVER_CLIENT_SECRET;
const NAVER_STATE = process.env.NAVER_STATE;

class SocialService {
    socialRepository = new SocialRepository();

    isNaver = async (code) => {
        const {data} = await axios.post(
            `https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&client_id=${NAVER_CLIENT_ID}&client_secret=${NAVER_CLIENT_SECRET}&code=${code}&state=${NAVER_STATE}`,
            {
                headers: {
                    "Content-type": "application/x-www-form-urlencoded;charset=utf-8",
                },
            }
        );

        let access_token = data.access_token;

        const naverUser = await axios("https://openapi.naver.com/v1/nid/me", {
                headers: {
                    Authorization: `Bearer ${access_token}`
                },
            });
        return naverUser.data.id
    }


    createUser = async (userId, nickName, myPlace, age, gender, likeGame, admin) => {
        const isSameNickname = await this.socialRepository.findUserAccountNick(nickName);

        // 유저 nickname 중복 검사
        if (isSameNickname) {
            const err = new Error(`UserService Error`);
            err.status = 409;
            err.message = "이미 가입된 닉네임이 존재합니다.";
            throw err;
        }

        const createUser = await this.socialRepository.createUser(userId, nickName, myPlace, age, gender, likeGame, admin);
        return createUser;
    }

    findUser = async (userId) => {
        const findUser = await this.socialRepository.findUser(userId)
        return findUser;
    }

    updateRefresh = async (userId, refresh_token) => {
        const updateRefresh = await this.socialRepository.updateRefresh(userId, refresh_token);
        return updateRefresh;
    }

    accessToken = async (userId) => {
        const accessToken = jwt.sign(
            {userId: userId},
            process.env.DB_SECRET_KEY,
            {
                expiresIn: "5m",
            }
        );
        return accessToken;
    }

    refreshToken = async () => {
        const refreshToken = jwt.sign({}, process.env.DB_SECRET_KEY, {
            expiresIn: "2h",
        });
        return refreshToken;
    }
}

module.exports = SocialService;