그저 내가 되었고

🎯Node.JS + OAuth:: 소셜로그인(구글, 카카오, 네이버) 백엔드 코드 뜯어보기 본문

개발/Node.js

🎯Node.JS + OAuth:: 소셜로그인(구글, 카카오, 네이버) 백엔드 코드 뜯어보기

hyuunii 2022. 12. 6. 23:07

1. 각각의 소셜 로그인 버튼 눌렀을 때 isGoogle, isKakao, isNaver 실행

 

2. 프론트에게 인가 코드 받고 ⇢ 그 인가코드와 액세스 토큰을 교환하고  유저 정보 확인 ⇢ 프론트로 access token과 refresh token 응답보냄

 

※3, 4번 실행은 isGoogle, isKakao 실행 시점에서 response로 오는게 단순히 id다 하면 추가 가입 화면으로 넘기는거고, token이 온다고 하면 로그인 완료 띄우고 메인으로 넘기는 방식

3. 만약 id가 있다? 그럼 바로 token call back

 

4. 만약 id가 없다? 그럼 프론트 측에서 추가가입 요청으로 넘김

 

5. 그리고 완료 버튼 누르면 google_callback, kakao_callback실행

 

6. 들어온값 다 DB에 넣고 token전달. 프론트는 response값으로만 체크.

 

 

social.router.js

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

const SocialController = require('../controllers/social');
const socialController = new SocialController();

router.post('/google/isGoogle', socialController.isGoogle);  //🌟

router.post('/google/callback', socialController.google_callback);

router.post('/kakao/isKaKao', socialController.isKakao);  //🌟

router.post('/kakao/callback', socialController.kakao_callback);

router.post('/naver/isNaver', socialController.isNaver);  //🌟

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

module.exports = router;

 

 

controllers/social.js

require("dotenv").config();
const SocialService = require("../services/social");

class SoicalController {
  socialService = new SocialService();

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

      const isGoogle = await this.socialService.isGoogle(code);
      //윗줄의 코드 돌면? 인가 코드 사용하여 구글 인증 url에서 data 받아오고 거기에 액세스 토큰 담겨있음.
      //그 액세스 토큰 헤더에 삽입한 후 구글 인증 url에서 유저 정보 받아옴. 이후 거기서 id 리턴.

      const findGoogleUser = await this.socialService.findUser(isGoogle);
      //윗줄 코드 돌면? id 갖고 db에서 이 유저가 기존 가입자인지 쳌쳌
      
      if (!findGoogleUser) {
        res.status(200).json({ userId: isGoogle });
      } else {
        const accessToken = await this.socialService.accessToken(isGoogle)  //액세스토큰 발급
        const refresh_token = await this.socialService.refreshToken()  //리프레쉬토큰 발급

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

        res.status(201).json({
          accessToken: `Bearer ${accessToken}`,
          refresh_token: `Bearer ${refresh_token}`,
        });
      }
    } catch (err) {
      res.status(400).send({
        success: false,
        errorMessage: err.message,
        message: "에러가 발생했습니다.",
      });
    }
  };
  
  isKakao = async (req, res, next) => {
    try {
      // 프론에게 인가코드 받기
      const { code } = req.body;

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

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

          // refreshtoken DB에 업데이트
          await this.socialService.updateRefresh(isKakao, 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: "에러가 발생했습니다.",
      });
    }
  };

  kakao_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: "에러가 발생했습니다.",
      });
    }
  };

  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 GOOGLE_GRANT_TYPE = "authorization_code";
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
const GOOGLE_REDIRECT_URL = process.env.GOOGLE_REDIRECT_URI;

const KAKAO_GRANT_TYPE = "authorization_code";
const KAKAO_CLIENT_ID = process.env.KAKAO_CLIENT_ID;
const KAKAO_REDIRECT_URL = process.env.KAKAO_REDIRECT_URL;

const NAVER_CLIENT_ID = process.env.NAVER_CLIENT_ID;
const NAVER_REDIRECT_URI = process.env.NAVER_REDIRECT_URI;
const NAVER_CLIENT_SECRET = process.env.NAVER_CLIENT_SECRET;
// const NAVER_STATE = process.env.NAVER_STATE;

class SocialService {
    socialRepository = new SocialRepository();

    isGoogle = async (code) => {
        const {data} = await axios.post(
            `https://oauth2.googleapis.com/token?code=${code}&client_id=${GOOGLE_CLIENT_ID}&client_secret=${GOOGLE_CLIENT_SECRET}&redirect_uri=${GOOGLE_REDIRECT_URL}&grant_type=${GOOGLE_GRANT_TYPE}`,
            {
                headers: {"content-type": "application/x-www-form-urlencoded"},
            }
        );

        let access_token = data.access_token;

        const userInfo = await axios(
            `https://www.googleapis.com/oauth2/v2/userinfo?access_token=${access_token}`,
            {
                headers: {
                    Authorization: `Bearer ${access_token}`,
                },
            }
        ).then((el) => {
            return el.data;
        });
        return userInfo.id
    }


    isKakao = async (code) => {
        const {data} = await axios.post(
            `https://kauth.kakao.com/oauth/token?grant_type=${KAKAO_GRANT_TYPE}&client_id=${KAKAO_CLIENT_ID}&redirect_uri=${KAKAO_REDIRECT_URL}&code=${code}`,
            {
                headers: {
                    "Content-type": "application/x-www-form-urlencoded;charset=utf-8",
                },
            }
        );

        let acess_token = data.access_token;

        // token을 카카오 쪽에 보내서 정보 요청 및 받기
        const kakaoUser = await axios("https://kapi.kakao.com/v2/user/me", {
            headers: {
                Authorization: `Bearer ${acess_token}`,
            },
        });
        return kakaoUser.data.id
    }


    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}&redirect_uri=${NAVER_REDIRECT_URI }`,
            {
                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;

인가코드를 이용해서 "카카오" 의 토큰을 받는 부분:

 

받은 "카카오" 토큰을 이용해서 정보를 요청하는 부분:

그래서 얻은 정보의 아이디만 리턴.

 

그 후 그걸로 서버의 토큰 재생성(카카오의 시크릿코드와 우리의 시크릿코드가 다르기 때문)

 

 

repositories/social.js

const Users = require("../schema/users");
const bookmark = require("../schema/bookmark");
const moment = require('moment')
const date = moment().format('YYYY-MM-DD HH:mm:ss')

class SocialRepository {
  // DB 상에 그 userId가 없을 경우 생성
  createUser = async(userId, nickName, myPlace, age, gender, likeGame, admin) => {
    const createUser = await Users.create({userId, nickName, myPlace, age, gender, likeGame, admin, createdAt: date, updatedAt: date, expireAt: date
    });
    await bookmark.create({ nickName });
    return createUser;
  }

  // 유저 nickname 찾기
  findUserAccountNick = async (nickName) => {
    const findUserAccountData = await Users.findOne({
      nickName: nickName,
    });
    return findUserAccountData;
  };

  // DB 상에서 userId를 가진 유저가 있는지 체크
  findUser = async (userId) => {
    const findUser = await Users.findOne({userId : userId})
    return findUser;
  }

  // DB 에 refresh token updata
  updateRefresh = async(userId, refresh_token) => {
    const updateRefresh = await Users.updateOne(
      { userId: userId },
      { $set: { refresh_token: refresh_token } }
    );
    return updateRefresh;
  }
}

module.exports = SocialRepository;