Mongoose 미들웨어
Mongoose는 Node.js에서 MongoDB를 조금 더 간편하게 다룰 수 있게 해주는 미들웨어다.
Mongoose가 MongoDB에 연결하고 모델을 만들 수 있으며 다양하게 사용가능하다.
몽구스를 이용하면서 passport 미들웨어를 통한 로그인을 구현하는 프로필 애플리케이션을 만든다.
(feat. cookie-parser, bcrypt-nodejs, connect-flash, express-session ...)
* bcrypt-nodejs는 bcrypt 대신 사용함. 이유는 bcrypt가 C코드를 사용해서 C코드 컴파일러 설정이 잘되어 있어야 사용가능한데 예제에서 문제를 일으키기 싫어서 대체. 속도가 느려진다면 나중에 bcrypt로 사용해야함.
프로필 애플리케이션 만들기 & 분석
1 2 3 4 5 6 7 8 9 10 11 12 | "dependencies": { "express": "^4.15.3", "body-parser":"^1.6.5", "bcrypt-nodejs":"0.0.3", "connect-flash":"^0.1.1", "cookie-parser": "^1.3.2", "ejs":"^1.0.0", "express-session":"^1.7.6", "mongoose":"^4.11.0", "passport":"^0.2.0", "passport-local":"^1.0.0" } |
1) package.json에 미리 사용할 미들웨어를 적고 한번에 npm install 명령으로 설치한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | var mongoose = require("mongoose"); var bcrypt = require("bcrypt-nodejs"); //해시 알고리즘 적용 회수, 높을수록 보안은 높음 속도는 느려짐 var SALT_FACTOR = 10; //몽구스 요청하고 필드 정의 var userSchema = mongoose.Schema({ username: {type:String,required:true,unique:true}, password: {type:String,required:true}, createdAt:{type:Date,default:Date.now}, diplayName: String, bio: String }); //모델에 간단한 메서드 추가 userSchema.methods.name = function(){ return this.displayName || this.username; }; //bcrypt를 위한 빈 함수 var noop = function(){}; //모델이 저장되기("save") 전(.pre)에 실행되는 함수 userSchema.pre("save",function(done){ var user = this; if(!user.isModified("password")){ return done(); } bcrypt.genSalt(SALT_FACTOR,function(err,salt){ if(err){return done(err);} bcrypt.hash(user.password,salt,noop,function(err,hashedPassword){ if(err){return done(err);} user.password = hashedPassword; done(); }); }); }); // 비밀번호 검사하는 함수 userSchema.methods.checkPassword = function(guess,done){ bcrypt.compare(guess,this.password,function(err,isMatch){ done(err,isMatch); }); }; //실제로 사용자 모델만들고 내보내기 var User = mongoose.model("User",userSchema); module.exports = User; |
[models/user.js]
2) mongoose 모델만들기 - 스키마, 속성, 메서드 가능
- 몽구스를 불러서 스키마를 만든다. (username, password등)
- 스키마에 함수를 만들 수도 있다. (여기선 name()메서드가 displayname or username 출력함)
- 비밀번호를 암호화해서 저장하는 함수. (userSchema가 저장되기 전에 비밀번호 사이사이에 salt(소금)을 쳐서 암호화시킴.)
- 비밀번호가 매치되는지 검사하는 함수. (추측값(guess)와 password가 일치하는지 bcrypt의 compare 메서드로 확인한다. (해커의 타이밍공격 방어/ ===으로 했으면 공격당할 수 있다.)
- 이렇게 작성한 스키마를 모델로 만들어 사용할 수 있도록 내보낸다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | var express = require("express"); var mongoose = require("mongoose"); mongoose.Promise = global.Promise; var path = require("path"); var bodyParser = require("body-parser"); var cookieParser = require("cookie-parser"); var session = require("express-session"); var flash = require("connect-flash"); var passport = require("passport"); var route = require("./routes");//라우트분할 var setUpPassport = require("./setuppassport"); var app = express(); //test 데이터베이스로 연결 mongoose.connect("mongodb://localhost:27017/test",{useMongoClient: true}); setUpPassport(); app.set("port",process.env.PORT || 3000); app.set("views",path.join(__dirname,"views")); app.set("view engine","ejs"); app.use(bodyParser.urlencoded({extended:false})); app.use(cookieParser()); app.use(session({ secret:"TKRvOIJs=HyqrvagQ#&!f!%V]Ww/4KiVs$s,<<MX",//임의의 문자 resave:true, saveUninitialized:true })); /*secret : 각 세션이 클라이언트에서 암호화되도록함. 쿠키해킹방지 resave : 미들웨어 옵션, true하면 세션이 수정되지 않은 경우에도 세션 업데이트 saveUninitialized : 미들웨어 옵션, 초기화되지 않은 세션 재설정*/ app.use(flash()); app.use(passport.initialize()); app.use(passport.session()); app.use(route); app.listen(app.get("port"),function(){ console.log("Server started on port"+app.get("port")); }); | cs |
[app.js]
3) MongoDB 연결
- mongoose.connect를 이용해서 몽고디비에 연결 (당연히 MondoDB가 켜져있어야 함.)
- procees.env.PORT || 3000은 환경변수의 포트가 있으면 그 포트를 쓰고 아니면 3000포트를 사용한다는 뜻. (heroku,AWS에서 서버돌릴 때 유용)
- passport와 쿠키, 세션은 나중에 설명.
- 라우트와 패스포트설정을 분할하고 서버를 키는 처리.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | var express = require("express"); var passport = require("passport"); var User = require("./models/user"); var router = express.Router(); //템플릿용 변수 설정 router.use(function(req,res,next){ res.locals.currentUser = req.user; res.locals.errors = req.flash("error"); res.locals.infos = req.flash("info"); next(); }); //컬렉션을 쿼리하고, 가장 최근 사용자를 먼저 반환(descending) router.get("/",function(req,res,next){ User.find().sort({createAt:"descending"}) .exec(function(err,users){ if(err){return next(err);} res.render("index",{users:users}); }); }); router.get("/signup",function(req,res){ res.render("signup"); }); router.post("/signup",function(req,res,next){ var username = req.body.username; var password = req.body.password; User.findOne({username:username},function(err,user){ if(err){return next(err);} if(user){ req.flash("error","사용자가 이미 있습니다."); return res.redirect("/signup"); } var newUser = new User({ username:username, password:password }); newUser.save(next); }); },passport.authenticate("login",{ successRedirect:"/", failureRedirect:"/signup", failureFlash:true })); router.get("/users/:username",function(req,res,next){ User.findOne({username:req.params.username},function(err,user){ if(err) {return next(err);} if(!user){return next(404);} res.render("profile",{user:user}); }); }); router.get("/login",function(req,res){ res.render("login"); }); router.post("/login",passport.authenticate("login",{ successRedirect: "/", failureRedirect: "/login", failureFlash : true })); router.get("/logout",function(req,res){ req.logout(); res.redirect("/"); }); function ensureAuthenticated(req,res,next){ if(req.isAuthenticated()){ next(); }else{ req.flash("info","먼저 로그인해야 이 페이지를 볼 수 있습니다."); res.redirect("/login"); } } router.get("/edit",ensureAuthenticated,function(req,res){ res.render("edit"); }); //put메서드는 현재 html에서 get post만 되니까 post로 일단 구현 router.post("/edit",ensureAuthenticated,function(req,res,next){ req.user.displayName = req.body.displayname; req.user.bio = req.body.bio; req.user.save(function(err){ if(err){next(err);return;} req.flash("info","Profile updated!"); res.redirect("/edit"); }); }); module.exports = router; |
[routes.js]
4) 라우트 설정
- res.locals.currentUser가 있는 라우트에서 템플릿에서 사용할 변수들을 설정한다. (전역)
- User 모델을 불러와서 .find() = 조회 요청 쿼리를 만든다. 그 후에 .exec() 실행하고 find() 전체 조회에 따른 리스트들이 오면 리스트와 함께 index.ejs로 렌더링 시킴.
- findOne({username:username}) 은 하나만 조회하는 것으로 username이 username인 것을 찾음. (몽고디비의 각 document마다 고유의 _id 가 있지만 여기서는 username도 unique 설정했으니까 조회할 때 사용할 수 있다.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Learn About Me</title> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"> </head> <body> <div class="navbar navbar-default navbar-static-top" role="navigation"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="/">Learn About me</a> </div> <ul class="nav navbar-nav navbar-right"> <% if (currentUser) {%> <li> <a href="/edit">Hello, <%= currentUser.name() %></a> </li> <li> <a href="/logout">Log out</a> </li> <%} else{%> <li> <a href="/login">Log in</a> </li> <li> <a href="/signup">Sign up</a> </li> <% } %> </ul> </div> </div> <div class="container"> <% errors.forEach(function(error){%> <div class="alert alert-danger" role="alert"> <%= error %> </div> <% }) %> <% infos.forEach(function(info){ %> <div class="alert alert-info" role="alert"> <%= info %> </div> <% }) %> | cs |
[views/_header.ejs]
1 2 3 4 | </div> </body> </html> |
[views/_footer.ejs]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <% include _header%> <h1>Welcome to Learn About me!</h1> <% users.forEach(function(user){%> <div class="panel panel-dafault"> <div class="panel-heading"> <a href="/users/<%=user.username%>"><%=user.name()%></a> </div> <% if(user.bio){%> <div class="panel-body"> <%=user.bio%> </div> <% } %> </div> <% }) %> <% include _footer%> |
[views/index.ejs]
1 2 3 4 5 6 7 8 9 | <% include _header %> <h1>Sign up</h1> <form action="/signup" method="post"> <input name="username" type="text" class="form-control" placeholder="Username" required autofocus /> <input name="password" type="password" class="form-control" placeholder="Password" required /> <input type="submit" value="Sign up" class="btn btn-primary btn-block" /> </form> <% include _footer %> | cs |
[views/signup.ejs]
5) passport
전형적인 웹 애플리케이션에서 사용자를 인증하는데 사용된 자격증명은 로그인 요청 동안에 전송된다.
인증이 성공하면, 세션이 구성되고 사용자의 브라우저에서 쿠키 집합을 통해 유지된다.
이 후 요청은 자격증명이 포함되는 것이 아니라, 그 세션을 식별하는 고유 쿠키를 포함한다.
로그인 세션을 지원하기 위해, passport에서는 세션에서 사용자 인스턴스를 직렬화하거나 역직렬화한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | var passport = require("passport"); var User = require("./models/user"); var LocalStrategy = require("passport-local").Strategy; passport.use("login",new LocalStrategy(function(username,password,done){ User.findOne({username:username}, function(err,user){ if(err){return done(err);} if(!user){ return done(null,false,{message:"No user has that username!"}); } user.checkPassword(password,function(err,isMatch){ if(err){return done(err);} if(isMatch){ return done(null,user); }else{ return done(null,false,{message:"Invalid password."}); } }); }); })); module.exports = function(){ //사용자 개체를 id로 전환 passport.serializeUser(function(user,done){ done(null,user._id); }); //id를 사용자 개체로 전환 passport.deserializeUser(function(id,done){ User.findById(id,function(err,user){ done(err,user); }); }); }; |
[setuppassport.js]
실제 인증은 Strategy를 설정하는 것이다. ( Strategy에서 페이스북, 구글등 사이트와의 인증 포함(API))
1)사용자 이름으로 사용자 찾고
2)사용자가 없으면 "없는 사용자다"하고 끝
3)사용자가 있으면 비밀번호 일치 여부 비교, 일치하면 현재 사용자 반환, 그렇지않으면 "잘못된 비번" 반환
* 공부하면서 든 의문이 몇가지 있는데 다른 예제를 보면서 이해하고 다시 정리.
1 2 3 4 5 6 7 8 9 10 11 12 13 | <% include _header%> <% if((currentUser) && (currentUser.id===user.id)){%> <a href="/edit" class="pull-right">Edit your profiles</a> <% } %> <h1><%= user.name()%></h1> <h2>Joined on <%= user.createAt %></h2> <% if(user.bio){%> <p> <%=user.bio%> </p> <% } %> <% include _footer%> |
[views/profile.ejs]
1 2 3 4 5 6 7 8 9 | <% include _header%> <h1>Log in</h1> <form action="/login" method="post"> <input name="username" type="text" class="form-control" placeholder="Username" required autofocus /> <input name="password" type="password" class="form-control" placeholder="Password" required /> <input type="submit" value="Login" class="btn btn-primary btn-block" /> </form> <% include _footer%> | cs |
[views/login.ejs]
1 2 3 4 5 6 7 8 9 | <% include _header %> <h1>Edit your profile</h1> <form action="/edit" method="post"> <input name="displayname" type="text" class="form-control" placeholder="Dispaly name" value="<%= currentUser.displayName || "" %>" /> <textarea name="bio" class="form-control" placeholder="Tell us about yourself"><%= currentUser.bio || ""%></textarea> <input type="submit" value="Update" class="btn btn-primary btn-block" /> </form> <% include _footer %> | cs |
[views/edit.ejs]
에러메시지 대처
DeprecationWarning: Mongoose: mpromise (mongoose's default promise library) is deprecated, plug in your own promise library instead: http://mongoosejs.com/docs/promises.html
이런 메시지가 있다면
require("mongoose"); 문장 밑에다가
mongoose.Promise = global.Promise; 입력.
에러메시지 대처 2
DeprecationWarning: `open()` is deprecated in mongoose >= 4.11.0, use `openUri()` instead, or set the `useMongoClient` option if using `connect()` or `createConnection()`. See http://mongoosejs.com/docs/connections.html#use-mongo-client
이런 메시지가 있다면
mongoose.connect("mongodb://localhost:27017/test",{useMongoClient: true});
처럼 커넥트하는 부분뒤에 {useMongoClient:true}입력.
* 사실 경고or에러 메시지를 읽어보면 거기서 다 해결법이 나온다.ㅠㅠ
'Javascript > Node.js' 카테고리의 다른 글
Express generator (Express 프레임워크 구조화 및 관리) (2) | 2017.10.05 |
---|---|
웹 개발하면서 처리해야할 보안, 최소한의 방어, 정보보안 (0) | 2017.10.01 |
MongoDB 설치 방법, 환경설정, 몽고 디비 구조 (0) | 2017.09.30 |
Pug 문법 정리 요약 (템플릿 엔진) (17) | 2017.09.29 |
Node.js API 구축 방법 기초 (8) | 2017.09.28 |