백엔드

패스트캠퍼스 챌린지 16일차 - 페이스북 소셜 로그인 / 로그아웃 기능 구현

꾸준이 2021. 9. 21. 17:56

0.공부인증

공부 시작 12 : 28

공부 종료  17 : 54

 

소셜 로그인 복잡함...

포기하면 편하다지만,,,, 요즘에 소셜 로그인이 안되는 웹이 어딨누...

1. GraphQL [ CH - 16 ]

- 페이스북에서 발표한 새로운 API규격

- type 시스템을 갖추고 있음 

- Apollo, Prisma 등 다양한 오픈 소스툴들이 있음

 

Query

- query는 데이터 요청에 사용됨.

- REST 의 GET과 동일함

 

Mutation

- 변경에 사용됨.

- REST의 POST, DELETE, UPDATE 등과 같음

 

2. OAuth 에 대해서

- 유저 진입장벽이 낮아짐.

- 유저 허용 여부에 따라 이메일, 프로필 사진, 닉네임 등 기본 정보를 가져올 수 있음.

 

- 페이스북 로그인 기능 추가하기

https://developers.facebook.com/

 

Facebook for Developers

iOS 14에 대비한 파트너 준비 사항: Facebook 광고에 영향을 미칠 Apple iOS 14 요구 사항에 대해 자세히 알아보세요. FACEBOOK으로 빌드하기 Facebook의 추천 플랫폼으로 고객과 소통하고 효율을 높여보세요

developers.facebook.com

에서 개발자 등록을 해야함.

 

https://github.com/JeongHoJeong/node-oauth-example

 

GitHub - JeongHoJeong/node-oauth-example

Contribute to JeongHoJeong/node-oauth-example development by creating an account on GitHub.

github.com

에는 패캠 강사분이 제작하신 BoilerPlate(?) 가 있음.

 

BoilerPlate 를 설명하자면,

우선 main.js 에서 시작.

관련 app은 app.js에 설정되어 있음. (기본적인 틀은 이미 다 짜여져 있음.)

 

클라이언트에서는 pug가 뷰엔진임.

pug에서는 public/fb.js가 로그인 관여 - 페이스북에서 제공하는 기본 틀을 해당 위치에 옮겨야 함.

 

페이스북에서 제공하는 자바스크립트 코드는 하기와 같음.

다만 실제로 사용하기 위해서는 수정이 필요하긴 함. 변수 명이라던지, 함수 선언 방법이라던지 등

<script>
  window.fbAsyncInit = function() {
    FB.init({
      appId      : '{your-app-id}',
      cookie     : true,
      xfbml      : true,
      version    : '{api-version}'
    });
      
    FB.AppEvents.logPageView();   
      
  };

  (function(d, s, id){
     var js, fjs = d.getElementsByTagName(s)[0];
     if (d.getElementById(id)) {return;}
     js = d.createElement(s); js.id = id;
     js.src = "https://connect.facebook.net/en_US/sdk.js";
     fjs.parentNode.insertBefore(js, fjs);
   }(document, 'script', 'facebook-jssdk'));
</script>

위의 방법처러하게 되면,
http 로컬 호스트 이기 때문에, 사용에 문제가 발생함.

그래서 ngrok 를 활용해서  가상의 https 서버를 운영할 수 있음.

 

ngrok는 설치 후에 명령어 가 안먹는 다면, 아래 링크를 확인해서 해결할 수 있음.

https://gist.github.com/jwebcat/ecaac7bc7ee26e01cd4a

 

Installing ngrok on Mac

Installing ngrok on Mac. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

또한 페이스북 디벨로퍼 페이지에서 각종 세팅을 완료하고, https로 연결까지 완료하면,

window.fbAsyncInit = () => {
  FB.init({
    appId      : APP_CONFIG.FB_APP_ID,
    // cookie     : true,
    // xfbml      : true,
    version    : 'v12.0'
  }); 

  document.getElementById('fb-login').addEventListener('click', () => {
    console.log("hello")
    FB.login(
      (response) => {
        console.log(response)
      }, 
      {scope: 'public_profile,email'}
    );
  })

    
};

((d, s, id) => {
    const fjs = d.getElementsByTagName(s)[0];

    if (d.getElementById(id)) {return;}

    const js = d.createElement(s); js.id = id;
    js.src = "https://connect.facebook.net/en_US/sdk.js";
    fjs.parentNode.insertBefore(js, fjs);
})(document, 'script', 'facebook-jssdk');

이 코드를 정상적으로 사용할 수 있음.

 

리스폰스를 찍어보면 하기와 같이 출력됨.

