나의개발일지
[Express+mongoose] AuthenticationAuthorization 본문
본 글은 작성자가 어디선가 주워듣고 이해한 내용들을 개인적인 언어로 작성한 게시물입니다.
잘못된 내용이 존재할 수 있으니, 읽게 되신다면 이점을 감안해 주세요!!!
- 우리는 결국 자신이 가진 이야기로 상대방을 이해할 수 있을 뿐이다.-
웹애플리케이션에서 중요한 기능 중 하나는 인증(Authentication)과 권한(Authorization)이다. 인증과 인가는 일반적인 웹애플리케이션이라면 모두 가지고 있는 기능 중 하나로 로그인과 로그인 유지와 관련된 내용이다. python을 이용한 인증과 권한을 구현한 경험은 있지만, node에서는 처음이기에 공부하면서 이해한 내용을 정리하고자 한다. 공부하면서 느낀 점은 django를 이용한 인증 권한 구현과 대략적인 흐름은 비슷하다는 것이었다. 다른 점이 있었다면, express에서도 JWT를 이용할 수 있으나 이번에는 session을 이용해서 인증과 권한을 구현했다는 것이다. 세션은 이전 포스팅 글에 정리한 내용이 있기에 이전 포스팅 글을 기본으로 인증과 인가를 구현했다. 권한을 구현하는 방법으로는 크게 세션과 JWT가 있는데, 이번에는 세션을 이용했고, JWT는 따로 공부를 해서 확인해 봐야겠다.
인증 Authentication과 권한 Authorization 그리고 기초적인 내용
인증은 쉽게 말해서 내가 누구인지 확인시키는 것이다. 사용자의 아이디와 이메일과 같은 고유한 식별자를 이용한다. 만약 동일한 식별자를 이용하게 되면 같은 사람이 두 명이 있을 수 있기 때문에 반드시 사용자별로 고유한 식별자를 이용해야 한다. 그래서 보통 회원가입을 할 때, 아이디가 중복되었는지 확인하는 절차가 있다. 권한은 인증이 이루어진 후 가능한 것으로 특정 사용자가 가능한 행동을 확인하는 것이다. 예를 들어서 권한이 없는 사람이 데이터를 삭제하거나 하면 안 되기 때문에, 특정행동을 하는 곳에 권한이 필요한 코드를 넣어서 아무나 그 행동을 할 수 없게 하는 것을 말한다.
이제 인증과 권한에 대해서 알았다면, 구현하는 방법에 대해서 좀 더 정리해 보자!!! 먼저 회원가입 시 유저아이디와 패스워드를 받는다고 가정하자. 이때 유저아이디는 고유한 식별자여야 한다. 사용자로부터 아이디와 패스워드를 받았다면 이제 데이터베이스에 해당 값을 저장하게 된다. 이때 저장하는 패스워드는 그대로 저장해도 될까??? 절대 never 안 된다. 저장할 때는 반드시 암호화를 해서 저장을 해야 한다. 그 이유는 데이터베이스가 만약 해킹을 당할 수 있기 때문이다. 또한 해당 웹애플리케이션의 데이터베이스가 해킹된 것이 다른 웹사이트의 비밀번호까지 유추 가능 할 수 있게 한다. 무슨 말이냐면, 사용자는 보통 동일한 패스워드를 많이 사용한다. 그렇기 때문에, 한 웹사이트에서 사용한 비밀번호를 다른 웹사이트에서도 사용할 수 있다는 것이다. 그렇기 때문에 우리의 웹애플리케이션뿐만 아니라 다른 웹애프리케이션에도 피해를 끼칠 수 있다는 것이다. 그러므로 반드시 암호화해서 저장해야 한다. 암호화는 해시함수를 이용해서 저장하고 그 결과를 저장한다.
해시함수는 임의적 크기의 데이터를 입력하면 함수가 고정된 크기의 데이터를 출력한다. 해시함수의 특징은 단방향 함수라는 것이다. 단방향함수는 한쪽에서만 방향이 있는 것으로 암호화된 데이터에서 원본 데이터를 유추할 수 없다. 다른 특징으로는 작은 변화 예를 들어 알파벳 하나만 바뀌어도 그 결괏값이 완전히 바뀐다는 것이다. 다음 특징으로는 동일한 입력은 동일한 값을 출력한다. 이는 레이보우 테이블 어택을 가능하게 할 수도 있지만, 뒤에 언급할 솔트를 통해서 막을 수 있다. 해시솔트란 암호화 시 별도의 값을 넣어서 암호를 알아내는 것을 어렵게 하는 것을 말한다. 이런 솔트를 뿌리는 것과 암호화하는 것을 도와주는 모듈이 있는데 바로 bcrypt이다. bcrypt는 대표적인 암호화 해시함수로 많이 사용된다. bcrypt모듈을 이용해서 쉽게 암호화 및 원본 데이터와 비교가 가능하다.
이제 회원가입을 했고, 로그인을 한다고 가정하면 우리는 웹사이트에서 내가 누구인지 알 수 있는 인증 단계를 마칠 수 있다. 하지만, 아직 권한 단계가 완료된 것이 아니다. 보통 http프로토콜의 요청과 요청은 공유(?)되지 않는다. stateless 한 상태이다. 그렇기 때문에 한 번 로그인을 했다고 해서 다음 단계에서 로그인이 필요하지 않은 것이 아니다. 그렇다면 우리는 페이지가 이동할 때마다 로그인을 해주어야 하는 것일까? 그렇다면 정말 번거로울 것이다. 이때 필요한 것이 로그인을 유지하는 것이고 나아가 권한을 설정하는 것이다. http프로토콜에 상태를 유지하는 방법으로 우리는 이전에 세션을 알아보았다. 그렇기 때문에 세션을 이용해서 로그인을 유지하고 권한을 줄 수도 있다. 세션을 이용해서 사용자의 브라우저에 로그인이 됐다는 데이터를 연관시키는 것이다. 많은 방법이 있지만, 작성자는 로그인한 사용자의 mongoose의 아이디를 저장하는 방법을 이용할 것이다. 로그인을 성공하면 세션에 사용자의 id를 추가해서 권한이 필요한 곳에서 확인을 하는 것이다. 이는 브라우저가 요청을 보낼 때마다 쿠키가 자동적으로 보내지는 점을 이용해서 구현할 수 있는 것이다.
인증과 권한 프로젝트
인증과 권한을 구현하는 간단한 프로젝트를 작성해 보자 회원가입, 로그인, 로그아웃, 그리고 게시물을 만들고 게시물은 작성자만 볼 수 있게 만들어보자!!!!!
User와 Post모델 만들기
//post 모델
const mongoose = require("mongoose");
const postSchema = new mongoose.Schema({
title:{
type:String,
required:true
},
content:{
type:String,
required:true
}
})
const Post = mongoose.model("Post", postSchema);
module.exports = Post;
//user 모델
const mongoose = require("mongoose");
const bcrypt = require("bcrypt")
const userSchema = new mongoose.Schema({
username:{
type:String,
required:[true, "Username cannot be black"],
unique:true
},
password:{
type:String,
required:[true, "Username cannot be black"],
},
posts:[{
type:mongoose.Schema.Types.ObjectId,
ref:"Post"
}]
})
const User = mongoose.model("User", userSchema);
module.exports = User;
username의 unique를 true로 설정해서 중복해서 같은 값이 들어가지 못하게 막는다. 유저와 게시물의 관계는 1대 N의 관계로 한 명의 유저는 많은 게시물을 가질 수 있다. 게시물은 반드시 한 명의 작성자가 존재해야 한다. 데이터를 저장하는 방식은 embedding이 아닌 reference방식으로 부모인 user가 post의 오브젝트 아이디를 가지고 있다.
초기 설정
//app.js
const express = require("express")
const app = express()
const mongoose = require("mongoose")
const path = require("path");
// 암호화를 위한 모듈 임포트
const bcrypt = require("bcrypt");
// 세션이용을 위해 모듈 임포트
const session = require("express-session");
const Post = require("./models/post");
const User = require("./models/user");
mongoose.connect('mongodb://127.0.0.1:27017/loginDemo',{
// username의 unique를 위한 설정
autoIndex: true, //make this also true
})
.then(()=>{
console.log("MONGO CONNECTION OPEN!!!")
})
.catch((err)=>{
console.log("MONGO ERROR")
console.log(err)
})
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");
app.use(express.urlencoded({extended:true}))
app.use(session({secret:"secret"}))
////////////////////////////////////////////////////////
로직 채우기
////////////////////////////////////////////////////////
app.listen(3000, ()=>{
console.log("Server Starting")
})
회원가입 만들기
//app.js
app.get("/register", (req,res)=>{
res.render("register")
})
app.post("/register", async(req,res)=>{
const {username, password} = req.body;
const user = new User({username, password});
await user.save()
// 회원가입 후 바로 로그인이 된 상태로 유지한다.
req.session.user_id = user._id
res.redirect("/homepage")
})

