컴퓨터언어/Node.js

[Passport] Cookie, Session을 활용하여 로그인 상태를 기억하는 웹페이지를 만들어보자

bbanpro 2020. 5. 17. 18:51
728x90
반응형

쿠키와 세션은 모두 브라우저가 서버와 통신할 때 사용되는 개념이다.

 

쿠키는,

사용자가 해당 웹사이트에 방문하고 행동한 것을 저장한 후, 재방문했을 때 이전의 최종 행동에 기반하여 맞춤형 정보를 제공해준다.

즉 클라이언트가 웹사이트에 접속하면, 먼저 GET으로 페이지를 렌더링한다.

그리고 이어서 클릭 등의 여러가지 POST 요청을 하게 되고, 서버는 그에 맞는 Response와 함께 브라우저에 쿠키를 심어놓는다.

그래서 이후에 다른 GET 요청이 있는 경우에도 이전 POST 요청과 유사한(광고 등 관심사 노출) 또는 동일한(유저 로그인상태 또는 찜목록) 정보를 보여줄 수 있는 것이다.

 

세션은,

브라우저가 로그아웃 등으로 끊기지 않으면서 서버와 지속적으로 통신하고 있는 진행 시간을 의미한다.

예를 들어 아이디와 비밀번호를 입력한 후 로그인 버튼을 누르면,

그 순간 "로그인"이라는 세션은 시작된 것이며, 인증이 성공하면 "이 유저는 로그인이 성공했으므로 이 로그인 상태를 저장하겠습니다" 에 해당하는 쿠키도 생성 및 저장된다.

세션은 브라우저를 종료하거나 로그아웃을 하지 않는 한 현재 활성화된 로그인 정보 쿠키를 항상 체크하기 때문에, 이어서 페이지를 돌아다녀도 계속해서 로그인을 묻지 않는 것이다.

 

위의 쿠키와 세션을 모두 활용하여 안전한 인증을 도와주는 것이 바로 Passport 모듈이다.

http://www.passportjs.org/

 

Passport.js

Simple, unobtrusive authentication for Node.js

www.passportjs.org

 

하지만 Passport를 사용하기 위해서는 express-session을 포함한 여러가지 모듈이 필요하고, 코드의 순서가 엇갈리면 망한다. 세팅 절차는 이어서 소개한다.

https://www.npmjs.com/package/express-session

 

express-session

Simple session middleware for Express

www.npmjs.com


1. 모듈을 설치한다.

npm i passport passport-local passport-local-mongoose express-session

2. passport는 기존 md5같은 모듈이 가지고 있는 해싱 등 기능을 포함하고 있다.

md5 등 모듈을 사용중이라면 충돌하지 않도록 주석처리 또는 지워준다.

 

3. passport-local 빼고 모두 require 한다.

이는 passport-local-mongoose를 사용하기 위한 상위 패키지일 뿐이므로, require하지 않는다.

const session = require("express-session");
const passport = require("passport");
const passportLocalMongoose = require("passport-local-mongoose");

4. 다음과 같은 순서로 express-session과 passport를 세팅한다.

secret 값은 임의의 값이다.

app.use(session({
  secret: "ThisSecretIsEqualToCustomKeyForEncryption.",
  resave: false,
  saveUninitialized: true,
  cookie: { secure: true }
}));

app.use(passport.initialize());
app.use(passport.session());

5. passport-local-mongoose를 자신이 정의한 DB 스키마에 연결한다. passport-local-mongoose는 로컬 DB의 필드를 Hashing, Salting해준다. (useCreateIndex는 서드파티 특성상 유효가 지난(deprecated) 코드를 수정해주기 위한 옵션이다.)

mongoose.connect("mongodb://localhost:27017/userDB", {useNewUrlParser: true, useUnifiedTopology: true});
mongoose.set('useCreateIndex', true);

const userSchema = new mongoose.Schema({
  email: String,
  password: String
});

userSchema.plugin(passportLocalMongoose)

 

6. 스키마에 passport-local-mongoose 플러그인을 추가했다면, DB model을 생성해주고,

const User = new mongoose.model("User", userSchema);

이어서 로그인 인증을 위한 Passport의 두가지 기능인 Serialize와 Deserialize를 사용하기 위해 Passport Strategy를 설정한다.

.createStrategy()를 먼저해주고, 그 다음 Serialize, Deserialize도 설정해준다.

passport.use(User.createStrategy());
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());

 

Serialize : 유저 정보를 압축하여 쿠키로 만들어준다.

Deserialize : 쿠키를 가지고 현재 유저가 누구인지 파악한다.


현재까지 코드는 다음과 같다.

require("dotenv").config();
const express = require("express");
const bodyParser = require("body-parser");
require("ejs");
const mongoose = require("mongoose");
const session = require("express-session");
const passport = require("passport");
const passportLocalMongoose = require("passport-local-mongoose");
const app = express();

app.use(express.static("public"));
app.use(bodyParser.urlencoded({extended: true}));
app.set("view engine", "ejs");

