Search
▪️

Sessions & Cookies

What is a Cookie?

사용자는 EJS Templating Engine을 이용하여(현 프로젝트에서...) Render된 View를 통해서 Frontend와 상호작용 한다.
또한 Frontend에서는 사용자로부터 입력받은 데이터나 행위를 토대로 Server에게 Request를 보내는 구조를 갖고 있다.
이와 같이 Request와 Response를 여럿 주고 받는 상황에 대해서 특정 데이터를 Browser에 유지를 해야하는 상황이라고 가정해보자. 이 때 쓰이는 것이 바로 Cookie이다.
그렇다면 Cookie들은 어떻게 생성 되는가? → 만일 Client-Side에서 특정 Request가 왔을 때, 서버는 Browser에게 Cookie라는 작은 기록 정보 파일을 사용자 컴퓨터의 파일 또는 메모리에 저장하도록 지시한다.
이 Cookie들은 Client-Side에 저장되어 사용된다.
차후에 Request를 보내게 될 때, Cookie들이 함께 전송되어(Http Header에 적재하며 트래픽 양이 늘어나는 원인이기도 하다.) Response를 받게 된다.
Cookie는 Name, Value, Expires, Domain, Path, Secure, HttpOnly 등의 요소로 구성된다.
Session Cookie / Persistant Cookie / Secure Cookie / Third-Party Cookie등으로 나뉜다.

Adding the Request Driven Login Solution

Post 요청을 통해서 Login을 했을 때, Request에 로그인 한 정보를 담아두고 매 Render마다 Front에서 요청 받은 Login 정보를 넘겨받으며 이용하면 Login이 유지되는가? → No
일단 Request는 Response를 받으면 죽어버리기 때문에, Response에 대한 작업을 Request에 담아두었던 정보들은 모두 사라지게 되는 것이다. 즉, response.redirect와 같은 작업을 하여도 Redirect되는 순간response를 받는 것이므로 사라지게 되는 것이다.
그렇다면 Routing 할 때, Path를 거치면서 View가 보이게 되는 것은 처음에 날린 Request가 아니라 Request, Response의 반복인 것인가? → Yes
Post 요청을 보내면서 (Request) Redirect를 하게 되면 (Response), 위에서 언급한 것과 같이 Request에 대한 Response를 받았으므로 Request는 죽게 된다. 그리고 Redirect 시 수행되는 Routing에 따라서 새로운 Request가 생성되고 Request에 따른 Rendering Response를 받게 되는 것이다. 즉 Request에 데이터를 담게 되면 데이터가 유실될 가능성이 매우 매우 높다.
이렇게 여러 Request, Response의 반복 과정은 고의로 설계된 것인가? → Yes
만일 Request에 데이터를 담아서 보내게 되면, 해당 데이터는 같은 Request에 대해서 작업하는 경우에만 살아남게 되는 것이다.
** Middleware들은 매 Incoming Request마다 우선적으로 모두 실행하게 된다..! 이런 이유로 이전 챕터에서 User에 대한 정보를 Middleware에 담은 것이다. 어떤 Request를 날리더라도, Middleware를 무조건 거치게 되면서 User에 대한 정보를 Request에 담게 되므로... 즉, Middleware의 실행 순서를 생각해보았을 때, Request 중간에 데이터를 담아야 하면 Middleware를 이용하는 것 역시 (바람직한 방법이든 아니든) 좋지 않은 방법임을 알 수 있다.

Setting a Cookie