{authResponse: {…}, status: 'connected'}
authResponse: 
accessToken: "sdfsdIDQ1c8NLcBAEDtYwuGvTYE8okfDp90qV6qQRUMlT8ZDFDSDAB0WoAaKotFn6L1MpwwAvsQfpq6SXWXMXa0WCYxBCZCZAVgqKxw2k1tPztok25xH8G09Axs8ot9yzqvEaXGAGawmky12LeJy9mX6QHD6CsNTw9u8tsSg0y09jqZC5bb258yfQd5bdrSN1A6urwfuPqIlI5LEz0v0ZCM5ZCEAuXj"
data_access_expiration_time: 1639977358
expiresIn: 6242
graphDomain: "facebook"
signedRequest: "N8_9nXspy6pUj4542th2suZNxao.eyJ1c2VyX2lkIjoiMTE1ODcwMzg0Nzg3MjY4MSIsImNvZGUiOiJBUUJJZmRTUUo0OHRTSkQwUjNSRjNVTzJ1OGcwZ0N2Mmx1N3F0bDlBQkJrRjBRemFmb2ZNX3pkWWJkRFBXaGpvc01rbXMxbDg2RHhSMGdLR3BBWEZTN2tmUzFaalJ2Zm9wbHlwNlhrV0MzaFFVX0VaVnJvTnVLX0pZOEdCOHBOczA5ZmI3V1dPVVhmXy0wTEY2SGZLLWZLTjlSU1F1RUdDSGVUQlZEWjJOX1loMUtvSU1mQ2V0c2UzNXRFRUhyQ2JfY1FTRkJHZ3FRY3JwRU52bVpYYlZTajJJbWFZZFg5cXBVb05jTU10Nk02T2N0TTN5NFZhRm51SmxEMWt2X0R1VGpobTA4ekVaSGZIa2M0aFRkaEw3Z3NGLUlrNlBkMk96N05vSklsSjI1UUZYX2gxeElpaC1LWTF0ZzZoMGd5UWlKY0w1Rl9ZN2tiQzhJaU1ScVpoSFVJQSIsImFsZ29yaXRobSI6IkhNQUMtU0hBMjU2IiwiaXNzdWVkX2F0IjoxNjMyMjAxMzU4fQ"
userID: "1158703847823423581"
[[Prototype]]: Objectstatus: "connected"[[Prototype]]: Object

그래서 이렇게 들어온 코드를 활용할 때,

uesrID 값이 아닌 accessToken을 활용해야 보안이 좋음.

 

accessToken을 활용하면, 이메일 등 사전에 합의된 정보만 가져올 수 있음.

또한 해당 토큰은 만료시간을 가지고 있어서, 해당 기간 동안 서버에 정보를 요청하여 값을 가져올 수 있음.

fetch(`/users/auth/facebook?access_token=${response.authResponse.accessToken}`)

 

백엔드로 돌아가서, 해당 라우팅 설정을 진행하자ㅏㅏㅏ.

// @ts-check

const express = require('express')
const { getUserAccessTokenForFacebookAccessToken } = require('../fb')

const router = express.Router()

router.post('/auth/facebook', async (req, res) => {
  const { access_token: fbUserAccessToken } = req.query

  if (typeof fbUserAccessToken !== 'string') {
    res.sendStatus(400)
    return
  }

  const userAccessToken = await getUserAccessTokenForFacebookAccessToken(
    fbUserAccessToken
  )

  res.cookie('access_token', userAccessToken, {
    httpOnly: true,
    secure: true,
  })
  res.sendStatus(200)
})

module.exports = router

 

 

토큰은 크게 두가지임.

-> 1, 페이스북에 정보를 요청할 때 사용하는 토큰.

-> 2, 서버 - 클라이언트에서 사용하는 토큰.

 

getUserAccessTokenForFacebookAccessToken 의 로직

async function getUserAccessTokenForFacebookAccessToken(token) {
  // TODO: implement it
  // 1. 페이스북 토큰으로부터 페이스북 아이디를 얻어내자.
  const facebookId = await getFacebookIdFromAccessToken(token)

  // 0. 해당 페이스북 아이디에 해당하는 유저가 데이터베이스에 있는 경우
  // -> 해당 유저를 찾아내서 액세스 토큰을 돌려주어야 함.
  const existingUserId = await getUserIdWithFacebookId(facebookId)

  if(existingUserId) return getAccessTokenForUserId(existingUserId)

  // 0. 해당 페이스북 아이디에 해당하는 유저가 데이터베이스에 없는 경우
  // -> 새로운 유저 엑세스 토큰을 뽑아서 새로운 유저 데이터를 생성
  const userId = await createUserWithFacebookIdAndGetId(facebookId)

  // 이제 유저 아이디로부터 토큰을 만들어서, 돌려주기만 하면 됨. 
  return getAccessTokenForUserId(userId)

}

위 함수 실행시 처음으로 실행되는 getFacebookIdFromAccessToken

