- What is Authentication?
- Implementing Signup & Login
- Sessions & Cookies
In many websites, certain areas should only be accessible by authenticated users.
1. User Sign Up: create account with email + password
2. User Login: enter email + password
3. User Authentication: grant access to protected pages
1. User Sign Up
비밀번호가 데이터베이스에 텍스트 그대로 저장될 때 해결하는 방법? : 암호를 해시!
- Understanding Password Hashing
- To render security-relevant data useless in case of a data breach, you should hash it.
- Hashing: converting a string to a non-decodable, diffrenet string
=> Securely hashed values can't be reverted, decoded or transformed back into the original value.
npm install bcryptjs
- 유명한 패키지: becrypt.js를 먼저 설치해준다. 그런 다음 routes를 다루는 파일에 bcrypt를 불러와서 사용해준다. 해시는 프로미스를 반환하기 때문에 꼭 await을 추가해준다.
const bcrypt = requrie("bcryptjs"); router.post('/signup', async function (req, res) { const userData = req.body; const enteredemail = userData.email; //userData['email'] const enteredConfirmEmail = userData['confirm-email']; const enteredPassword = userData.password; const hashedPassword = await bcrypt.hash(enteredPassword, 12); const user = { email: enteredemail, password: hashedPassword, } db.getDb().collection('users').insertOne(user); res.redirect('/login'); });
2. User Login
: 사용자 이메일과 비밀번호가 데이터베이스에 저장된 정보와 맞는지 확인하기
먼저 사용자가 입력한 이메일 정보가 없다면 Could not log in 메시지를 콘솔창에 뜨게 하고 다시 로그인 페이지로 가게 한다. 그리고 이메일 정보가 있다면 위에서 해시된 상태로 저장된 비밀번호와 사용자가 입력한 비밀번호가 동일한지 확인해야 한다. 따라서bcrypt.compare(해시되지 않은 값, 해시된 값)
을 사용해서 패스워드 정보를 확인한다.router.post("/login", async function (req, res) { const userData = req.body; const enteredEmail = userData.email; const enteredPassword = userData.password; const existingUser = await db .getDb() .collection("users") .findOne({email : enteredEmail}); if (!existingUser) { console.log('Could not log in!'); return res.redirect('/login'); } const passwordAreEqual = await bcrypt.compare(enteredPassword, existingUser.password); if (!passwordAreEqual) { console.log('Could not log in - passwords are not equal!'); return res.redirect('/login'); } console.log('User is authenticated!'); res.redirect('/admin');n });
사용자가 회원 가입시 제대로된 이메일로 한번만 가입할 수 있게 하려면?
Sign Up 과정에서 사용자가 빈칸으로 입력하지 못하도록, 그리고 제대로된 이메일 형식을 지키도록, 비밀번호가 맨 처음과 뒤의 공백을 제거하고도 6글자 이상이 되도록 등등 여러가지 옵션을 걸어 확인해준다. - 강의에서는
trim
을 사용해주었다!
또한 이메일이 데이터베이스에 존재하는지도 확인한다. 이 다음에 다시 위와 같이 비밀번호 해시 과정 코드들을 입력해주면 된다.router.post("/signup", async function (req, res) { const userData = req.body; const enteredEmail = userData.email; const enteredConfirmEmail = userData["confirm-email"]; const enteredPassword = userData.password; if ( !enteredEmail || !enteredConfirmEmail || !enteredPassword || enteredPassword.trim() < 6 || enteredEmail !== enteredConfirmEmail || !enteredEmail.includes("@") ) { console.log("Incorrect data"); return res.redirect("/signup"); } //POST 요청의 경우 render 보다는 redirect를 사용한다. //HTML 콘텐츠를 다시 보내는 것을 피하려하기 때문에 사용자가 창을 새로고침하면 경고창이 뜨기 때문
- redirect를 하면 2가지 요청을 브라우저에게 알린다.
1. 브라우저가 redirect 해야하는 것
- Redirected page를 보여주기
3. User Authentication
: 웹 사이트의 특정 영역을 인증되지 않은 액세스로 부터 보호할 수 있도록 해야함!
웹 사이트의 일부 부분을 어떻게 잠글 수 있을까?
- An 'entry ticket' must be saved on the server and handed out to the visitor! 일종의 입장 티켓을 생성하는 코드를 작성해야 하고 이 코드를 사용자에게 나눠주어야 한다.
=> Session with unique ID (e.g. database document/ record)
모든 세션은 고유한 아이디를 갖는다. 그리고 이 세션을 사용자에게 매핑할 수 있다. => Cookie (contains session ID) : response/ request header data
Session & Cookies
- Server-side: Session
- Client-side: Session Cookie
- Sessions and Cookies are NOT exclusively used for authentication! 쿠키는 사용자를 추적할 때도 사용될 수 있다.
- 세션과 쿠키는 보통 패키지를 사용해서 구현한다.
- Session: express-session (이것만 설치하면 cookie-parser도 설치된다)
- Cookie: cookie-parser
npm install express-session // 강의에서는 노드로 몽고db와 세션을 다루는 패키지도 설치하였다. npm install connect-mongodb-session
app.js
... const session = require("express-session"); const mongodbStore = require("connect-mongodb-session"); ... const MongoDBStore = mongodbStore(session); ... const sessionStore = new MongoDBStore({ uri: "mongodb://localhost:27017", databaseName: "auth-demo", collection: "sessions", }); ... app.use( session({ secret: "super-secret", resave: false, saveUninitialized: false, store: sessionStore, // cookie: { // maxAge: 30 * 24 * 60 * 60 * 1000 // } }) ); ...
세션에 인증 데이터 저장 & 액세스 제어를 위한 세션, 쿠키사용
router.post("/login", async function (req, res) { ... req.session.user = { id: existingUser._id, email: existingUser.email }; req.session.isAuthenticated = true; req.session.save(function () { res.redirect("/admin"); }); }); ... router.get("/admin", function (req, res) { if (!req.session.isAuthenticated) { // if (!req.session.user) return res.status(401).render("401"); } res.render("admin"); });
로그아웃 => 세션에서 해당 인증 데이터를 삭제
router.post("/logout", function (req, res) { req.session.user = null; req.session.isAuthenticated = false; res.redirect("/"); });