유튜브 클론코딩[USER AUTHENTICATION]

김동현·2022년 1월 31일
0

nodeJS

목록 보기
5/14

Create Account

  • User모델을 만든다.

  • init.js 파일에 만든 모델을 로드해주기 위해 import 시킨다.

  • join페이지를 만들고 라우터 및 컨트롤러도 업데이트한다.

  • join의 get요청 페이지만 존재하니까 post요청 페이지도 만들어준다.

  • post 컨트롤러에서 request된 폼의 정보를 몽고DB에 저장한다. 그 후 로그인페이지로 리다이렉트 한다. (보통 회원가입을 한후 로그인 페이지로 이동하기 때문이다.)

    그런데 문제가 있다. 몽고DB에 접속해보면 회원정보의 비밀번호가 적나라하게 보인다.

    그래서 패스워드는 디비에 절대 바로 저장해서는 안된다. 해싱된 패스워드를 저장해야 한다.

  • 해싱해주는 패키지(bcrypt)를 설치하자. npm i bcrypt

  • saltRounds 는 해싱횟수를 말한다(예: 2면 해싱을 2번 한다는 의미)

  • promise를 지원한다.

  • 몽고DB에 패스워드가 저장되기전에 해싱이 되어야 하니까 사전 Hook를 정의한다.

Form Validation

  • 중복된 이메일 또는 유저네임을 입력시 에러가 발생하며 무한로딩이 된다.
    db의 unique옵션때문이다.
    db에서 걸러지는건 좋지않으며(최후의 보루느낌) 코드로 먼저 거르도록 하자.
    에러발생후 함수를 킬해야 하므로 return을 사용하자.

  • 또는 or 오퍼레이션을 사용할 수 있다.


    이 경우엔 유저입장에서 뭐가 중복된건지 확인하기 어렵다.
    무엇을 택할지 선택은 개발자 마음이다.

  • 비밀번호 확인 하는 인풋을 하나 더 만들어서 컨트롤러에서 확인작업하는 코드를 추가하자.

Status Codes

  • 회원가입에 실패했는데 브라우저에서 비밀번호를 저장할지를 묻고있다.
    이는 서버가 브라우저에게 정상적인 status코드(200)를 응답했기 때문이다.

  • status code에 관한 자료: https://ko.wikipedia.org/wiki/HTTP_%EC%83%81%ED%83%9C_%EC%BD%94%EB%93%9C

  • status code가 200인 번호를 받으면 브라우저가 정상적인 접근이라 판단하고 브라우저 히스토리에 기록을 남긴다. 하지만 400번대같이 오류코드를 넘겨받으면 히스토리에 기록하지 않는다. 이처럼 상황에 맞는 코드를 넘겨주는게 유저입장에서는 좋다.

  • 우리는 400코드를 넘겨주도록 한다. 그리고 앞서 404페이지를 만들었는데 그것또한 404코드를 넘겨주도록 한다. 404코드는 Not Found 코드이다.

Login

  • join 컨트롤러에서 발생할 만한 예상 오류를 2차례 걸러냈다. 하지만 또다른 오류가 발생할 수 있는 페이지이기 때문에 나머지 부분을 try-catch문으로 또 한번 걸러내자.

  • 로그인 페이지를 만들어보자.
    로그인 페이지도 get, post컨트롤러 둘다 필요하다.

  • postLogin 컨트롤러에서의 로직은 다음과 같다.

    1. 아이디가 존재하는지 확인

    2. 비밀번호가 맞는지 확인

  • 유저가 입력한 비밀번호를 해싱하고 그 값을 DB에 저장된 해싱된 비밀번호 값과 비교한다.

  • bcrypt 모듈에 관련 함수가 정의되어 있다.

  • 비밀번호 비교를 위해서는 특정계정을 알아야 하는데 그러기 위해서 다시 DB를 검색해야하므로 작업의 최소화를 위해 exist()함수코드를 없애고 findOne()함수코드로 대체했다.

