이번 sprint는 뒤의 Token과 OAuth에 비해 쉽게 이해할 수 있는 sprint였다. 이번에도 비슷하게 server와 client를 왔다갔다 하면서 하려고 했지만, 나눠서 해도 무리가 없을것 같아 나누어서 시작한다.
먼저 server부터 살펴보자.
이번스프린트에서 마찬가지로 설정해주어야 할 것은 환경변수의 설정과 https를 사용하기위한 인증서를 등록해 두는 것이다.
교육용(?)으로 발급하기위한 사설 인증서를 받기 위해서는 mkcert라는 설치하여 로컬환경에서 신뢰할 수 있는 인증서를 만들어야한다. urclass에 설명이 잘 나와 있다.
$ brew install mkcert
위의 명령어로 설치를 마쳤다면, 로컬을 인증된 발급기관으로 추가해야 한다.
$ mkcert -install
다음은 로컬환경에 대한 인증서를 만들어야 한다. localhost로 대표되는 로컬 환경에 대한 인증서를 만들려면 다음 명령어를 입력해야 한다.
$ mkcert -key-file key.pem -cert-file cert.pem localhost 127.0.0.1 ::1
이제 옵션으로 추가한 localhost, 127.0.0.1(IPv4), ::1(IPv6)에서 사용할 수 있는 인증서가 완성된 것이다. cert.pem, key.pem이라는 파일이 생성된 것을 확인할 수 있다.
위에서 만든 인증서를 node의 https 모듈을 이용하려면 아래와 같이 설정해 주면 되겠다.
const https = require('https');
const fs = require('fs');
https
.createServer(
{
key: fs.readFileSync(__dirname + '/key.pem', 'utf-8'),
cert: fs.readFileSync(__dirname + '/cert.pem', 'utf-8'),
},
function (req, res) {
res.write('Congrats! You made https server now :)');
res.end();
}
)
.listen(3001);
마찬가지로 express를 사용한다면 아래와 같이 설정한다.
const https = require('https');
const fs = require('fs');
const express = require('express');
const app = express();
https
.createServer(
{
key: fs.readFileSync(__dirname + '/key.pem', 'utf-8'),
cert: fs.readFileSync(__dirname + '/cert.pem', 'utf-8'),
},
app.use('/', (req, res) => {
res.send('Congrats! You made https server now :)');
})
)
.listen(3001);
참조
[WEB] 🌐 SSL 이란 (Secure Socket Layer)
다음으로 sprint에 있는 index.js파일을 살펴보자.
const express = require('express');
const cors = require('cors');
const session = require('express-session');
const logger = require('morgan');
const fs = require('fs');
const https = require('https');
const usersRouter = require('./routes/user');
const app = express();
const FILL_ME_IN = 'FILL_ME_IN';
const PORT = process.env.PORT || 4000;
// TODO: express-session 라이브러리를 이용해 쿠키 설정을 해줄 수 있습니다.
app.use(
session({
secret: '@codestates',
resave: false,
saveUninitialized: true,
cookie: {
domain: FILL_ME_IN,
path: FILL_ME_IN,
maxAge: 24 * 6 * 60 * 10000,
sameSite: FILL_ME_IN,
httpOnly: FILL_ME_IN,
secure: FILL_ME_IN,
},
})
);
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// TODO: CORS 설정이 필요합니다. 클라이언트가 어떤 origin인지에 따라 달리 설정할 수 있습니다.
// 메서드는 GET, POST, OPTIONS를 허용합니다.
app.use(cors({}));
/**
* /users 요청에 대해서 라우터를 이용하기 때문에,
* 반드시 아래와 같은 주소와 메서드로 요청을 보내야 합니다.
*
* POST https://localhost:4000/users/login,
* POST https://localhost:4000/users/logout,
* GET https://localhost:4000/users/userinfo
*/
app.use('/users', usersRouter);
let server;
// 인증서 파일들이 존재하는 경우에만 https 프로토콜을 사용하는 서버를 실행합니다.
// 만약 인증서 파일이 존재하지 않는경우, http 프로토콜을 사용하는 서버를 실행합니다.
// 파일 존재여부를 확인하는 폴더는 서버 폴더의 package.json이 위치한 곳입니다.
if (fs.existsSync("./key.pem") && fs.existsSync("./cert.pem")) {
server = https
.createServer(
{
key: fs.readFileSync(__dirname + `/` + 'key.pem', 'utf-8'),
cert: fs.readFileSync(__dirname + `/` + 'cert.pem', 'utf-8'),
},
app
)
.listen(PORT);
} else {
server = app.listen(PORT)
}
module.exports = server;
index.js파일을 보면 어느부분을 채워야할지 대략적으로 눈에 보이는게 몇가지가 있는것 같다. 그렇다면 우리가 위에서 설정을 어떤 것을 왜 해주는지에 대한 답을 찾아야겠다.
참조
urclass에도 내용이 있지만, 공식문서에 대한 내용을 잘 살펴보자.
secure
- 브라우저가 HTTPS를 통해서만 쿠키를 전송하도록 합니다.httpOnly
- 쿠키가 클라이언트 JavaScript가 아닌 HTTP(S)를 통해서만 전송되도록 하며, 이를 통해 XSS(Cross-site scripting) 공격으로부터 보호할 수 있습니다.domain
- 쿠키의 도메인을 표시합니다. URL이 요청되고 있는 서버의 도메인에 대해 비교할 때 사용하십시오. 두 도메인이 일치하는 경우에는 그 다음으로 경로 속성을 확인하십시오.path
- 쿠키의 경로를 표시합니다. 요청 경로에 대해 비교할 때 사용하십시오. 이 경로와 도메인이 일치하는 경우에는 요청되고 있는 쿠키를 전송하십시오.expires
- 지속적 쿠키에 대한 만기 날짜를 설정하는 데 사용됩니다.위의 값들을 참조하여 설정하고 cors에 대한 설정도 해보자.
참조
[express] [미들웨어] [cors] [express-session]
[TIL #18] React - Credential 이란?
위의 참조를 보면 express로 cors옵션을 설정할 수 있는 tip들이 존재하여 가져왔다. 위의 참조들을 이용하려 session에 사용할 쿠키에 대한 설정을 해주면 되겠다.
위의 부분의 설정에서 쿠키를 위한 설정을 따로 잡아주어야 하는 것이 문제였었다. 참조에도 있듯, cors 설정에서 credentials 설정을 true로 해주어야 브라우져에서 cookie의 사용이 허락이 되는 것이다. 하여 이 설정도 같이 넣어주면 되겠다.
const express = require('express');
const cors = require('cors');
const session = require('express-session');
const logger = require('morgan');
const fs = require('fs');
const https = require('https');
const usersRouter = require('./routes/user');
const app = express();
const FILL_ME_IN = 'FILL_ME_IN';
const PORT = process.env.PORT || 4000;
// TODO: express-session 라이브러리를 이용해 쿠키 설정을 해줄 수 있습니다.
app.use(
session({
secret: '@codestates',
resave: false,
saveUninitialized: true,
cookie: {
domain: "localhost",
path: "/",
maxAge: 24 * 6 * 60 * 10000,
sameSite: "None",
httpOnly: true,
secure: true,
},
})
);
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// TODO: CORS 설정이 필요합니다. 클라이언트가 어떤 origin인지에 따라 달리 설정할 수 있습니다.
// 메서드는 GET, POST, OPTIONS를 허용합니다.
app.use(cors({
origin : 'https://localhost:3000',
method : ['post', 'get', 'options'],
credentials : true
}));
/**
* /users 요청에 대해서 라우터를 이용하기 때문에,
* 반드시 아래와 같은 주소와 메서드로 요청을 보내야 합니다.
*
* POST https://localhost:4000/users/login,
* POST https://localhost:4000/users/logout,
* GET https://localhost:4000/users/userinfo
*/
app.use('/users', usersRouter);
let server;
// 인증서 파일들이 존재하는 경우에만 https 프로토콜을 사용하는 서버를 실행합니다.
// 만약 인증서 파일이 존재하지 않는경우, http 프로토콜을 사용하는 서버를 실행합니다.
// 파일 존재여부를 확인하는 폴더는 서버 폴더의 package.json이 위치한 곳입니다.
if (fs.existsSync("./key.pem") && fs.existsSync("./cert.pem")) {
server = https
.createServer(
{
key: fs.readFileSync(__dirname + `/` + 'key.pem', 'utf-8'),
cert: fs.readFileSync(__dirname + `/` + 'cert.pem', 'utf-8'),
},
app
)
.listen(PORT);
} else {
server = app.listen(PORT)
}
module.exports = server;
// 해당 모델의 인스턴스를 models/index.js에서 가져옵니다.
const { Users } = require('../../models');
module.exports = {
post: async (req, res) => {
}
}
}
코드를 만들어 구현하기 전에 이 js파일이 어떠한 역할을 하는지 생각해보자. 현재 controller와 연결이 되어있고, route로 /uers/login으로 들어오게 되어있다.(controller/users/index에 보면, route가 연결 되어있음.)
하여 해당 파일은 클라이언트에서 로그인요청이 들어올 때, 입력된 id와 password가 데이터베이스와 일치하게 되면 적절한 응답을 돌려주게 되는것이다. session sprint에서는 데이터를 session이라는 곳에 저장하고 session_id를 클라이언트에게 전달해주면, 클라이언트는 데이터 요청을 할 때 마다, session_id를 확인 받고 서버에 데이터를 받게 된다. 이러한 흐름을 생각하면서 코드를 접해보자.
먼저 로그인의 요청이 어디로 들어오는지 알아야 한다. 하여 console.을 찍어보자.
commend
console.log(req.body)
result
{ userId: 'kimcoding', password: '1234' }
클라이언트에서 login요청을 할때 위와 같이 body에 해당 값이 들어온다고 생각하면 되겠다. 그렇다면 이 값들이 데이터베이스에 있는지를 확인해야겠다.
참조
이전에 배웠던 sequelize의 findOne을 사용하여 데이터베이스에 해당 값들이 들어있는지 확인해보자.
const userInfo = await Users.findOne({
where : {userId : req.body.userId, password : req.body.password}
})
위의 명령어로 해당하는 데이터베이스를 확인할 수 있게 되는 것이다.
여기서 2가지 조건을 생각할 수 있을 것이다. 해당 Id나 password가 다를 경우나 맞을 경우에 대한 조건이 있어야겠다.
userInfo를 가져올 수 없을 때는 데이터베이스에 해당값이 없는 것이므로, 유저가 잘못 입력한 값일테니 400번대 에러를 전해주면 되겠다.
그리고 만약 데이터베이스에 값이 존재한다면, 그 값을 session에 저장하고, 해당하는 session_id의 값을 클라이언트에게 전달해주면 되겠다.
if(!userInfo){
res.status(400).json({data : null, message : "not authorized"})
}
위와 같은 명령어를 입력하고 나는 고민에 빠진 것이 있었다. 도대체 해당 데이터를 session어디에 저장할지 막막한 것이였다.
대부분 session store라는 것을 사용한다고 한다. 세션이 데이터를 저장하는 장소를 말하는데 파일에 저장하는 session-file-store라는 것이 있고, mysql에 저장하는 mysql-session 등 여러 모듈이 존재한다.
하지만 index.js를 찾아보아도, package.json을 살펴봐도 해당 모듈이 없는 것을 확인할 수 있었다. 도대체 어디다가 저장을 할 것인가?
참조
[Node.js] session store의 물리적 위치
[EXPRESS] 📚 express-session 세션 미들웨어
현재 스프린트에서는 Inmemory 방식을 사용한다. 일회성인 방식이며, 서버를 닫게 되면 해당 session에 저장한 데이터가 모두 지워지게 되는 잘 안쓰는 방식이라고 한다.
현재 우리는 세션관리용 미들웨어인 express-session을 사용하고 있다. 세션은 사용자별로 req.session객체 안에 유지 되므로, 이 객체 안에 사용할 key와 값을 부여해 넣어주면 되는것이였다.
하여 In memory방식의 접근으로 session에 데이터를 저장해보자.
만약, 데이터베이스에서 userInfo를 잘 가져오고 req.session에 우리가 원하는 데이터를 넣을 곳의 key 가 존재하지 않는다면 만들어주어야 할 것이다.
else{
if(req.session.userId === undefined){
req.session.userId = userInfo.userId
res.status(200).json({data:userInfo, message: 'ok'})
}
}
사실 이 부분에서도 의문이 갓던 것이 session에 어떤 키의 이름으로 넣어 줄 것인가, 어떤 데이터를 넣어줄 것인가에 대한 고민을 하였었는데, test case에 내용이 있어서(?) 위와같이 구현하게 되었다.
이렇게 되면 login.js의 구현이 마무리 되었다.
// 해당 모델의 인스턴스를 models/index.js에서 가져옵니다.
const { Users } = require('../../models');
module.exports = {
post: async (req, res) => {
const userInfo = await Users.findOne({
where: { userId: req.body.userId, password: req.body.password },
});
console.log(req.body)
if (userInfo) {
if(req.session.userId === undefined) {
req.session.userId = userInfo.userId
res.status(200).json({data : userInfo, message: 'ok'})
}
} else {
res.status(400).json({data:null,message:"not authorized"}
}
}
}
자 이제 다음 userinfo.js 파일의 코드를 구성해보자. 이 파일은 로그인이 허용된 클라이언트가 유저의 데이터 요청을 받는 역할을 한다. 위에서도 말했듯, 로그인이 되면 session_id를 받게 되고, Token과 비슷하게 데이터 요청을 할 때, req.session에 해당값이 들어오게 된다.
const { Users } = require('../../models');
module.exports = {
get: async (req, res) => {
}
};
서버는 req.session에 들어오는 값을 확인하고, 그 값이 맞다면 데이터베이스에서 값을 찾아 클라이언트에게 보내주게 되는 것이다.
그렇다면 여기서도 똑같이 req.session이 어떻게 들어오고 있는지 console을 찍어보자.
commend
console.log(req.session)
result
Session {
cookie: {
path: '/',
_expires: 2022-03-23T01:40:35.809Z,
originalMaxAge: 86400000,
httpOnly: true,
secure: true,
domain: 'localhost',
sameSite: 'None'
},
userId: 'kimcoding'
}
위와 같은 값들이 들온다. login에서 req.session.userId로 입력한 값이 잘 들어오는 것을 확인할 수 있고, 서버에서 보내준 session이 확인 되었으므로 해당 요청에 대한 값을 보내주면 되겠다.
위의 코드에서와 마찬가지로, req.session이 존재하지 않는다면, 클라이언트 에러를 400번대로 보내주면 되겠다.
const sessionId = req.session.userId
if(!sessionId){
res.status(400).json({data : null, message : not authorized})
}
else{
const result = await Users.findOne({
where : {userId : sessionId}
})
res.status(200).json({data : result, message : "ok"})
}
이로서, userInfo.js는 쉽게 마무리 되었다.
const { Users } = require('../../models');
module.exports = {
get: async (req, res) => {
const sessionId = req.session.userId
if (!sessionId) {
res.status(400).json({data: null, message: "not authorized"})
} else {
try{
const result = await Users.findOne({
where: { userId: sessionId },
});
res.status(200).json({data : result, message : 'ok'})
}catch(err){console.log(err)}
}
},
};
참조
마지막으로 logout을 구현해 보면 되겠다.
module.exports = {
post: (req, res) => {
},
};
해당 파일은 logout버튼을 눌렀을 때 클라이언 요청에 관한 데이터를 넘겨주어야하는 곳이다. 로그아웃을 하는데 어떤 값을 넘겨주어야 할까를 고민해보았다. 딱히 어떤 ‘값’을 넘겨준다기 보다는, session에 저장되어있는 데이터를 지우고, data에 있는 값들을 null로 전해주어 state에 저장되어있는 값들을 모두 초기화 시키는 것이지 않을까 생각해본다.
그렇다면, 먼저 console로 req가 어떻게 들어오는지 살펴보자.
commend
console.log(req.session)
result
Session {
cookie: {
path: '/',
_expires: 2022-03-23T01:40:35.809Z,
originalMaxAge: 86400000,
httpOnly: true,
secure: true,
domain: 'localhost',
sameSite: 'None'
},
userId: 'kimcoding'
}
여기서도 userInfo에서 구현했던것과 같이 req.session.userId가 없을 경우와 있을경우로 나누면 되겠다. 해당 값들을 나눈 후 userId가 존재한다면, session을 삭제해야할 것이다.
참조
[MongoDB] Session 저장/삭제하기 - 로그인/로그아웃 구현(Node.js, express-session,Mongoose)
위의 코드의 반복이라 굳이 설명은 하지않고 logout을 완성해보았다.
위의 코드의 반복이라 굳이 설명은 하지않고 logout을 완성해보았다.
module.exports = {
post: (req, res) => {
const sessionId = req.session.userId
if (!sessionId) {
res.status(400).send({data:null, message: "not authorized"})
} else {
req.session.destroy((err) => {
if(err) return console.log(err)
else res.status(200).json({data : null, message:'ok'})
})
}
},
};