Node JS #11
로그인 기능과 관련된 간단한 복습을 해보자.
우리가 만든 세션 data는 cookie에 저장되지 않고, BE에 저장된다.
그래서 BE에서는 이 세션 DB에 데이터들을 저장한뒤 세션 ID를 FE에게 cookie 형태로 준다.
이제 BE에서 세션을 DB에 저장하는 것을 구현해보자.
지금은 서버를 재시작하게 되면 모든 세션 정보가 날라간다.
mongoDB의 세션 저장소인 connect-mongo를 사용하자.
Connect-mongo
MongoDB session store for Connect and Express written in Typescript.
Install
$ npm install connect-mongo
import session from "express-session";
import MongoStore from "connect-mongo";
app.use(
session({
secret: process.env.COOKIE_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: process.env.DB_URL,
}), // MongoUrl의 위치에 우리 세션을 저장, ex) mongodb://127.0.0.1:23421/wetube
// 다만 보안의 위해 따러 .env 파일을 만들어 위치를 숨김
})
);
이렇게 실제로 DB에 세션 정보가 저장된 것을 확인할 수 있다.
따라서 우린 서버가 재부팅되어도 세션 정보를 날리지 않을 수 있게 된다.
이 connection-mongo 패키지의 옵션들을 알아보자.
saveUninitialized
Forces a session that is “uninitialized” to be saved to the store. A session is uninitialized when it is new but not modified. Choosing
false
is useful for implementing login sessions, reducing server storage usage, or complying with laws that require permission before setting a cookie. Choosingfalse
will also help with race conditions where a client makes multiple parallel requests without a session.The default value is
true
, but using the default has been deprecated, as the default will change in the future. Please research into this setting and choose what is appropriate to your use-case.
초기화되지 않은 세션을 저장할 것인지의 여부를 체크한다.
만일 우리의 사이트를 방문하는 익명의 모든 사용자들에게 세션을 만들어 세션 ID를 발급해준다는 것은 매우 큰 비용이 드는 작업일 것이다.
따라서 로그인한 유저만 기억하면 비교적 적은 비용이 들 것이고, 이를 이 옵션을 이용해 구현한다.
saveUninitialized: true;
이 상태에서는 사이트를 익명의 브라우저(로그인 유저가 아니라 사용자를 기억할 가치가 없는 상태)에서도
세션 ID가 발급되어 있는 것을 볼 수 있다.
실제 DB에서 이 저장할 가치가 없는 브라우저의 정보를 열심히 저장하고 있는 것을 확인할 수 있고, 비용이 쓸데없이 많이 들것이다.
saveUninitialized: false,
이 옵션에 false를 주면 초기화 되지 않은 세션은 저장하지 않는다.
이렇게 로그인을 해야만 세션 정보를 초기화하므로 비로소 DB에 세션이 저장되는 것을 확인할 수 있고, 따라서 브라우저는 세션 ID로 쿠키로 받는다.
그렇다면 우리는 어디서 세션 정보를 초기화해주었을까?
바로 로그인처리 핸들러에서다!
export const postLogin = async (req, res) => {
const { username, password } = req.body;
const pageTitle = "Login";
//check if account exists
const user = await User.findOne({ username, socialOnly: false });
if (!user) {
return res.status(404).render("login", {
pageTitle,
errorMessage: "An account with this username does not exists.",
});
}
//check if password correct
const checkPassword = await bcrypt.compare(password, user.password);
if (!checkPassword) {
return res.status(404).render("login", {
pageTitle,
errorMessage: "Wrong password",
});
}
//session initialized!
req.session.loggedIn = true;
req.session.user = user;
return res.redirect("/");
};
따라서 순서는 다음과 같다.
- 사용자가 로그인 정보 입력
- 로그인 처리 핸들러 호출
- 로그인 처리 핸들러가 세션 초기화
- 세션이 초기화 되었으므로 서버 세션 DB에 저장
- 서버는 브라우저에 세션 ID 발급
resave
세션에 변경사항이 없을 때, 세션을 다시 저장할지의 유무를 결정한다.
세션의 TTL을 설정해주는 기능이 없으면 resave:true로 두어 매 request마다 세션을 업데이트 시킬 수 있다.
다만, 변경사항이 없는데도 세션을 계속 저장하면 비효율적이므로 resave:false를 사용할 수 있다.
Cookie
쿠기의 속성에는 아래와 같은 속성들이 있다.
Domain : 쿠키를 발행해주는 기관 도메인, 구글이면 구글에서만 통용되는 쿠키
Path : 경로
Secret : 비밀키
This is the secret used to sign the session cookie. This can be either a string for a single secret, or an array of multiple secrets. If an array of secrets is provided, only the first element will be used to sign the session ID cookie, while all the elements will be considered when verifying the signature in requests. The secret itself should be not easily parsed by a human and would best be a random set of characters
Expires : 만료 시점
Specifies the number (in milliseconds) to use when calculating the
Expires Set-Cookie
attribute. This is done by taking the current server time and addingmaxAge
milliseconds to the value to calculate anExpires
datetime. By default, no maximum age is set.If both
expires
andmaxAge
are set in the options, then the last one defined in the object is what is used.maxAge
should be preferred overexpires
.cookie: { maxAge: 1000 * 60 * 60 * 24 * 30, },
Enviroment Variables : dotenv
위에서 쿠키에는 Secret 값이 있다고 했다. 또한 MongoDB를 연결하는 과정에서 DB Url이 입력되는데, 만일 이러한 값들이 깃허브에 올릴때 그대로 노출된다면 보안상 취약해질 것이다.
따라서 우린 이런 환경 변수의 값을 숨겨야 한다.
dotenv 패키지를 사용해 이를 쉽게 구현하자.
🌱 Install
# install locally (recommended)
npm install dotenv --save
🏗️ Usage
how to use dotenv video tutorialyoutube/@dotenvorg
Create a .env file in the root of your project:
S3_BUCKET="YOURS3BUCKET"
SECRET_KEY="YOURSECRETKEYGOESHERE"
As early as possible in your application, import and configure dotenv:
require('dotenv').config()
console.log(process.env) // remove this after you've confirmed it is working
.. or using ES6?
import 'dotenv/config'
That's it. process.env now has the keys and values you defined in your .env file:
여기서 import를 가능한한 빠른 시점에 하라고 했는데,
package.json을 보면
"scripts": {
"dev": "nodemon --exec babel-node src/init.js"
},
우리는 src/init.js를 시작파일로 서버를 구동하므로 이것이 가장 빠른 시점인 것을 확인할 수 있다.
따라서 init.js에서
import "dotenv/config";
import를 시켜주면 된다. 만일 다른 파일에 import해서 뒤늦은 시점에 env가 사용시작될 경우, 그 앞에서는 env 변수를 이용할 수 없게되어 undefined가 뜬다.
환경변수들을 저장할 파일인 .env파일을 생성하자.
이때, .env에는 관례상 대문자로 적는다
//.env
COOKIE_SECRET=sdfds2389n3njk2l
DB_URL=mongodb://127.0.0.1:27017/wetube
//GH_CLIENT는 SECRET이 아니지만 접근성을 위해 여기다 정의
GH_CLIENT=20f72f7fd7a9a67cd510
GH_SECRET=f483dcbc82455c2e43f828ef9bcd104f5a8ee6b4
그리고 이 파일을 깃허브에 업로드하지 않기 위해 .gitignore파일에 기재하자.
//.gitignore
/node_modules
.env
/uploads
그후 모든 파일에서 process.env.{키 값} 으로 환경변수를 사용할 수 있다.
console.log(process.env.COOKIE_SECRET);
//OUTPUT : sdfds2389n3njk2l
Leave a comment