본문 바로가기

Javascript/Node.js

mongoose로 Mongodb 사용하기 (+passport 로그인 인증, 보안)

반응형

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에러 메시지를 읽어보면 거기서 다 해결법이 나온다.ㅠㅠ

반응형