Sessions and Cookies

  • 쿠키 : 브라우저와 서버간의 이동 매개체.

  • 세션 : 서버에서 저장되는 브라우저와 서버간의 정보이다.

  • 토큰 : 네이티브앱에서는 쿠키를 사용할 수가 없다. 그 대체로 토큰을 사용한다. 형태는 문자열이다.

  • JWT : 정보를 가지고 있는 토큰이다. 서버에서 인증만 할 수 있으며 DB를 사용하지 않는다는 점이 장점이다.

  • 흐름

    1. 로그인

    2. 서버에서 로그인한 유저에 대한 세션을 만든다.
      세션id를 포함해서 유저정보가 저장된다.

    3. 세션id를 쿠키에 담아서 브라우저에게 전달

    4. 브라우저는 요청할때마다 세션id가 담긴 쿠키를 건넨다.

    5. 서버는 세션id를 통해 유저를 구분한다. 그러므로 다시 로그인을 해야 하는 과정을 거치지 않아도 된다.

  • 세션역할을 하는 express-session 미들웨어 패키지를 설치하자.

  • 서버에 요청할때 헤더정보와 세션정보를 보자.
    아래는 첫번째 요청일 때이다.

  • 헤더에 쿠키도 없고 세션도 없다. 당연하다.
    처음 요청할땐 쿠키고 세션이고 없다.
    하지만 브라우저에 저장된 쿠키스토리지를 보면

    쿠키가 저장되어 있는걸 볼 수 있다.
    이는 서버가 브라우저의 요청을 받고 세션을 만든후 서버 메모리에 저장하고, 세션의 ID를 쿠키에 담아 브라우저에게 건네주었기 때문이다.
    새로고침을 해서 다시 한번 요청해보면

    헤더정보에 쿠키가 생성되어 서버에 보내지는걸 볼 수 있다.
    쿠키에는 세션의 ID가 포함되어 있으며 서버에 저장된 세션정보를 보니 동일한 것을 볼 수 있다.

Logged In User

  • 세션에 유저 정보를 저장하고 pug 템플릿에 표시해보자.
    로그인 컨트롤러에서 다음과 같이 세션에 유저 정보를 저장한다.

  • pug 코드도 로그인일때와 로그아웃일때 다르게 보이도록 수정한다.

  • 근데 위의 req.session.loggedIn은 어디서 받아오는 걸까?
    컨트롤러는 res.redirect() 을 리턴하기 때문에 값을 전달받지 않았다.
    그러므로 위의 코드는 에러가 난다.
    pug에서 사용된 req.session.loggedIn은 받아오지 않은 값이기 때문이다.
    그렇다면 session에 저장된 정보를 템플릿에 어떻게 보내는가?
    바로 locals 오브젝트를 이용하는 것이다.


  • 즉, locals object는 이미 모든 pug template에 import된 object이다.

  • 이제 pug 템플릿에 세션정보를 넘겨줄 수 있다.
    세션에 loggedIn 정보를 locals 오브젝트에 저장하고 템플릿에서 locals오브젝트의 loggedIn 변수를 사용하면 된다.

  • 요청할때마다 로그인 상태 정보를 알아야 하므로 미들웨어로 따로 만든다.

    위 코드는 아래와 같이 변경할 수 있다.

    false 또는 undefined가 나올수 있으니 Boolean()으로 false만 나오게 만든다.

  • 이제 request 할때마다 세션에서 로그인 상태확인후 locals에 정보를 보낸다. 그리고 템플릿은 로그인 정보를 확인하고 그에따른 화면을 랜더링한다.

MongoStore

  • 세션id는 쿠키에 저장되지만 세션 데이터 자체는 서버에 저장된다.
    세션의 디폴트 저장소(store)는 서버 메모리이며 테스트용으로만 사용하지 실제로는 사용하지 않는다.
    따로 세션 저장소가 필요하다.

  • express-session 홈페이지에 가보면 연동되는 세션 스토어들이 있다. 그 중에 몽고DB를 하도록 한다.

  • connect-mongo패키지를 설치한다.


  • 이제 세션이 DB에 저장된다. 로그인 하면 서버를 껐다켜도 유지된다.

Uninitialized Sessions

  • 만약 악의적인 의도로 봇이 웹페이지에 계속 접근하면 세션 id가 대량으로 만들어 질 것이고, 그에따른 용량도 많이 늘어날 것이다.
    이는 좋은 방법이 아니다.
    그러므로 비로그인 계정에게는 쿠키를 주지말고 로그인계정에게만 세션을 만들어 저장하고 쿠키를 주도록 하자.

    세션을 수정할 때만(여기서는 세션수정을 로그인 컨트롤러에서 이루어진다) 세션을 DB에 저장하고 쿠키를 넘겨준다.
    즉, 로그인 유저에게만 쿠키를 넘겨준다.

  • resave : 모든 request마다 세션의 변경사항이 있든 없든 세션을 다시 저장한다.

    • true:
      • 스토어에서 세션 만료일자를 업데이트 해주는 기능(touch()메소드)이 없으면 true로 설정하여 매 request마다 세션을 업데이트 해주게 한다.
    • false:
      • 변경사항이 없음에도 세션을 저장하면 비효율적이므로 동작 효율을 높이기 위해 사용한다.
      • 각각 다른 변경사항을 요구하는 두 가지 request를 동시에 처리할때 세션을 저장하는 과정에서 충돌이 발생할 수 있는데 이를 방지하기위해 사용한다.
  • saveUninitialized : uninitialized 상태인 세션을 저장한다.
    여기서 uninitialized 상태인 세션이란 request 때 생성된 이후로 아무런 작업이 가해지지않는 초기상태의 세션을 말한다.

    • true:
      • 클라이언트들이 서버에 방문한 총 횟수를 알고자 할때 사용한다.
    • false:
      • uninitialized 상태인 세션을 강제로 저장하면 내용도 없는 빈 세션이 스토리지에 계속 쌓일수 있다.
        이를 방지, 저장공간을 아끼기 위해 사용한다.