첫 번째 get라우터핸들러는 회원가입 랜더링 폼을 위한 것이다. 랜더링 한 폼에서 데이터를 username과 password를 받아서 데이터베이스에 저장하게 된다. 회원가입이 성공하면 session에 user_id라는 데이터를 넣어준다. user_id는 데이터베이스에 생성되는 user의 고유식별자이 _id에 해당한다. 여기서 알아야 하는 것은 password를 암호화해서 저장해야 한다는 것이다. 하지만 위의 코드를 보면 암호화하는 코드가 없다. 사실 위의 로직에 암호화하는 코드를 작성해도 되지만 mongoose의 미들웨어를 이용해서 작성을 할 수도 있다. 암호화하는 로직을 미들웨어 쪽으로 빼는 것이다.
//유저 모델
userSchema.pre("save", async function(next){
// password의 저장이 아닌 다른 변경사항에 대해서는 password를 다시 암호화할 필요가 없다.
if(!this.isModified("password")) return next();
this.password = await bcrypt.hash(this.password,12)
next()
})
위와 같이 유저 모델의 스키마에 미들웨어로 save()가 발생하기 전에 특정 함수를 작동하게 한다. 위의 코드에서 패스워드를 암호화한다. bcrypt의 hash() 메서드를 이용해서 this.password = 사용자에게 입력받은 패스워드를 암호화한다.
로그인하기
//app.js
app.get("/login", (req, res)=>{
res.render("login")
})
app.post("/login", async(req,res)=>{
const{username, password} = req.body;
const foundUser = await User.findAndValidate(username, password);
if (foundUser){
req.session.user_id = foundUser._id
res.redirect("/homepage")
}else{
res.redirect("/login")
}
})

