회원가입 페이지 + 비밀번호 암호화
우선 회원가입 페이지부터 만들기
로그인 페이지와 입력하는 레이아웃을 유사하니 login.ejs 파일 그대로 가져와서 form 태그의 action 만 /register로 변경하고 submit 을 회원가입으로 변경하기
<!doctype html>
<html>
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css"
integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
<link rel="stylesheet" href="/public/main.css">
<title>NodeJS+MongoDB</title>
</head>
<body>
<%- include('nav.html')%>
<h4 class="container mt-4"><strong>회원가입 페이지</strong></h4>
<div class="container mt-3">
<form action="/register" method="POST">
<div class="form-group">
<label>Id</label>
<input type="text" class="form-control" name="id">
</div>
<div class="form-group">
<label>PW</label>
<input type="text" class="form-control" name="pw">
</div>
<button type="submit" class="btn btn-danger mt-1">회원가입</button>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous">
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous">
</script>
<script>
</script>
</body>
</html>
그리고 server.js 에서도 signup 페이지를 GET, POST 요청시에 대한 코드도 작성하기
여기서 주의할 점은 회원 기능이 필요하다면 passport 셋팅하는 부분이 위에 있어야한다고 한다.
// GET
app.get("/signup", (req, res) => {
res.render("signup.ejs");
});
// POST
app.post("/register", (req, res) => {
db.collection("login").insertOne(
{ id: req.body.id, pw: req.body.pw },
(err, data) => {
res.render("login.ejs", { data: "로그인 성공" });
}
);
});
다만 위의 POST 요청시 고려해야하는 점이 몇가지가 있는데
- 저장전에 ID가 이미 있는지 중복 검사를 했는지
- ID에 알파벳 또는 숫자만 들어가 있는지
- 비밀번호 저장시 암호화를 했는지
강의에는 위의 고려사항을 하지 않았지만 따로 해보고 싶어서 했음.
우선 첫 번째 ID 중복 검사에 대한 코드는 아래와 같다.
app.post("/register", (req, res) => {
db.collection("login")
.find({ id: req.body.id })
.toArray((err, data) => {
console.log(data);
if (data.length === 0) {
// 배열로 가져와지는 data의 length 즉 길이로 판단했음.
}
});
});
두 번째로 ID에 알파벳 또는 숫자만 들어가 있는지 체크하는 코드는 아래와 같다.
// 정규식 파악 함수
// pattern의 /(슬래시) 사이에 작성하는 것으로
// ^ 와 $ 로 시작과 끝을 알려주고
// [] 사이에 a-zA-Z0-9 로 소문자 a 부터 z, 대문자 A 부터 Z, 0부터 9까지만 포함되도록 하기.
function isTrueId(id) {
const pattern = /^[a-zA-Z0-9]+$/;
return pattern.test(id);
}
app.post("/register", (req, res) => {
db.collection("login")
.find({ id: req.body.id })
.toArray((err, data) => {
console.log(data);
if (data.length === 0) {
// 해당 데이터가 없다면 정규식 검사로 알파벳 또는 숫자만 들어가 있는지 확인
if (isTrueId(req.body.id)) {
// 확인되고 만약 잘 작성되었다면 비밀번호 암호화 후 DB에 저장하기 <----- 비밀번호는 저장하지 않고 salt와 hash 값을 저장해줘서 로그인시 매칭할때 사용하기
}
}
});
});
세 번째로 비밀번호 저장시 암호화를 했는지를 위한 코드
// 암호화
const crypto = require("crypto");
// 정규식 확인 함수
function isTrueId(id) {
const pattern = /^[a-zA-Z0-9]+$/;
return pattern.test(id);
}
// 비밀번호 해시화 함수
// salt와 hash를 만들어줘서 login 시 비교하도록 하기.
// 여기서 암호화한 해쉬 알고리즘은 sha512를 사용
function hashPassword(pw) {
const salt = crypto.randomBytes(16).toString("hex"); // 임의의 salt 생성
const hash = crypto.pbkdf2Sync(pw, salt, 1000, 64, "sha512").toString("hex"); // 입력한 비밀번호와 salt를 가지고 비밀번호를 해싱
return {
salt: salt,
hash: hash,
};
}
app.post("/register", (req, res) => {
db.collection("login")
.find({ id: req.body.id })
.toArray((err, data) => {
console.log(data);
if (data.length === 0) {
// 해당 데이터가 없다면 정규식 검사로 알파벳 또는 숫자만 들어가 있는지 확인
if (isTrueId(req.body.id)) {
// 확인되고 만약 잘 작성되었다면 비밀번호 암호화 후 DB에 저장하기 <----- 비밀번호는 저장하지 않고 salt와 hash 값을 저장해줘서 로그인시 매칭할때 사용하기
const { salt, hash } = hashPassword(req.body.pw);
db.collection("login").insertOne(
{ id: req.body.id, salt: salt, hash: hash },
(err, data) => {
if (!err) {
res.render("login.ejs", { data: "성공" });
}
}
);
}
}
});
});
여기서 끝이 아니라
초기 로그인시엔 암호화 되지 않은 비밀번호로 로그인을 했기에 로직을 변경해줘야 한다. (귀찮…)
// 기존 로그인시 post 요청하는 코드
app.post(
"/login",
passport.authenticate("local", {
failureRedirect: "/fail",
}),
(req, res) => {
// 아이디, 비번 맞으면 메인 페이지로 보내주기.
res.redirect("/");
}
);
위의 코드는 form 태그 안에 있는 input 태그의 name 속성으로 object를 넘겨주는 passport로 로그인을 하는 코드이기에 passport를 사용하는 코드였던 아래 코드를 건드려줘야한다.
아래 코드는 기존 코드
passport.use(
new localStrategy(
{
usernameField: "id", // form 태그의 name 속성의 id 값
passwordField: "pw", // form 태그의 name 속성의 pw 값
session: true, // 로그인 후 세션을 저장할 것인지에 대한 셋팅
passReqToCallback: false, // 아이디/비밀번호 말고도 다른 정보로 검증을 할 것인지에 대한 셋팅
// 만약 passReqToCallback: true로 한다면 밑의 (id, pw, done) 에서 req를 추가해서 req.body 하면 데이터가 나온다고 한다.
},
(id, pw, done) => {
// done(1번째 인자: 서버에러와 같은 db 연결 불가 등, 2번째 인자: 요청을 성공했을 때 사용자 db 데이터 만약 실패의 경우 false 넣어야 함, 3번째 인자: 에러 메시지)
db.collection("login").findOne({ id: id }, (err, data) => {
if (err) return done(err);
if (!data)
// 결과가 없다면 --> db에 대항 아이디가 없다면
return done(null, false, { msg: "존재하지 않는 아이디 입니다." });
if (pw == data.pw) {
// pw가 암호화 되어있지 않기에 보안이 좋지 않지만 지금은 우선 이렇게 진행함.
// db에 아이디가 있다면, 입력한 비밀번호와 db에 있는 비번이랑 확인해보기.
// 아이디와 비번이 맞아서 로그인을 성공하면 세션 정보를 만들어줘야 함 (로그인 했는지 확인하기 위해)
return done(null, data); // <--- 여기 성공시의 data가 257번줄의 serializeUser((user, done))의 user 데이터에 들어가게 됨
} else {
return done(null, false, { msg: "잘못된 비밀번호 입니다." });
}
});
}
)
);
아래 코드가 변경된 코드
// 로직 순서는 아래와 같다.
// db.collection의 login 에서 form 태그 안의 name이 id인 input 태그의 value에 해당하는
// id를 찾아서 해당 data를 찾은 다음 checkPassword 메서드 안에
// form 태그 안의 name이 pw인 input 태그의 value인 pw와 data로 가져온 salt와 hash값을 전달해
// 비밀번호가 맞는 값인지에 대한 return 값을 boolean으로 가져와서
// 맞다면 로그인 시켜주고 세션을 저장한 다음 쿠키를 브라우저에 저장하기.
function checkPassword(pw, salt, savedHash) {
const hash = crypto.pbkdf2Sync(pw, salt, 1000, 64, "sha512").toString("hex"); // 입력된 비밀번호를 해시화
return hash === savedHash;
}
passport.use(
new localStrategy(
{
usernameField: "id", // form 태그의 name 속성의 id 값
passwordField: "pw", // form 태그의 name 속성의 pw 값
session: true, // 로그인 후 세션을 저장할 것인지에 대한 셋팅
passReqToCallback: false, // 아이디/비밀번호 말고도 다른 정보로 검증을 할 것인지에 대한 셋팅
// 만약 passReqToCallback: true로 한다면 밑의 (id, pw, done) 에서 req를 추가해서 req.body 하면 데이터가 나온다고 한다.
},
(id, pw, done) => {
// done(1번째 인자: 서버에러와 같은 db 연결 불가 등, 2번째 인자: 요청을 성공했을 때 사용자 db 데이터 만약 실패의 경우 false 넣어야 함, 3번째 인자: 에러 메시지)
db.collection("login").findOne({ id: id }, (err, data) => {
// 해당 아이디의 salt, hash 값 가져오기
const isPW = checkPassword(pw, data.salt, data.hash);
if (err) return done(err);
else if (!data)
// 결과가 없다면 --> db에 대항 아이디가 없다면
return done(null, false, { msg: "존재하지 않는 아이디 입니다." });
else if (isPW) {
console.log("로그인 했다!");
// pw가 암호화 되어있지 않기에 보안이 좋지 않지만 지금은 우선 이렇게 진행함.
// db에 아이디가 있다면, 입력한 비밀번호와 db에 있는 비번이랑 확인해보기.
// 아이디와 비번이 맞아서 로그인을 성공하면 세션 정보를 만들어줘야 함 (로그인 했는지 확인하기 위해)
return done(null, data); // <--- 여기 성공시의 data가 257번줄의 serializeUser((user, done))의 user 데이터에 들어가게 됨
} else {
return done(null, false, { msg: "잘못된 비밀번호 입니다." });
}
});
}
)
);
'NodeJS+MongoDB' 카테고리의 다른 글
NodeJS+MongoDB Part3 - 이미지 업로드 & 이미지 서버 만들기 (0) | 2023.06.28 |
---|---|
NodeJS+MongoDB Part3 - 게시판, API 관리 (0) | 2023.06.27 |
NodeJS+MongoDB Part3 - Search (0) | 2023.06.26 |
NodeJS+MongoDB Part3 - User (Session, JWT, OAuth) (0) | 2023.06.23 |
NodeJS+MongoDB Part2 + PUT (0) | 2023.06.23 |