app.use(session({
  secret: "ThisSecretIsEqualToCustomKeyForEncryption.",
  resave: false,
  saveUninitialized: true,
  cookie: { secure: true }
}));

app.use(passport.initialize());
app.use(passport.session());

mongoose.connect("mongodb://localhost:27017/userDB", {useNewUrlParser: true, useUnifiedTopology: true});

mongoose.set('useCreateIndex', true);

const userSchema = new mongoose.Schema({
  email: String,
  password: String
});

userSchema.plugin(passportLocalMongoose)

const User = new mongoose.model("User", userSchema);

passport.use(User.createStrategy());
 
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());

7. 이제 회원가입 POST를 처리하기 위해, app.post()를 담당하는 Route 내부에 코드를 작성한다.

이때 CRUD의 Create는 기존 mongoose의 .save()가 아니라, passport-local-mongoose의 .register()다.

 

.register() 함수는 DB모델의 .(dot)메서드로 접근되며,

첫번째 인자로는 해시가 필요없는 필드(id),

두번째 인자로는 해시가 필요한 필드(password),

세번째 인자로는 에러와 성공을 처리하기 위한 콜백함수를 전달한다.

 

회원가입 페이지가 /register에 존재한다고 가정하면, 코드는 다음과 같다.

app.route("/register")
  .get(function (req, res) {
    res.render("register")
  })

  .post(function (req, res) {
    User.register({username: req.body.username}, req.body.password, function (err, newUser) {
      if (err) {
        console.log(err);
        res.redirect("/register");
      } else {
        // HTML 폼에서 POST를 보내는 과정, 즉 회원가입 과정에서 에러가 없는 경우에 한해 아래 로그인세션 기억 코드(passport.authenticate())가 실행된다.
        passport.authenticate("local") (req, res, function () {
          // 아래 콜백함수 코드는 회원가입에 이어 그 상태로 로그인까지 연속해서 인증성공했고 + 해당 인증 내용을 바탕으로 하는 로그인 상태 쿠키 생성 성공일 때 실행된다.
          res.redirect("/secrets")
        })
      }
    })
  });

바로 여기서 이 포스트의 진가, Passport의 로그인세션 유지 효과가 드러난다!

지금은 우선 회원가입 페이지에서 DB에 새로운 유저정보를 넣었는데,

어떤 통신 오류가 없었다면 로그인까지 잘 이어지게 하였고,(저기 위에 4번에서  cookie: { secure: true } 를 삭제하면 로그인까지 한번에 된다)

passport.authenticate() 함수는 쿠키를 사용하여 해당 로그인 세션을 유지하도록 한 것이다!

authenticate() 함수는 serialize된 유저정보 쿠키를 브라우저에게 넘겨 이를 갖고있도록 하고, 서버와 계속 로그인 세션을 유지시킨다.

이는 이제부터 브라우저를 닫거나 로그아웃을 하지 않는 한 페이지를 돌아다녀도 로그인 상태가 유지된다는 것이다.

따라서 res.redirect("/secrets")처럼 로그인해야만 입장 가능한 어떤 secrets 페이지로 리다이렉트가 가능해졌다.

 

express-session 에게 박수~👏

 

Passport를 사용하면, 비밀번호를 단지 숫자 1만 입력해도 해시는 이렇게 어마무시하다. passport에게도 박수~ 🙌

7. 그렇다면 그 secrets 페이지가 보이도록 만들어주어야 한다.

단, 이 페이지는 로그인 상태여야만 입장 가능하도록 로그인 세션유지여부 체크하는 것을 잊지 말자.

아래 코드는 로그인 세션이 유지되는 경우에만 secrets 페이지로 입장이 언제든지 가능하며, 세션이 만료되면 login 페이지로 튕기도록 설정한 것이다.

그럼 이어서 login 페이지를 작성한다.

app.get("/secrets", function (req, res) {
  if (req.isAuthenticated()) {
    res.render("secrets");
  } else {
    res.redirect("/login");
  }
});

8. login 페이지를 담당하는 Route를 작성한다.

로그인에는 "User라는 DB 모델"을 기반으로 만들어지는 레코드 인스턴스 "user"를 req.login() 함수의 인자로 전달해야 한다.

app.route("/login")
  .get(function (req, res) {
    res.render("login")
  })

  .post(function (req, res) {
    const user = new User({
      username: req.body.username,
      password: req.body.password
    });

    req.login(user, function (err) {
      if (err) {
        // const user에 해당하는 정보로 로그인할 수 없을 경우 호출되는 부분
        console.log(err);
      } else {
        passport.authenticate("local") (req, res, function () {
          res.redirect("/secrets");
        })
      }
    })
  });

이제부터 로그인해야만 볼 수 있는 페이지는 위와 같이 passport.authenticate()를 넣어주면 되고, 로그인 상태가 아니라면 로그인 페이지로 튕기도록 redirect("/login") 시키면 된다.

 

9. Logout 기능을 부여한다.

app.route("/logout")
  .get(function (req, res) {
    req.logout();
    res.redirect("/");
  });

 

10. 완료!

728x90
반응형