첫 번째는 로그인폼을 랜더링 하는 라우트 핸들러이고 두 번째는 로그인을 하는 라우트 핸들러이다. username과 password를 받아서 데이터베이스에 있는 데이터와 일치하는지 확인한다. 일치하는지 확인하는 코드는 findAndValidate() 함수로 정적메서드를 새롭게 정의해 준 것이다. 모델에서 정의할 수 있다.
//유저 모델
userSchema.statics.findAndValidate = async function(username, password){
const foundUser = await this.findOne({username});
const isValid = await bcrypt.compare(password, foundUser.password);
// isValid가 true라면 foundUser를 isValid가 false라면 false를 반환한다.
return isValid ? foundUser : false
}
findAndValidate()의 반환값이 유저라면 세션에 user_id에 해당 유저의 데이터베이스 고유식별자 id를 저장한다. 반환값이 false라면 로그인 페이지로 이동한다. findAndValidate() 함수에서 사용되는 것은 bcrypt의 메서드인 compare() 메서드로 저장된 암호화된 패스워드와 입력받은 패스워드가 같은지 확인한다.
로그아웃하기
//app.js
app.post("/logout",(req,res)=>{
// session에서 id만 지우는 것이다.
// req.session.user_id = null;
// 만약 많은 정보를 저장하고 있다면 destory로 모든 정보를 지울 수 있다.
req.session.destroy()
res.redirect("/login");
})
session에 있는 정보를 지우면 로그아웃이 된다. 만약 지워야 하는 정보가 user_id 만이라면 위와 같이 null로, 정보가 많다면 destory() 메서드를 이용해서 정보를 모두 지운다.
권한확인하기
//app.js
// 미들웨어를 이용한 로그인 확인
const requireLogin = (req,res,next)=>{
if (!req.session.user_id){
return res.redirect("/login")
}
next();
}
미들웨어를 이용해서 로그인 확인이 필요한 권한확인이 필요한 곳에 위의 미들웨어를 콜백함수로 넣어준다. req.session.user_id가 없다면 login페이지로 이동시킨다. 만약 로그인이 되었다면 다음 함수를 호출하면 된다. 여기서 권한에 대한 문제는 session에 저장된 user_id와 비교해서 추가적인 작업이 필요할 것이다.
로그인된 유저만 게시물 만들기
//app.js
app.get("/post", requireLogin, (req,res)=>{
res.render("newpost");
})
app.post("/post", requireLogin, async (req,res)=>{
const {title, content} = req.body;
const id = req.session.user_id;
const post = new Post({title, content});
const user = await User.findById(id)
await post.save();
user.posts.push(post);
await user.save();
res.redirect("/homepage")
})
requireLogin 미들웨어를 콜백함수로 넣어주어서 먼저 확인한 뒤 뒤의 코드를 이어간다. 만약 로그인이 된 상태라면 새로운 게시물을 만들 수 있는 페이지로 이동한다.

