User모델을 만든다.
init.js
파일에 만든 모델을 로드해주기 위해 import
시킨다.
join페이지를 만들고 라우터 및 컨트롤러도 업데이트한다.
join의 get요청 페이지만 존재하니까 post요청 페이지도 만들어준다.
post 컨트롤러에서 request된 폼의 정보를 몽고DB에 저장한다. 그 후 로그인페이지로 리다이렉트 한다. (보통 회원가입을 한후 로그인 페이지로 이동하기 때문이다.)
그런데 문제가 있다. 몽고DB에 접속해보면 회원정보의 비밀번호가 적나라하게 보인다.
그래서 패스워드는 디비에 절대 바로 저장해서는 안된다. 해싱된 패스워드를 저장해야 한다.
해싱해주는 패키지(bcrypt)를 설치하자. npm i bcrypt
saltRounds
는 해싱횟수를 말한다(예: 2면 해싱을 2번 한다는 의미)
promise를 지원한다.
몽고DB에 패스워드가 저장되기전에 해싱이 되어야 하니까 사전 Hook를 정의한다.
중복된 이메일 또는 유저네임을 입력시 에러가 발생하며 무한로딩이 된다.
db의 unique
옵션때문이다.
db에서 걸러지는건 좋지않으며(최후의 보루느낌) 코드로 먼저 거르도록 하자.
에러발생후 함수를 킬해야 하므로 return을 사용하자.
또는 or 오퍼레이션을 사용할 수 있다.
이 경우엔 유저입장에서 뭐가 중복된건지 확인하기 어렵다.
무엇을 택할지 선택은 개발자 마음이다.
비밀번호 확인 하는 인풋을 하나 더 만들어서 컨트롤러에서 확인작업하는 코드를 추가하자.
회원가입에 실패했는데 브라우저에서 비밀번호를 저장할지를 묻고있다.
이는 서버가 브라우저에게 정상적인 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 코드이다.
join 컨트롤러에서 발생할 만한 예상 오류를 2차례 걸러냈다. 하지만 또다른 오류가 발생할 수 있는 페이지이기 때문에 나머지 부분을 try-catch
문으로 또 한번 걸러내자.
로그인 페이지를 만들어보자.
로그인 페이지도 get, post컨트롤러 둘다 필요하다.
postLogin 컨트롤러에서의 로직은 다음과 같다.
아이디가 존재하는지 확인
비밀번호가 맞는지 확인
유저가 입력한 비밀번호를 해싱하고 그 값을 DB에 저장된 해싱된 비밀번호 값과 비교한다.
bcrypt 모듈에 관련 함수가 정의되어 있다.
비밀번호 비교를 위해서는 특정계정을 알아야 하는데 그러기 위해서 다시 DB를 검색해야하므로 작업의 최소화를 위해 exist()
함수코드를 없애고 findOne()
함수코드로 대체했다.
쿠키 : 브라우저와 서버간의 이동 매개체.
세션 : 서버에서 저장되는 브라우저와 서버간의 정보이다.
토큰 : 네이티브앱에서는 쿠키를 사용할 수가 없다. 그 대체로 토큰을 사용한다. 형태는 문자열이다.
JWT : 정보를 가지고 있는 토큰이다. 서버에서 인증만 할 수 있으며 DB를 사용하지 않는다는 점이 장점이다.
흐름
로그인
서버에서 로그인한 유저에 대한 세션을 만든다.
세션id를 포함해서 유저정보가 저장된다.
세션id를 쿠키에 담아서 브라우저에게 전달
브라우저는 요청할때마다 세션id가 담긴 쿠키를 건넨다.
서버는 세션id를 통해 유저를 구분한다. 그러므로 다시 로그인을 해야 하는 과정을 거치지 않아도 된다.
세션역할을 하는 express-session 미들웨어 패키지를 설치하자.
서버에 요청할때 헤더정보와 세션정보를 보자.
아래는 첫번째 요청일 때이다.
헤더에 쿠키도 없고 세션도 없다. 당연하다.
처음 요청할땐 쿠키고 세션이고 없다.
하지만 브라우저에 저장된 쿠키스토리지를 보면
쿠키가 저장되어 있는걸 볼 수 있다.
이는 서버가 브라우저의 요청을 받고 세션을 만든후 서버 메모리에 저장하고, 세션의 ID를 쿠키에 담아 브라우저에게 건네주었기 때문이다.
새로고침을 해서 다시 한번 요청해보면
헤더정보에 쿠키가 생성되어 서버에 보내지는걸 볼 수 있다.
쿠키에는 세션의 ID가 포함되어 있으며 서버에 저장된 세션정보를 보니 동일한 것을 볼 수 있다.
세션에 유저 정보를 저장하고 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에 정보를 보낸다. 그리고 템플릿은 로그인 정보를 확인하고 그에따른 화면을 랜더링한다.
세션id는 쿠키에 저장되지만 세션 데이터 자체는 서버에 저장된다.
세션의 디폴트 저장소(store)는 서버 메모리이며 테스트용으로만 사용하지 실제로는 사용하지 않는다.
따로 세션 저장소가 필요하다.
express-session 홈페이지에 가보면 연동되는 세션 스토어들이 있다. 그 중에 몽고DB를 하도록 한다.
connect-mongo
패키지를 설치한다.
이제 세션이 DB에 저장된다. 로그인 하면 서버를 껐다켜도 유지된다.
만약 악의적인 의도로 봇이 웹페이지에 계속 접근하면 세션 id가 대량으로 만들어 질 것이고, 그에따른 용량도 많이 늘어날 것이다.
이는 좋은 방법이 아니다.
그러므로 비로그인 계정에게는 쿠키를 주지말고 로그인계정에게만 세션을 만들어 저장하고 쿠키를 주도록 하자.
세션을 수정할 때만(여기서는 세션수정을 로그인 컨트롤러에서 이루어진다) 세션을 DB에 저장하고 쿠키를 넘겨준다.
즉, 로그인 유저에게만 쿠키를 넘겨준다.
resave
: 모든 request마다 세션의 변경사항이 있든 없든 세션을 다시 저장한다.
saveUninitialized
: uninitialized
상태인 세션을 저장한다.
여기서 uninitialized
상태인 세션이란 request 때 생성된 이후로 아무런 작업이 가해지지않는 초기상태의 세션을 말한다.
개발자 옵션에 쿠키를 보면 이런 쿠키에 대한 여러 메타데이터들을 볼 수 있다.
secret
:
쿠키에 사인할때 사용하는 스트링이다.
특정 백앤드가 자신이 준 쿠키라는걸 증명하기 위해 사인을 한다.
세션 하이잭(납치)이라는 공격 유형이 있는데 이 공격으로부터 보호해준다.
스트링은 길고 랜덤하게 작성할 수록 좋다.
domain
:
브라우저에게 쿠키를 만들어 보낸 백앤드가 누군지 알려준다.
그렇게 함으로써 브라우저는 쿠키를 해당 도메인에게만 넘겨줄 수 있다.
즉, 인스타나 페이스북이 만든 쿠키를 유튜브로 보내지 않는다.
쿠키가 어디서 왔는지 어디로 가는지를 알려준다고 보면 된다.
path
: url이라 보면 된다.
expires
:
쿠키의 만료일자이다.
만약 session이라 되어있으면 만료일자를 명시하지 않은거다.
그것을 세션쿠키라고 한다.
만료일자를 지정하지 않으면 세션쿠키가 자동설정된다.
세션쿠키는 웹 브라우저가 켜져있는 동안 유효하고 끄고 다시켜면 없어진다.
maxage
:
쿠키가 얼마동안 유지할 것인지 알려주는 시간이다.
단위는 밀리초.
다시 정리하자면
Session cookies
Permanent cookie
Permanent cookie
기간 설정은 Expries
와 Max-Age
가 있음
Expires
: 만료되는 시간 설정
Max-Age
: 얼마동안 유지할 것 인지 설정
코딩을 하다보면 숨겨야 하는 스트링이 있다.
여기서는 secret
스트링과 몽고DB의 url을 숨겨야 한다.
그럴때 사용하는 방법 중 하나는 환경변수를 설정하는 것이다.
.env파일을 만들고 환경변수를 넣었다.
관습적으로 .env
에 포함되는 변수는 대문자로 한다.
당연히 이 파일은 깃허브에 올리면 안된다.
그러므로 .gitignore
에 설정한다.
사용할 때는
process.env.DB_URL
, process.env.COOKIE_SECRET
와 같이
사용한다.
그런데 에러가 났다.
그 이유는 .env
파일을 읽고 각각의 변수들을 process.env에 넣는 작업이 필요하기 때문이다.
dotenv 모듈은 .env
파일에 있는 변수들을 읽고 process.env
에 넣어준다. 설치하자.
사용법은 아래와 같다.
최대한 빨리 모듈을 불러와야 환경변수를 로드하지 못하는 에러를 방지할 수 있다.
이 프로그램에서 제일 먼저 시작하는 파일이 init.js파일이다.
그곳의 첫째줄에서 사용하도록 한다.
그런데 위처럼 작성하면 환경변수를 로드하지 못해 에러가 난다.
그 이유는 require
는 표현식이고 import
는 선언문이라 호이스팅때문에 require
보다 import
가 먼저 실행되기 때문이다.
import
가 먼저 실행되면서 환경변수가 사용되는데 아직 dotenv모듈을 불러오기 전이라 로드가 제대로 되지 못한다.
따라서 아래와 같이 사용해야 한다.
참고 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:user
와 user: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
방법이 좋은듯하다.
<수정 전>로그인 규칙을 정해보자.
primary: true, verified: true
인 이메일만을 받기 때문에 본인이라 봐도 된다고 볼 수 있다.<수정 후>로그인 규칙을 정해보자.
깃헙에서 받아오는 유저 정보중에 avatar_url
이 있는데 이건 유저 프로필 사진의 url을 뜻한다.
깃헙으로 가입하는 유저들 정보에 프로필 사진을 추가해보자.
User스키마에 avatar_url필드를 추가하고
깃헙정보로 가입하는 유저정보에 자동으로 추가되도록 한다.
애플리케이션 등록
https://developers.kakao.com/docs/latest/ko/getting-started/app
인가 코드 받기
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-code
토큰 받기
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token
사용자 정보 가져오기
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info