Expiration and Secrets

  • 개발자 옵션에 쿠키를 보면 이런 쿠키에 대한 여러 메타데이터들을 볼 수 있다.

  • secret :

    쿠키에 사인할때 사용하는 스트링이다.
    특정 백앤드가 자신이 준 쿠키라는걸 증명하기 위해 사인을 한다.
    세션 하이잭(납치)이라는 공격 유형이 있는데 이 공격으로부터 보호해준다.
    스트링은 길고 랜덤하게 작성할 수록 좋다.

  • domain :
    브라우저에게 쿠키를 만들어 보낸 백앤드가 누군지 알려준다.
    그렇게 함으로써 브라우저는 쿠키를 해당 도메인에게만 넘겨줄 수 있다.
    즉, 인스타나 페이스북이 만든 쿠키를 유튜브로 보내지 않는다.
    쿠키가 어디서 왔는지 어디로 가는지를 알려준다고 보면 된다.

  • path : url이라 보면 된다.

  • expires :
    쿠키의 만료일자이다.
    만약 session이라 되어있으면 만료일자를 명시하지 않은거다.
    그것을 세션쿠키라고 한다.
    만료일자를 지정하지 않으면 세션쿠키가 자동설정된다.
    세션쿠키는 웹 브라우저가 켜져있는 동안 유효하고 끄고 다시켜면 없어진다.

  • maxage :
    쿠키가 얼마동안 유지할 것인지 알려주는 시간이다.
    단위는 밀리초.

  • 다시 정리하자면

    • Session cookies

      • 웹브라우저가 켜져있는 동안 유효하고 끄고 다시 켜면 없어짐
    • Permanent cookie

      • 웹브라우저를 껐다 켜도 유지됨
    • Permanent cookie 기간 설정은 ExpriesMax-Age가 있음

      • Expires : 만료되는 시간 설정

      • Max-Age : 얼마동안 유지할 것 인지 설정

  • 코딩을 하다보면 숨겨야 하는 스트링이 있다.
    여기서는 secret 스트링과 몽고DB의 url을 숨겨야 한다.
    그럴때 사용하는 방법 중 하나는 환경변수를 설정하는 것이다.

  • .env파일을 만들고 환경변수를 넣었다.
    관습적으로 .env에 포함되는 변수는 대문자로 한다.
    당연히 이 파일은 깃허브에 올리면 안된다.
    그러므로 .gitignore에 설정한다.

  • 사용할 때는
    process.env.DB_URL, process.env.COOKIE_SECRET 와 같이
    사용한다.


    그런데 에러가 났다.
    그 이유는 .env파일을 읽고 각각의 변수들을 process.env에 넣는 작업이 필요하기 때문이다.

Environment Variables

  • dotenv 모듈은 .env 파일에 있는 변수들을 읽고 process.env에 넣어준다. 설치하자.

  • 사용법은 아래와 같다.

  • 최대한 빨리 모듈을 불러와야 환경변수를 로드하지 못하는 에러를 방지할 수 있다.

  • 이 프로그램에서 제일 먼저 시작하는 파일이 init.js파일이다.
    그곳의 첫째줄에서 사용하도록 한다.

    그런데 위처럼 작성하면 환경변수를 로드하지 못해 에러가 난다.
    그 이유는 require는 표현식이고 import는 선언문이라 호이스팅때문에 require보다 import가 먼저 실행되기 때문이다.
    import가 먼저 실행되면서 환경변수가 사용되는데 아직 dotenv모듈을 불러오기 전이라 로드가 제대로 되지 못한다.
    따라서 아래와 같이 사용해야 한다.