게시물이 등록이 완료되면 homepage로 이동하게 되는데 homepage는 로그인된 유저만 볼 수 있으며, 로그인한 유저는 자기가 작성한 글만 볼수 있다.
//app.js
app.get("/homepage", requireLogin, async(req,res)=>{
const id = req.session.user_id;
const user = await User.findById(id).populate("posts")
res.render("homepage", {user})
})
populate로 참조하는 post의 값을 가지고 와서 같이 보내준다.


session에서 user_id를 가지고 와서 해당 유저의 게시물 목록만 보여주도록 만들 수 있다. 만약 모든 게시물을 가지고 오고 해당 유저만 게시물을 수정하거나 삭제하는 코드도 session에서 id를 가지고 와서 작업할 수 있을 것이다.
정리하자면
아마 오늘 정리한 내용은 많이 부족한 부분이 있을 것이다. 하지만 이런 흐름으로 로그인과 회원가입 그리고 권한의 확인 등이 이루어질 것이라는 예감이 든다. 실제로는 에러처리등 좀 더 복잡한 처리가 많이 있을 수 있으나 그것은 다음에 다시 한번 작업하는 것으로 하자!!!! 앞서 말한 데로 세션을 이용해서 작업을 해주었지만, 이외에도 다른 방법이 있을 수 있다.
본 게시물은 작성자가 공부한 내용을 토대로 정리한 글입니다.
잘못된 내용이 포함될 수 있습니다.
재미있다?? 즐겁다!!!!!!
'NodeJS.Express.MongoDB' 카테고리의 다른 글
| [Express - passport] passport 여권 인증 인가 <<><><><>< (1) | 2023.01.28 |
|---|---|
| [express+cloudinary] 이미지를 저장하자!!! (0) | 2023.01.27 |
| [Express] 맛있는 cookie!!!! 멋있는 session!!! (0) | 2023.01.23 |
| [mongoose] mongoose의 1:N 관계를 설정해보자!!! (0) | 2023.01.22 |
| [Joi] 유효성 검사를 해 볼까????? 해보자!!!!!! (0) | 2023.01.21 |