async function getFacebookIdFromAccessToken(accessToken) {
  // TODO: implement the function using Facebook API
  // https://developers.facebook.com/docs/facebook-login/access-tokens/#generating-an-app-access-token
  // 해당 페이스북 토큰이 유효한지 확인하는 작업이 필요함.
  // https://developers.facebook.com/docs/graph-api/reference/v10.0/debug_token

  // 앱자체의 엑세스 토큰을 발급 받는 것.
  const appAccessTokenReq = await fetch(`https://graph.facebook.com/oauth/access_token?client_id=${FB_APP_ID}&client_secret=${FB_CLIENT_SECRET}&grant_type=client_credentials`)

  console.log(appAccessTokenReq)

  // 앱 자체의 엑세스 토큰
  const appAccessToken = (await appAccessTokenReq.json()).access_token  

  console.log(appAccessToken)

  // 애플리케이션 액세스 토큰을 가져왔음으로, 디버그 토큰 진행
  // 앱 자체의 엑세스 토큰과, 사용자의 액세스 토큰
  const debugReq = await fetch(`https://graph.facebook.com/debug_token?input_token=${accessToken}&access_token=${appAccessToken}`)

  const debugResult = await debugReq.json()

  console.log(debugResult)

  if(debugResult.data.app_id !== FB_APP_ID){
    throw new Error("Not a valid access token")
  }

  // FB에서 내려준 user_id 정보만 리턴함.
  return debugResult.data.user_id
}

위 함수에서는 페이스북에서 직접제공해준 userId 를 받을 수 있음.

 

 

이후, 조건은 크게 두가지로 갈림.

-> 1. 유저의 페이스북 아이디가 우리 앱의 DB에 있는 경우

 해당 데이터 베이스의 정보를 가져와서 return user.id

async function getUserIdWithFacebookId(facebookId) {
  // TODO: implement it
  // 데이터 베이스에서 조회하려고 하는 페이스북 아이디가 있는 지 확인.
  const users = await getUsersCollection()
  const user = users.findOne({
    facebookId
  })

  // 있으면 유저의 아이디를 돌려주고,
  if(user) return user.id

  // 없으면 언디파인을 돌려주자
  return undefined
}

-> 2. 유저의 페이스북 아이디가 우리 앱의 DB에 없는 경우 (상단 로직에 따라서, 없으면 자동으로 내려옴.)

아이디를 생성하기 위해서 uuid를 생성함.

해당 정보와 users 데이터 베이스에 정보를 저장해야함.

 async function createUserWithFacebookIdAndGetId(facebookId) {
  // TOOD: implement it
  const users = await getUsersCollection()
  const userId = uuidv4() // 우리 서비스 내의 유저 아이디가 만들어진 것임.
  await users.insertOne({
    id : userId,
    facebookId
  })

  // 새로운 계정을 만들고, 우리 서비스내의 고유 아이디를 돌려줌.
  return userId
}

해당 정보를 리턴하게 되면, 

async function getUserIdWithFacebookId(facebookId) {
  // TODO: implement it
  // 데이터 베이스에서 조회하려고 하는 페이스북 아이디가 있는 지 확인.
  const users = await getUsersCollection()
  const user = users.findOne({
    facebookId
  })

  // 있으면 유저의 아이디를 돌려주고,
  if(user) return user.id

  // 없으면 언디파인을 돌려주자
  return undefined
}

/**
 * @param {string} facebookId
 * @returns {Promise<string>}
 */
 async function createUserWithFacebookIdAndGetId(facebookId) {
  // TOOD: implement it
  const users = await getUsersCollection()
  const userId = uuidv4() // 우리 서비스 내의 유저 아이디가 만들어진 것임.
  await users.insertOne({
    id : userId,
    facebookId
  })

  // 새로운 계정을 만들고, 우리 서비스내의 고유 아이디를 돌려줌.
  return userId
}

최종적으로 해당 정보를 리턴하면  userId를 돌려받을 수 있다. 

 

그리고 그 정보를 쿠키에 저장을 해주면 된다.

// @ts-check

const express = require('express')
const { getUserAccessTokenForFacebookAccessToken } = require('../fb')

const router = express.Router()

router.post('/auth/facebook', async (req, res) => {
  const { access_token: fbUserAccessToken } = req.query

  if (typeof fbUserAccessToken !== 'string') {
    res.sendStatus(400)
    return
  }


  // JWT 토큰 까지는 생성됨.
  const userAccessToken = await getUserAccessTokenForFacebookAccessToken(
    fbUserAccessToken
  )

  res.cookie('access_token', userAccessToken, {
    // 아래와 같은 세팅이 있어야 자바스크립트에서 접근이 불가능함.
    httpOnly: true,
    secure: true,
  })
  res.sendStatus(200)
})

module.exports = router

그리고 클라이언트에서 서버로 데이터를 요청할때,

해당 정보를 가지고 유저 정보를 조회할 수 있다.

 

 

로그아웃하기!(로그인에 비해서 Joonnnaaa  쉬움)

클라이언트 쿠키에서 access_token 을 삭제하고, 

새로고침 해주면 됨.

        fetch(`/users/auth/facebook?access_token=${response.authResponse.accessToken}`,
        {
          method : 'POST'
        })
        .then(() => window.location.reload())
      },

 

 

페이스북 - 소셜 로그인 기능 구현 끝......

 

어렵...

 

 

 

환급 챌린지 (16/30)

 

 

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.

https://bit.ly/37BpXiC

 

패스트캠퍼스 [직장인 실무교육]

프로그래밍, 영상편집, UX/UI, 마케팅, 데이터 분석, 엑셀강의, The RED, 국비지원, 기업교육, 서비스 제공.

fastcampus.co.kr

 

#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는Node.js웹프로그래밍초격차패키지