위와 같은 방법으로 Request에 데이터를 담아 두는 방식이 되지 않는다면, 대체로 어떤 방식이 있을까?
첫 번째 대체 방식으로는 Global Variable을 두는 것이다. Global Variable을 파일 같은 곳에 저장하고 해당 변수를 Export하여 사용 하면 해당 변수를 바꾸는 것도 가능하고, Request Cycle동안에도 죽지 않고 데이터가 살아남게 된다.
위와 같은 방식의 단점은 무엇일까? → 모든 Request에도 살아남아 공유되는 데이터가 되므로 모든 사용자에게도 공유될 여지가 있게 된다.
따라서 두 번째 대체 방법인 Cookie를 사용하게 되는 것이다. Cookie를 사용하게 되면 사용하려는 데이터를 특정 사용자의 Browser에 담아서 저장할 수 있고, 저장한 데이터에 대해서 Cookie를 저장한 특정 사용자에게만 Customizing이 가능하기 때문에 다른 사용자에 대해서는 영향을 끼치지 않을 수 있다.
즉, 두 번째 대체 방법을 사용하면 Request Cycle에도 데이터가 사라지지 않도록 할 수 있으므로 다른 Request에 적재하여 보낼 수 있게 되는 것이다.
그렇다면 Cookie는 어떻게 설정하는 것인가? → Cookie는 Server측에서 Request에 대한 Response에 담아서 보내는 것이므로 Response의 setHeader('Set-Cookie, '$value') Method를 통해서 설정할 수 있다.
실제로 Cookie를 설정하고 Inspect Factor를 통해서 웹을 까보면, 설정된 Cookie를 Storage에 Cookies라는 항목에서 찾아볼 수 있다.
이런 Cookie들은 한 번 설정 되면, Browser에 의해 자동으로 모든 Request마다 Include되어 전송된다. 매 Request에 담겨서 전송된 Cookie들은 Header에 담겨서 전송된다. (console.log(request.get('Cookie'))를 통해서 확인할 수 있다.)
즉, Cookie는 Cross Request Data Storage라고 보면 된다. (여전히 큰 단점은 있다.)
** Cookie에 대한 값을 활용할 때, split(), trim() Method를 활용하면 용이하다.

Manipulating Cookies

위에서 Cookie값을 빼내서 사용하는데 꽤 많은 Split과 Trim을 이용하면서 복잡한 코드가 되었는데, 이에 대해서 깔끔하게 Parsing해주는 Third Party Package가 있다. 이 부분에 대해서는 나중에 얘기하고, 더럽고 복잡한 코드 외에도 위에서 언급한 큰 단점이 하나 있다.
Browser를 이용하면서, 설정된 Cookie에 쉽게 접근할 수 있을 뿐 아니라 매우 쉽게 변조가 가능하다는 것이다. 단순히 사용자가 Inspect Factor에서 값을 설정한다고 쉽게 인증에 대해서 변조할 수 있는 것은 바람직하지도 않고 매우 큰 문제이다.
민감하지 않은 데이터에 대해서는 상관 없지만, 민감한 데이터의 경우에는 사용자에 의해서 변조되지 않도록 Browser에 저장하지 않아야 한다. (즉, Request Cycle이 죽어도 데이터를 이용할 수 있어야 하므로 Cookie를 이용하긴 해야 하지만, Browser에 노출되지 않도록 하는 것이 중요한 것이다. ) 따라서 Session이 필요한 것이다.

Configuring Cookies

Cookie가 민감한 데이터를 저장하는 데에 적합하지 않은 방법임을 알고 Session이 필요하다는 사실 이전에, Cookie로 사용할 수 있는 것들에 대해서 알아보자.
사용자를 추적하거나 광고를 추적하는 등에는 유용한 자료일 수 있다. 즉 다르게 말하면, Cookie는 나에게도 유용한 데이터일 수도 있지만 내 웹 Browser 페이지 외에 다른 Browser 페이지에도 전송될 수 있는 데이터들이다.
위 예시와 같이 Cookie는 방문 했던 웹 Browser 페이지의 추적용 도구로 많이 이용된다. (Tracking Pixels on Pages라고 불린다.) 이는 Image URL의 형태를 띄지만 아무 이미지를 갖고 있지 않다.
만일 이 이미지가 Google Server에 존재하는 데이터였고, 내 웹 Browser는 해당 Cookie를 갖고 있다면, ㄴ현재는 Google Server의 웹 Browser에 상주하고 있지 않더라도 Google Server는 내가 어느 페이지를 방문했고 웹에서 어떻게 Routing을 해갔는지 추적할 수 있다.
저장되어 있는 Cookie를 통해서 추적을 하게 되는 것이고, 이 Cookie들을 지우면 Tracking Mechanism을 Block할 수 있다.
Cookie로는 단순 Value에 대한 저장 외에도 더 많은 것들을 할 수 있다. (setHeader() Method의 두 번째 인자가 Cookie에 담기는 값들인데 Multiple 값을 담을 수 있고, ;로 구부 짓는다.) 예를 들면 Cookie의 만료 시간을 설정할 수 있다. 'Expires=$duration'과 같이 설정할 수 있고, $duration의 경우 HTTP Date Format을 준수한다.
만료 시간 외에도 Cookie의 생존 시간을 설정할 수 있다. (Max-Age=$age) 설정한 초가 지나면 만료된다.
이런 것들은 민감하지 않은 데이터에 대해서 다룰 때에도 이용하지만, Authentication을 진행할 때도 이용하기도 한다. (예를 들면, Authenticated Session의 유지 시간 설정, 온라인 뱅킹 Timeout 설정 등...)
Domain 인자를 통해, Cookie가 전송될 도메인을 지정할 수도 있다.
Cookie에 Secure인자가 주어지면 (등호 표시 없이...) HTTPS에서만 Cookie가 전송된다. 이런 경우 HTTP에서 접근하면 Cookie가 보이지 않는다.
HttpOnly라는 인자를 주면 Browser 내부에 존재하는 JavaScript Code에서는 Cookie값을 읽을 수 없게 한다. (여전히 Browser에서는 읽을 수 있지만...) 이런 기법들은 Cross-Site Scripting Attack을 방지할 수 있다. (3자가 Client-Side에 추가로 Malicious JavaScript Code를 주입했을 때, Cookie 값을 읽을 수 없도록 하는...)
Cookie를 이용할 때, 민감한 자료는 저장하지 않으면서 인증에는 이용할 수 있도록 하는 것이 중요하다.
실제로 Cookie를 설정할 때는, Direct하게 Cookie를 건드리지 않고 Third Party Package를 통해서 조금 더 체계적인 방법을 쓰는 것들로 설정한다.
Cookie에서 제시된 문제들을 피하며, 민감한 데이터들을 Request를 지나다니면서 저장하는 Session에 대해서 정확히 배울 필요가 있다.
하지만 Session 사용함에 있어서도 Cookie는 굉장히 중요한 것이다.

What is a Session?

Request Response 동작 과정 중, 유지되어야 하는 데이터를 Client-Side에 두는 것을 Cookie라고 했다.
유지되어야 하는 데이터를 Server-Side에 두는 것을 Session이라고 한다. 이는 Cookie가 Browser와 같이 쉽게 접근 및 변조 가능한 곳에 있다는 단점 때문에 Server-Side에 두는 것이다.
Session은 Cookie와 같이 Request에 담아서 쓰는 값도 아니며, Express App과 같이 특정 변수에 담아 두는 것도 아니다. (변수에 담아두게 되면, 모든 사용자와 모든 요청에 대해서 공유 될 여지가 있으므로...)
우리가 Session을 두는 가장 큰 이유는, 같은 사용자에 대해서 모든 요청에 대해서 데이터를 공유하려고 하는 것이다. 즉, Session에 있는 데이터는 특정 사용자만 볼 수 있는 것이다.
Session은 Session Storage로 데이터베이스에 저장된다.
Client 측에서는 자신이 어느 Session에 속하는지 Server에게 알려야 한다. 이를 통해, 데이터베이스에서 찾은 Session이 Entry가 된다.
자신이 어떤 Session을 갖는지 Server에게 알릴 때, IP Address Matching을 사용하지 않는다. IP Address의 정보를 유지하기 힘들 뿐 아니라, 쉽게 변조가 가능하기 때문이다.
그렇다면 자신이 어떤 Session을 갖는지 Server에게 알리는 방법은 무엇인가? → Cookie를 이용한다. Cookie에 로그인한 사용자의 ID 값을 넣어서 보낸다. 이 때, ID 값이 Browser에 노출 되어도 큰 위험이 없도록 Hash값을 취해서 보낸다. 이 Hash값의 Plain 값은 Server에서만 확인할 수 있다.
자신이 쥐고 있는 Session을 Hash된 ID값으로 찾게 되면, Session을 통해 Confidential한 정보들에 접근이 가능하다. 이런 정보들은 Cookie와는 달리 Browser에서 변조가 불가능하다.

Initializing the Session Middleware

Session에 대한 Third Party Package는 아래 명령어를 통해서 설치할 수 있다.
npm install —save express-session
Package 설치가 끝났다면, app.js로 가서 Session에 대해서 초기화 설정을 해야 한다. 서버를 시작했을 때, Session에 대한 Middleware를 모두 초기화 하고, 초기화된 Session에서부터 매 Incoming Request에 대해서 사용되기 때문이다.
session이라는 상수에 Third Party Package를 할당한다. 이후에 session() Method를 use() Middleware에 할당한다. session() Method안에 들어가는 인자는 Session에 대한 Configuration 값들이다.
Middleware에 추가하는 Session의 Configuration 값은 JSON 형태로 준다.
JSON으로 주는 Key값들은 secret, resave, saveUninitailized등이 있다.
secret은 일종의 비밀 키로써, cookie-parser의 비밀 키로 작용한다. 일반적으로 secret 값은 굉장히 긴 String으로 준다.
resave 옵션을 true로 할 시, 매 Request이 완료 될 때마다 (Response를 받을 때마다) Session을 저장한다.
saveUninitialized 옵션을 true로 할 시, Session에 저장할 것이 없더라도 Session을 저장한다. (즉, Change Log가 없어도 Session을 저장한다. 반대로는 해당 옵션이 false일 때, Change Log가 없으면 별도로 Session을 저장하지 않는다.)
이와 같이 Session에 대한 초기화 및 초기 설정이 끝나면 Session을 이용할 수 있다.
** 추가로... JSON으로 주는 Key값으로는 cookie라는 Key에 Object 형태로 Value값을 주어, Cookie에 대한 설정도 가능하다. (maxAge나 Expires와 같은...) 이는 별도의 설정 없이 그냥 Default 값으로 이용해도 상관 없다.

Using the Session Middleware

Session을 이용하려는 Controller에서 Request에 Cookie를 담는 것 대신, request.session Object에 접근하여 이용한다. (request.session은 Session Middleware에 의해 자동으로 추가된 것이다.)
request.session을 이용하는 방법은 Cookie를 이용할 때 response.setHeader('Set-Cookie')와 달리, request.session.$addingKey = $addingValue와 같이 이용한다.
이와 같이 Session에 Key와 Value Pair를 저장했다면, Cookie에 connect.sid라는 Session ID Cookie가 추가된 것을 확인할 수 있다. 직접 값을 확인하려고 하면, 난수 값으로 나타나 있는 것을 확인할 수 있다. 이 Session Cookie는 Browser를 종료하면 Expire 된다.
Session Cookie를 통해 사용자가 Browser에 상주하고 있는 것을 알 수 있고, 사용자가 어느 페이지들을 Browsing하고 다니는지 확인할 수 있다. (이는 request.session을 console.log로 찍어보면 Session에 대한 정보가 상세하게 나오는 것을 확인할 수 있다. 이를 통해 사용자의 행동 역시 유추할 수 있다.)
비록 여전히 Cookie가 필요하긴 하나, 사용자의 민감한 정보들은 서버에 저장되어 운영된다. (따라서 Client 단에서 임의로 변조하기 어렵다.)
** Browser가 바뀌게 되면, 완전히 다른 Session과 다른 Environment로 인식한다.
** Session에 대한 구현을 할 때는, Browser별 및 User별로 Session을 갖도록 해야한다. 또한, 해당 정보를 Memory에 두는 것이 아니라 별도의 데이터베이스를 운영하는 것이 좋다.

Using MongoDB to Store Sessions

Session을 메모리에 저장하는 것은 그리 좋은 기법은 아니다. 메모리의 양은 무한하지 않고 한계가 있기 때문이다. (동시 접속자가 늘어날수록 메모리 지옥에 빠지게 된다.) 파일에 저장하는 것 역시 적절한 성능을 내지 못할 가능성이 높다. 따라서 데이터베이스에 Session을 저장하고 쓰는 것이 더 좋은 방법이다.
MongoDB의 경우 아래 명령어를 통해서 Session 저장을 MongoDB에 쉽게 할 수 있다.
npm install —save connect-mongodb-session
Package를 설치 했다면, 이전에 Session에 대한 설정은 Session을 이용하기 위한 설정인 것이므로 저장에 대한 설정이 필요하다. 아래와 같이 Package를 Import하며, 동시에 session을 인자로 받는다.
const MongoDBStore= require('connect-mongodb-session')(session);
위 MongoDBStore라는 상수는 session이라는 인자를 받아서 Constructor를 호출하는 상수이다. 해당 상수를 갖는 상수를 하나 선언한다. 새로 선언한 이 상수를 통해서 MongoDB의 Session을 쉽게 이용할 수 있다. MongoDBStore의 인자로 주어지는 JSON 형태의 데이터에는 어떤 데이터베이스에 접근할지에 대한 uri, 데이터베이스 내에 어떤 Collection에 저장할지에 대한 collection, 만료시간에 대해서 설정하는 expires 들이 담긴다. (만료 시간의 경우 MongoDB에 의해 자동으로 처리된다..)
const store= new MongoDBStore({uri: $uri, collection: $collection});
위와 같은 설정이 끝나면 Session을 저장하기 위해서 store라는 상수를 쓸 수 있다. 해당 상수는 이전에 Session을 Initialize할 때 JSON 형태의 옵션들을 준 곳에 store라는 옵션의 값으로 쓸 수 있다.
즉, express-session과 connect-mongodb-session에 대한 Package Import 후, session 설정 및MongoDBStore 상수를 통한 store 상수의 설정이 끝난다면, session의 초기화 인자로 store를 주면 데이터베이스에 Session 정보를 저장할 수 있게 되는 것이다.
이렇듯 실제 배포용 개발에서는 Session을 메모리에 저장하지 않고, 데이터베이스에 유지하여 사용한다. (보안과 용량적 측면에서...)
다시 한 번 언급하지만, Session이 강력한 도구로써 작용하는 이유는, 모든 Request들을 단일 사용자에 따라서 구분 지음과 동시에 구분된 모든 Request들 간에 특정 데이터들을 공유할 수 있기 때문이다. Session을 통해서 Authentication을 효율적으로 관리할 수 있게 된다.
** Express의 Session의 경우 대부분의 데이터베이스를 지원한다. 원하는 데이터베이스에 Session을 연동하고 싶다면 Express Session의 Documentation을 확인하는 것이 좋다.
** 예전에는 Cart에 대한 것들도 데이터베이스에 Session을 이용하지 않고 저장했지만, 현재는 Cart같은 것들도 Session으로 빼두는 것이 일반적이다.
** 항목 추가 삭제 등, CRUD에 대해서는 Authenetication 여부를 판별하여 처리해야 한다. 또한 Session값을 가지고 있다가 유실했을 때에 대해서 처리 로직이 별도로 없으면 Browser에 Crash가 생길 수 있으므로, 처리 로직을 둬야 한다.
** 그리고 Session에 대해서 Clearing하면서 동시에 Session Cookie를 삭제하는 로직도 필요하다.

Deleting a Cookie

단순히 Session Cookie만 지우는 것이 능사가 아니라, 데이터베이스에 존재하는 정보를 지우면서 Session Cookie를 지워야 한다.
우선 데이터베이스에서 삭제하는 경우에 대해서는, session에서 지원하는 간단한 Method를 통해서 지울 수 있다. request.session.destroy() Method를 통해서 MongoDB에 존재하는 Session의 정보에 대해서 삭제할 수 있다.
이 때 destroy() Method 안에는 Error Catch용 Callback Function을 넣을 수 있다. Session 삭제를 실패했을 경우에 대한 Error 로직을 처리할 수 있다.
그렇다면, Session Cookie는 어떻게 해야할까? → 그냥 두면 된다. 어차피 재 로그인 시에 Session Cookie를 Overwritten하게 되어 새로이 갱신될 뿐 아니라, Permanent Cookie가 아니기 때문에 Browser를 닫으면 해당 Cookie는 자동으로 삭제된다. (Expiry Date나 Max Age의 설정이 없다면 말이다... 있으면 그 시간 까지 살아있다.)

Making "Add to Cart" Work Again

이전처럼 Session을 두지 않고 Middleware를 통해서 매 Request마다 사용자를 저장해서 처리하는 경우, Middleware에서는 Mongoose가 지원하는 Method를 통해 데이터베이스로 부터 사용자를 받아왔기 때문에 Full Object를 가지고 있었다. 하지만 데이터베이스를 이용하더라도 Session을 이용하여 사용자에 대한 정보를 불러오는 경우, 이전에 단순히 Middleware내에 존재하는 데이터베이스 접근으로 불러온 사용자 정보는 조금 다른 형태를 가진다.
우선 Session에 대해서는 매 Request마다 Middleware를 통해서 사용자 정보를 Fetch하는 것이 아니라 이미 Session에 저장된 정보를 접근하는 것이기 때문에, 얻어 오려는 데이터의 Full Obeject가 아닌 Session Data들이다.
헷갈릴 수 있으니 다시 설명하면, Session 자체는 MongoDB에 저장되어 있는 것이 맞다. 하지만 Session내의 정보는 MongoDB가 아닌 MongoDBStore라는 connect-mongodb-session이기 때문에 이는 Mongoose의 Model을 이해하지 못한다. 이것이 문제인 것이다.
코드를 통해서 따라가 보면, store라고 하는 MongoDBStore를 사용했을 때 MongoDB에서 session에 대한 Collection들을 모두 Fetch하게 되고, 이 데이터들은 Object가 아니다. 즉, Mongoose에서 제공하는 Method를 썼을 때 얻을 수 있는 Object와는 다르다는 것이다.
이 문제를 해결하기 위해선, 지웠던 Middleware를 다시 생성하고 위 문제를 처리하는 별도의 로직을 생성하는 것이다. (app.js의 session을 초기화 했던 부분에 이어서...) 추가 되는 로직은 request.session을 이용하는 것이 아니라, request.user와 같이 새로운 request의 Key와 Value이다. (코드 참고할 것) 이렇게 하면 이전처럼 매 Request마다 Middleware를 거치게 되지만, 이전의 문제와는 달리 Session을 기반한 데이터를 이용한 것이므로 문제가 되지 않는다. 이는 Mongoose의 좋은 Method들을 쓸 수 있는 것은 아니지만, 직접 데이터에 접근하여 처리함으로써 Error없이 해결할 수 있다.
다른 방법도 있다. MongoDB를 이용하여 Session을 저장하고 관리할 때, 사용자 정보 User Model을 request.session.user로 둔다면, User Model 및 User의 데이터에 대한 접근에 어려움이 (타입 관련으로 인한...) 생길 수 있다. 이에 대해서는 별도의 Middleware를 통해서 request.session.user가 User Model임을 명시해야 한다. 따라서 Mongoose를 이용하는 경우 가장 초기 Middleware에 init() Method를 통해 해당 Model로 초기화를 해주면 된다.
두 방식 모두, 로그아웃과 같이 사용자의 정보를 잃었을 때의 별도 처리가 필요하다.
** 전자의 경우, request.session에 user의 정보가 없다면 next()를 호출하도록 Middleware를 수정하면 된다.

Two Tiny Improvements

일반적으로 Session이 생성되고 나서는, Session 값이 변경될 때마다 일일이 save() Method를 호출할 필요는 없다. 하지만 Session이 생성되었음을 확실히 하고 싶을 때엔 해당 Method를 사용하게 되고, save() Method안에는 Callback Function을 통해서 Error가 있는지 확인할 수 있다. 만일 Error가 없다면, Error가 없을 때 처리할 로직을 구현할 수 있다.