네이버api를 이용한 Oauth2.0 로그인 구현(NextJs + NodeJs)

성찬홍·2023년 7월 21일
0

1. Oauth2.0란?

  • Oauth(Open Authorizaion)는 인터넷 사용자들이 비밀번호를 제공하지 않고, 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로써 사용되는, 접근 위임을 위한 개방형 표준이다.
  • 이러한 방식은 로그인 관리 책임을 'Third-Party Application(google,kakato,naver,twitter 등)에 위임할 수 있다.
  • Oauth 2.0은 다양한 클라이언트 환경에서 적합한 인증 및 인가의 부여 방법을 제공하고, 그 결과로 클라이언트에 접근 토큰( Access Token)을 발급하는 것에 대한 구조로 볼 수 있다.

2. Oauth2.0의 플로우

  • google,kakao, naver 등 Oauth를 제공하며 비슷한 플로우를 가지고 있다.그리고 아래는 잘 정리된 플로우 그림이다.

1. 용어 설명

(1) Client
: 운영되는 사이트
(2) Authorization Server
: google, naver 등 인증에 필요한 데이터를 제공해주는 서버
(3)Resource Server
: google, naver 등 사용자의 개인정보를 가지고 있는 서버
(4) Server
: 자신이 운영하는 사이트의 서버
(5) DB Server
: 자신의 사이트의 DB서버

2. 플로우 상세 ( NextJs , NodeJs, MYSQL, Naver 로 설명 )

  1. 클라이언트에서 로그인 요청 ( naver 로그인 페이지로 이동 )
  2. 로그인 후 authorization code 응답
  3. node 서버에 authorization 코드를 보내, node 서버로 로그인 요청
  4. authorization code를 Naver Resource Server로 accesstoken 및 회원 정보 요청
  5. 올바른 code를 받았으면, Naver에서 node 서버로 accesstoken과 요청한 회원정보를 응답
  6. 정보를 받은 NodeJs 서버는 회원정보를 가지고 MYSQL에 회원 중복(or 가입 여부 확) 후 클라이언트로 리다이렉트

3. 구현 시작

단계별로 구현을 해보겠습니다.
제 프로젝트 환경은 (클라이언트 : NextJS / 백엔드 : NodeJs / DB: MYSQL)입니다.

Step 1 :Naver Developers에서 Naver api 요청 등록하기

  • 등록 과정은 아래 링크로 대체하겠습니다.
    [naver Oauth 사용 요청]https://kimmjieun.tistory.com/63
  • 해당 과정에서 CLIENT_ID, CLIENT_SECRET, CALLBACK_URL 을 저장해두어야 합니다.
  • CALLBACK_URL은 네이버 로그인 이후 , nodeJs 서버로 요청하기 위한 URL입니다.

Step2 : 클라이언트에서 Naver 로그인 창 요청하기


//NextJS 클라이언트 
const goNaverLogin = () => {
    window.location.href =
      "https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=" +
      CLIENT_ID +
      "&redirect_uri=" +
      CALLBACK_URL +
      "&state=" +
      "asd2222222";
  };

//밑에 버튼 코드 jsx코드 생략
  • 위 url로 이동 시, 아래 네이버 화면을 만날 수 있습니다.
  • 에러가 날 시 ,naver Developers에서 url을 제대로 작성했는지 확인해봐야합니다.


Step 3 Node에 전달된 코드로 Naver 서버에 accesstoken 및 회원정보 요청

//node app.js
app.get("/naver/callback/oauth", async (req, res) => {
  // console.log(req.query.code); // 1. 일단 authorization code 받기 완료
  const code = req.query.code; // callback으로 전달받은 authorization code
  const state = req.query.state;//callback으로 전달받은 state
  const client_id = process.env.NAVER_CLIENT_ID;// developer에서 받은 id
  const client_secret = process.env.NAVER_CLIENT_SECRET;
  //developer에서 받은 secret
  const redirectURI = process.env.NAVER_REDIRECT_URL;
  //이건 여기서는 사용되지 않을 예졍이지만, 등록해줘야하는 부분이라 등록해줬다.
  const naver_api_url = `https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&response_type=code&client_id=${client_id}&client_secret=${client_secret}&redirect_uri=${redirectURI}&code=${code}&state=${state}`;
// 네이버 서버에 accesstoken 및 회원 정보 요청 url
  const options = {
    url: naver_api_url,
    headers: {
      "X-Naver-Client-Id": client_id,
      "X-Naver-Client-Secret": client_secret,
    },
    json: true,
  };// 이렇게 options를 담아서 요청하는 것이 규칙인 것 같다.
 
  const result = await request.get(options);
  // request 객체를 이용해서 요청 후 result에 저장 //const request = require("request-promise");
  const token = result.access_token;// 받은 accesstoken 저장


  const info_options = {
    url: "https://openapi.naver.com/v1/nid/me",
    headers: { Authorization: "Bearer " + token },
  };//토큰을 해제하기위해 구성
  const info_result = await request.get(info_options);
  // 결과저장
  const info_result_json = JSON.parse(info_result).response;
  // 여기서 로그인한 회원의 정보가 저장된다.아래와 같이 네이버 개인정보를 받아올 수 있다.
  /*
  Ex)
         {
    id: '!@!!!!',
    nickname: '별명',
    gender: 'M',
    email: '이메일',
    name: '이동기',
    birthday: '02-14',
    birthyear: '2000'
  }
  */
});

