
Node.js + MongoDB를 공부해보자! 위대하신 코딩애플(codingapple.com)님과 함께:)
CSS 파일은 관습적으로 public 폴더에 보관한다고 한다.
(CSS, 이미지 등 잘 안 바뀌는 static 파일들은 다 public에 넣어 주면된다.)
다만, link태그만 단순히 HTML 파일 위에 첨부하면 되는게 아니라,
서버용 JS 파일에도 public 폴더가 있다는 사실을 알려주어야 한다.
<script>
app.use('/public', express.static('public'))
</script>
여러가지 페이지에 동일한 요소가 있는 경우, 변경되었을 때 모든 파일을 다시 수정하는 것을 너무나도 불합리하고 비효율적인 업무 방식이다.
navbar 같은 경우가 그러한데, 이럴 때 별도로 html 파일을 만들어서 빼주고, 그 파일을 다른 파일들에 include(첨부)하여 사용하도록 하자!
(.html이 아닌 .ejs 에서 사용되는 문법)
<%- include('nav.html') %>
그러나, form 태그에서는 method란에 GET과 POST만 가능하기 때문에,
PUT 요청을 하기 위해서는 DELETE처럼 Ajax를 사용하거나 / method-override라는 라이브러리를 활용하면 된다.
method-overrid라는 라이브러리를 사용하기 위해서는
<script>
// HTML에서 PUT/DELETE 요청을 위한 method-override 등록
const methodOverrice = require("method-override");
app.use(methodOverrice("_method"));
</script>
<form action="/add?_method=PUT" method="POST">
<input>
</form>
++) 그렇다면, edit 페이지를 위한 코드는 대략 이렇게 된다.
<script>
// /edit?_method=PUT 이라는 api로 put 요청이 오면,
app.put("/edit", function (req, res) {
db.collection("post").updateOne(
// 해당 request의 name이 id인 값을 _id로 가진 데이터를 찾고
{ _id: parseInt(req.body.id) },
// 제목과 날짜를 다음 같이 $set한다.
{ $set: { 제목: req.body.title, 날짜: req.body.date } },
function (err, result) {
console.log("터미널에 표시 : ToDo 수정완료");
// response에 /list라는 api로 redirect하도록 한다 :)
res.redirect("/list");
}
);
});
</script>
특징 : 세션을 서버에 다 저장함. 장점이자 단점
? 쿠키 : 브라우저에 저장할 수 있는 긴 문자열
특징 : 세션을 서버에 저장할 필요가 없음.
? 토큰 : 암호화된 긴 문자열. 유통기한이 있는 열쇠라고 생각하면 됨
특징 : 별도의 id, pw가 필요없고 유저가 정보입력을 하지 않아도 동의를 통해 불러올 수가 있음. 단점은 연동되는 타 사이트가 사라지게 된다면... ToT
<script>
// Session 방식 로그인 기능 구현을 위한 라이브러리 연결
// req와 res사이의 미들웨어로 등록하기
const passport = require("passport");
const LocalStrategy = require("passport-local");
const session = require("express-session");
app.use(session({ secret: "비밀코드", resave: true, saveUninitialized: false }));
app.use(passport.initialize());
app.use(passport.session());
</script>
? 미들웨어 : request와 response 사이에 실행시키는 코드들. 요청이 적법한지 검사하는 기능들을 보통 많이 담는다.
아래 코드의 의미는 "/login 요청과 응답(res) 사이에 passport 라이브러리가 제공하는 Local 방식의 인증 과정을 거치고, 실패할 시에는 /fail로 이동하고 성공하면 홈페이지(/)로 이동하자" 이다.
<script>
app.post('/login', passport.authenticate('local', {failureRedirect : '/fail'}), function(req, res){
res.redirect('/')
});
</script>
그렇다면 local 방식으로 어떻게 아이디와 비밀번호를 검사하는지 정의하는데,
<script>
passport.use(
new LocalStrategy(
{
usernameField: "id", // form의 name이 id 인 것이 username
passwordField: "pw", // form의 name이 pw 인 것이 password
session: true, // session을 저장할 것인지
passReqToCallback: false, // id/pw 외에 다른 정보 검증 시
},
function (inputID, inputPW, done) {
db.collection("login").findOne({ id: inputID }, function (err, result) {
if (err) return done(err);
// done 문법 (서버에러, 성공시 사용자 DB, 에러메세지)
if (!result)
return done(null, false, { message: "존재하지 않는 아이디 입니다." });
// 현재 암호화가 전혀 되어있지 않은 상태이기에 추후 변경 필요
if (inputPW == result.pw) {
return done(null, result);
} else {
return done(null, false, { message: "비밀번호가 일치하지 않습니다." });
}
});
}
)
);
</script>
대략적인 프로세스는 다음과 같다.
1. 로그인 페이지 제작 / 라우팅
2. 로그인 요청시 아이디/비번 검증 미들웨어 실행시키기
3. 아이디/비번 검증하는 세부 코드
4. 아이디/비번을 DB와 비교
5. 일치한다면 세션아이디를 발급 및 쿠키로 전송
아래 코드의 의미는 무엇일까?
<script>
// id를 이용해서 세션을 저장시키는 코드(로그인 성공 시)
passport.serializeUser(function (user, done) {
done(null, user.id);
});
// 이 세션 데이터를 가진 사람을 DB에서 찾는 코드.
// 하단 코드의 '아이디'는 윗 코드의 user.id이다.
passport.deserializeUser(function (아이디, done) {
// DB에서 user.id로 유저를 찾은 뒤에, 유저 정보를 {}안에 넣음\
db.collection("login").findOne({ id: 아이디 }, function (err, result) {
done(null, result);
});
});
</script>
passport의 serializerUser는 세션데이터를 만들고, 세션아이드를 쿠키로 만들어서 사용자의 브라우저로 보내주는 역할을 한다.
passport의 deserializerUser는 세션아이디에 숨겨져있던 유저의 아이디와 일치하는 로그인 정보를 찾아서, 그 결과를 반환해주는 역할을 한다. 그렇게 된다면 로그인 정보(id, pw, _id)가 req.user 부분에 꽂히게 된다.
회원가입 기능은 우선 다음과 같이 구현해보았다.
<script>
app.post("/register", (req, res) => {
db.collection("login")
.find({ id: req.body.id })
.toArray((err, result) => {
if (err) {
return console.log(err);
} else if (result.length === 0) {
db.collection("login").insertOne(
{ id: req.body.id, pw: req.body.pw },
(err, result) => {
res.redirect("/");
}
);
} else {
res.send("이미 존재하는 아이디입니다.");
}
});
});
</script>
해석하자면, /register라는 api로 POST요청이 들어오면, login이라는 collection에서 입력된 id를 찾아서 Array형태로 반환한다.
하지만, 비밀번호를 저장할 때, 암호화하지 않고 바로 pw : req.body.pw로 login collection에 저장한다는 문제점이 있다.
암호화하는 crypto라는 라이브러리를 사용해서 진행해보자.
구글에 있는 다른 분들의 설명을 참고해서 진행해 보았다.
<script>
crypto.randomBytes(64, (err, buf) => {
crypto.pbkdf2(req.body.pw, buf.toString("base64"),
100000, 64, "sha512", (err, key) => {
let encodeBUF = buf.toString("base64");
let encondePW = key.toString("base64");
});
});
</script>
대략 이런 코드가 들어가는데, 단방향 암호화를 사용해보았다.
순수하게 crypto만을 사용해서 암호화를 하였을 경우, 같은 비밀번호에 대해서 같은 암호화된 비밀번호 결과가 나오기 때문에, salt를 추가한다.
? salt : 말 그대로 소금을 치는 건데, 기존에 문자열에 salt를 붙여서 새로운 문자열을 만드는 것.
위의 코드에서는 10만번 salt를 하는데, 이렇게 해도 속도는 얼마 걸리지 않으며, 99999번째 salt와 10만번째는 완전히 다르다. 횟수도 10만처럼 깔끔한 숫자가 아니라 규칙없는 숫자면 더욱 보안에 좋다고 한다.
로그인을 할 경우에는,
1. 해당 아이디와 일치하는 아이디가 없는 경우 : 존재하지 않는 아이디입니다.
2. 해당 아이디와 일치하는 아이디가 있는 경우 : 유저 DB에서 salt(buf)를 가져와서, 입력된 PW를 암호화한 후 유저 DB의 암호화된 PW와 비교한다!
위의 Local 방식 검사의 일부를 이렇게 수정하면된다.
<script>
function (inputID, inputPW, done) {
//console.log(입력한아이디, 입력한비번);
db.collection("login").findOne({ id: inputID }, function (err, result) {
if (err) return done(err);
// done 문법 (서버에러, 성공시 사용자 DB, 에러메세지)
if (!result) return done(null, false, { message: "존재하지 않는 아이디 입니다." });
// buf 참조해서 암호화 및 비교진행
crypto.pbkdf2(inputPW, result.buf, 100000, 64, "sha512", (err, key) => {
let newPW = key.toString("base64");
console.log("newPW : ", newPW);
if (newPW == result.pw) {
return done(null, result);
} else {
return done(null, false, { message: "비밀번호가 일치하지 않습니다." });
}
});
});
}
</script>
포트번호나, DB 접속 문자열, 위의 crypto에서 salt의 횟수 등등
컴퓨터가 변경되면 바뀌어야하는 코드, 내 id와 pw같은 환경에 따라 가변적인 변수 데이터들을
보통 "환경변수"라고 부른다. (environment variable)
그래서 보통 개발자들은 미래에 대비해서 이러한 환경변수나 민감한 정보들을 .env파일에 저장하여 사용한다. 그래서 그 방법은?
ex)
PORT=8080
DB_URL="mongodb+srv://codingapple1@저쩌구"
굳이 .env파일이 아니더라도, Google이나 Naver나 AWS 등을 이용해서 서버를 발행할 때 비슷한 세팅을 할 가능성이 높다. 그러니 디테일보다는 개념과 필요한 이유를 기억하도록 하자:)