Github Login

  • 참고 url: https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps

  • 대략적인 플로우는 위와 같으며, 다른 sns도 이와 유사하다.

  • 먼저 하기전에, 깃헙 oauth app부터 만들어야 한다. =https://github.com/settings/apps

    register application 버튼을 누르면

    이 화면이 나온다. 닫지 않고 진행한다. 다시 돌아가서 위의 1,2,3번 진행순서를 나타나는 페이지에 간다.

    위의 url로 방문하는 코드를 작성한다.

    저장하고 링크에 접속하면 404페이지가 나온다.

    로그인 url에 파라미터를 같이 보내야 한다.


    그렇게 하면 아래와 같은 페이지가 나온다.

  • public data only 말고 개인 이메일 같은 데이터를 알고싶다면 로그인 파라미터에 더 많은 정보를 요구하면 된다.

  • allow_signup는 깃헙 사용자가 아닌 사용자에게 깃헙에 가입하게 하는 버튼이 표시되는데 우리는 필요없으므로 false로 설정한다.

  • scope는 유저에게서 얼마나 많이 정보를 읽어내고 어떤 정보를 가져올 것에 대한 것이다. 당연히 필요한 정보만을 요청하도록 한다.
    아래는 스코프 정보를 보여주는 url이다.
    https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps

    우리가 필요한건 저 2개, read:useruser:email이다.
    2개 이상의 스코프를 요청할때는 공백으로 분리한다.

    요청데이터가 변한걸 볼 수 있다.

    url이 너무 길고 바꿀때마다 오타로 오류날 확률이 크다.
    한곳으로 따로 모아서 관리하자.
    a태그로 바로 github에 접속하는 것보다 라우터를 만들어 그곳으로 접속하도록 하고, 그 곳에서 따로 url을 처리한 다음 github으로 리다이렉트 해주도록 한다.


    저 두개를 합치면 원하는 url을 얻게 된다. js에서 관련 메소드를 지원해준다. URLSearchParams()이다.

    스트링이 인코딩되어 나온다.

    이전의 a태그 href속성에 길고긴 string을 쓰는 것보다 이렇게 하나의 작업을 거치는 편이 훨씬 좋다.

    이제 버튼을 눌러본다.

    낯익은 url로 리다이렉트 됨과 동시에 code파라미터도 주어진다.
    localhost의 오타도 고치고, url도 callback말고 finish로 고친다.(start라는 라우터가 있으니 finish라는 라우터도 있으면 깔끔하니까)



    다시 돌아와서 flow를 보면

    받은 code를 access_token으로 바꿔주라고 되어있다.
    그리고 나와있는 url로 post request를 한다.

    위의 파라미터 3개는 필수로 건네줘야 한다.

    client_id를 2번 이상 사용했다.
    스트링은 두번 이상 사용하면 좋지않다.
    따라서 환경변수에 넣어서 사용한다.
    client_id가 비밀이라서가 아니라(어차피 url에 보여짐) 값을 한 장소에 넣음으로써 어디서든지 값을 사용할 수 있게 하기위해서이다.
    client_secret은 비밀 스트링이다.
    따라서 환경변수에 저장하도록 한다.

    이 경우엔 리다이렉트가 아니라 POST방법의 request만 할 꺼다.
    fetch()메소드를 사용한다.
    fetch()메소드는 무언가를 하고싶거나 무언가를 가져오고 싶을때 사용한다.

    json포맷을 받기위해 헤더에 위와 같은 코드를 추가해서 request하였다.

    fetch().then()을 사용하는게 익숙할지도 모르지만 await fetch()를 한 다음에 JSON을 가져오는걸 선호한다.

    실행해보면 오류가 난다.
    fetch()메소드가 정의되어 있지 않다고 한다.
    fetch()는 브라우저에서만 사용가능하기 때문이다.
    fetch()기능이 nodeJS엔 없다.
    사실 우리가 자바스크립트를 쓰고 있지만 브라우저에서 사용하는 자바스크립트와 완전히 같지는 않다.
    예를 들자면 브라우저에서는 alert()메소드가 동작하지만 nodeJS에서는 동작하지 않는다.

    nodeJS에서도 fetch()를 쓰기위해 node-fetch 모듈을 설치한다.(node-fetch가 3.0버전부터는 ESM-only Module이라서 오류가 날 수 있으니 node-fetch@2.6.1 설치를 하도록 한다.)


    실행이 잘 된다. 받아온 json파일에 access_token이 있다.즉, code를 갖고 request하니 access_token을 받아올수 있다.
    이제 이 access_token을 가지고 한번 더 request 하면 깃헙 api를 이용하여 user의 정보를 얻어올 수 있다.

    한번 받아온 access_token은 한번만 사용할 수 있으며 두번 이상 사용하게 되면 access_token이 포함된 json파일이 아닌 아래와 같은 파일이 받아와진다.

    그러므로 두 경우를 분기해서 코드를 작성해야 한다.

  • render("login", {errorMessage}) 대신에
    redirect("/login")을 한 이유는


    보다시피 지저분한 url이 보인다.
    이 url은 단지 어디론가 데려가주는 url일 뿐, 로그인페이지를 위한 url이 아니다.
    그러므로 render()가 아닌 redirect()를 쓰는 것이며 오류 메시지를 redirect()와 함께 보내는 작업은 추후에 배우도록 한다.

  • 실행을 해보니

    내가 원하는건 user의 정보인데 다른 결과가 나왔다. 저 url로 들어가보자.

    시키는 대로 해보자. 먼저 octokit을 설치한다.

    "인증 필요"메시지를 받았으므로 인증을 해야한다. 개인 키를 만들고...하다보니 갑자기 인증을 왜 하는거지? 라는 의문이 든다. 인증을 위해 access_token까지 받은건데...다시한번 코드를 봐본다. fetch의 헤더부분에 header가 아니라 headers가 와야 한다. 즉 s하나의 오타로 인해 access_token이 전달되지 못한것. 어쩐지 인증을 해라 하더니...

    잘 받아와진다.

    그런데 email이 null로 받아와진다. private정보라서 그렇다.
    하지만 우리는 이메일을 알고싶다.

    github api doc에 들어가보면 관련 내용이 나온다.


    /user/emails로 get요청을 해본다.


    두개의 이메일이 담긴 리스트가 나온다.
    여기서 우리가 필요한건 private인 이메일이다.
    브라우저 콘솔위에서 코딩을 통해 필터해보자.

    문제없이 잘 나온다. 나머지 코드들은 정리해주도록 한다.

  • 만약 위의 코드들을 fetch().then()형식으로 만든다면 복잡해진다.
    잠깐 맛만 보면

  • 저기서 더 길어진다. fetch().then()형식으로 만들면 복잡해진다. await방법이 좋은듯하다.

  • <수정 전>로그인 규칙을 정해보자.

    • 로그인 성공 :
      • 일반 로그인
      • 소셜 로그인
      • 등록된 유저가 아닐때 소셜 로그인 => 소셜 정보를 바탕으로 자동으로 회원가입후 로그인
    • 로그인 실패 :
      • 소셜 로그인으로 회원가입했지만 일반 로그인을 한 경우
      • 일반 로그인으로 회원가입했지만 소셜 로그인을 한 경우
    • 위의 <일반 로그인으로 회원가입했지만 소셜 로그인을 한 경우>를 로그인 성공에 넣어도 된다. response에서 primary: true, verified: true 인 이메일만을 받기 때문에 본인이라 봐도 된다고 볼 수 있다.
  • <수정 후>로그인 규칙을 정해보자.

    • 로그인 성공 :
      • 일반 로그인 (단, DB서칭할때 socialOnly:false 조건을 추가해서 검색함으로써 소셜가입자들의 정보 가져오는 것을 차단한다.)
      • 소셜 로그인 (본인의 이메일을 가져오는 것이므로 본인이라 판단, 전체 DB를 검색해서 이메일이 존재하면 로그인하고 존재하지 않으면 소셜 로그인에서 받아온 정보를 토대로 회원가입한 후 로그인한다.
    • 로그인 실패:
      • 소셜 로그인으로 회원가입했지만 일반 로그인을 한 경우 => 일반 로그인할때 검색하는 정보는 일반 가입자들을 대상으로만 시행하므로 이런 경우는 없다.
      • 일반 로그인으로 회원가입했지만 소셜 로그인을 한 경우 => 소셜 계정에서 받아오는 이메일이 사이트 유저DB내에 저장되어있는 이메일이라면 본인이라 판단, 로그인을 허용한다.
  • 깃헙에서 받아오는 유저 정보중에 avatar_url 이 있는데 이건 유저 프로필 사진의 url을 뜻한다.
    깃헙으로 가입하는 유저들 정보에 프로필 사진을 추가해보자.

    User스키마에 avatar_url필드를 추가하고

    깃헙정보로 가입하는 유저정보에 자동으로 추가되도록 한다.

카카오 로그인

  1. 애플리케이션 등록
    https://developers.kakao.com/docs/latest/ko/getting-started/app

  2. 인가 코드 받기
    https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-code

  3. 토큰 받기
    https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token

  4. 사용자 정보 가져오기
    https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info

Log out

  • 세션을 없애고 다시 홈으로 재 접속하니까 로그아웃된다.

    디비 세션을 확인해봤더니 역시나 사라져있다.
profile
프론트에_가까운_풀스택_개발자

0개의 댓글