Step 4 전달받은 개인정보로 MYSQL에 회원 중복 or 가입여부 확인 후 클라이언트에 , 토큰을 리다이렉트해준다.

//node app.js
// 위의 정보 가져오는 api에 이어지는 코드들이다.

//우선 비동기 를 이용할 함수를 하나 만들어줬다. 아래에서 확인할 수 있다.
const checkUserQuery = async (SQLdata) => {
    return new Promise((resolve, reject) => {
      connection.query(confirmIDSQL, SQLdata, (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      });
    });
  };

// 받아온 유저데이터로 MYSQL에 유저 확인
  const SQLdata = [info_result_json.email];//email저장
  const confirmIDSQL = "SELECT U_IDX,U_EMAIL FROM USER_TABLE WHERE U_EMAIL=? ";//유저 가입확인을 위한 SQL
  let count = 0; 
  let userData = "";
  try {
    //위에서 만든 함수에 쿼리문을 넣어 이메일을 통해 SELECT된 갯수를 result에 저장한다.
    const result = await checkUserQuery(SQLdata);
    count = result.length;// count=0 => 신규유저, count=1 기존회원
    userData = result; 
  } catch (error) {
    console.error(error);
    res.status(500).send("Internal server error");
    return;
  }
//count=0이면, 신규회원이므로 , INSERT문으로 가입 후 , email정보만 토큰에 담아서 클라이언트로 리다이렉트해준다.
  if (count === 0) {
    const birth = info_result_json.birthyear + "-" + info_result_json.birthday;
    const insertData = [
      info_result_json.email,
      "naverpass",
      info_result_json.name,
      info_result_json.gender,
      birth,
    ];

    const insertSQL =
      "INSERT INTO USER_TABLE(U_EMAIL,U_PASSWORD,U_NAME,U_GENDER,U_BIRTH) VALUES(?,?,?,?,?)";
    // 신규 유저 저장 쿼리문 실행
    connection.query(insertSQL, insertData, (error, rows) => {
      const email = info_result_json.email;
      const booleanLogin = "true";
      const message = "로그인 완료";
      console.log(rows.insertId);
      const result = [{ U_IDX: rows.insertId, U_EMAIL: email }];
      //accessToken 생성
      const accessToken = jwt.sign(
        {
          email: email,
          result,
        },
        process.env.JWT_SECRET_KEY,
        {
          expiresIn: "30m",
          issuer: "hong",
        }
      );
      // cookie에 accessToken을 담아 전달
      res.cookie("accessToken", accessToken, {
        path: "/",
        secure: true,
        httpOnly: true,
        sameSite: "none",
      });
      // 클라이언트에 로그인 여부를 쿼리스트링에 담고 원하는 페이지로 리다이렉트 요청
      res.redirect(
        `https://club-front.vercel.app/Login/?login=${booleanLogin}&message=${message}`
      );
    });

  } 
// count!==0이므로 이미 회원이다. 그러므로 accesstoken 발급 후, 리다이렉트 해준다. if문의 insert문 제외 동일 과정이다.
else {
    console.log("이미 회원임이다");
    const result = userData;
    const booleanLogin = "true";
    const message = "로그인 완료";
    // 이미 회원이므로 토큰에 담아서 보내주자.
    const email = info_result_json.email;
    const accessToken = jwt.sign(
      {
        email: email,
        result,
      },
      process.env.JWT_SECRET_KEY,
      {
        expiresIn: "30m",
        issuer: "hong",
      }
    );
    res.cookie("accessToken", accessToken, {
      path: "/",
      secure: true,
      httpOnly: true,
      sameSite: "none",
    });
    res.redirect(
      `https://club-front.vercel.app/Login/?login=${booleanLogin}&message=${message}`
    );
  }
});

Step 5 완료
테스트 후 완료됐다.


4. 느낀점

플로우를 먼저 이해한 뒤 구현을 해보았다. 플로우를 이해하는데는 크게 어려움은 없었다.
그러나 Next,node를 이용한 Oauth 구현 정보를 찾는 과정에서 Next-auth,passporte 등의 라이브러리나 next에서 제공해주는 방법으로 구현하는 방법이 나와있었다.
Next-auth로 구현해보려했으나, 나와있는 정보로는 DB까지 저장하고 구분하는 플로우까지 구현하기에는 나와있는 정보가 부족해서 사용하지 못하고 passport라이브러리도 예상치 못한 오류들을 맞딱들여서 성공하지 못했다.
결국 , 위의 플로우대로 단계별로 나눠서 구현을 시도하니 위와같이 구현을 성공했다.
다행히 구현하는데는 성공해서 만족스럽지만, 나중에 Next-auth나 passport로 구현하는 방법도 좀 더 찾아보고 시도해봐야겠다.

참고 링크

https://velog.io/@singco/Oauth-Login%EC%9D%84-%ED%86%B5%ED%95%B4-%EC%B6%94%EA%B0%80%EC%A0%95%EB%B3%B4-%EC%9E%85%EB%A0%A5%EB%B0%9B%EC%95%84-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85%ED%95%98%EA%B8%B0feat-React-Spring

profile
꾸준한 개발자

0개의 댓글