이 글은 Local Authentication Using Passport in Node.js를 번역한 글입니다.

 

Local Authentication Using Passport in Node.js

Paul Orac shows how Passport, Node.js, Express, and MongoDB can be used to implement local authentication with a MongoDB back end.

www.sitepoint.com

 

 

1. 앞선 글.

 

 

웹앱을 만들 때 일반적인 요구사항은 사용자가 보호된 뷰 또는 리소스에 액세스 하기 전에 자신을 인증할 수 있도록 로그인 시스템을 구현하는 것입니다. 운 좋게도 NodeJS에서 앱을 구축하는 사람들에게는 Passport라는 미들웨어가 존재합니다. 이 미들웨어는 Express 기반의 웹 애플리케이션에 탑재되어 단지 몇 개의 명령만으로 인증 메커니즘을 제공해줍니다.

 

이 튜토리얼에서는 Passport를 사용해 MongoDB 백엔드로의 로컬 인증 즉, ID와 비밀번호로 로그인하는 것에 대해 구현하는 방법을 설명합니다. Facebook 또는 GitHub을 통해 인증을 구현하려는 경우 여기를 참조해 주세요.

 

이 글의 모든 코드는 여기서 다운로드할 수 있습니다.

 

 

 

2. 준비사항.

 

이 튜토리얼을 진행하기 위해선 NodeJS와 MongoDB가 설치되어 있어야 합니다. MongoDB는 Community Server를 설치해 주세요.

 

각각의 홈페이지에서는 훌륭한 문서를 제공하고 있으며 이 글에서 별도의 설치방법에 대해서 다루지 않겠습니다. 

 

 

 

3. 인증 전략: 세션과 JWT.

 

시작하기 전에 인증방법에 대해 간단히 이야기하겠습니다.

 

오늘날 온라인으로 제공되는 많은 튜토리얼은 JWT를 이용한 토큰 기반 인증을 선택합니다. 이 접근법은 아마 최근 가장 단순하고 가장 인기 있는 방법일 겁니다. 인증 책임의 일부를 클라이언트에 위임하고 모든 요청과 함께 토큰을 전송하며 서명된 토큰을 통해 사용자를 계속 인증하는 방법입니다. 

 

세션 기반 인증은 더 오래 지속된 방법입니다. 이 방법은 인증의 무게를 서버에 두는 방법입니다. 쿠키를 사용하여 노드 애플리케이션과 DB가 함께 동작하며 사용자의 인증 상태를 추적합니다.

 

이 튜토리얼에서는 Passport의 로컬 인증 전략의 핵심인 세션 기반 인증을 사용합니다.

 

두 방법 모두 장단점이 존재합니다. 두 방법의 차이점에 대해 더 자세히 알고 싶다면 여기를 참고해 주세요.

 

 

 

4. 프로젝트 생성.

 

사전 준비가 끝나면 이제 시작할 수 있습니다.

 

먼저 프로젝트 폴더를 생성 한 뒤 폴더로 이동합니다: 

 

mkdir AuthApp
cd AuthApp

 

Node앱을 만들기 위해 다음 명령어를 수행합니다: 

 

npm init

 

Node앱의 package.json에 제공할 정보를 묻는 메시지가 나타납니다. 기본 구성을 사용하려면 Return키를 누르시거나 -y플래그를 사용하세요.

 

 

 

5. Express 설정하기.

 

이제 Express를 설치해야 합니다. 터미널에서 다음 명령어를 수행해 주세요:

 

npm install express

 

또한 Passport가 사용자를 인증하는 데 사용하는 요청문을 파싱 하는 데 사용하기 위해 body-parser 미들웨어도 설치해야 합니다. 그리고 세션을 사용하기 위해 express-session 미들웨어도 설치해줘야 합니다.

 

설치해 봅시다. 다음 명령을 실행해 주세요.

 

npm install body-parser express-session

 

설치가 완료되면 프로젝트의 루트 폴더 경로에 index.js 파일을 생성 한 뒤 다음과 같이 코딩해 주세요.

 

/*  EXPRESS SETUP  */

const express = require('express');
const app = express();

app.use(express.static(__dirname));

const bodyParser = require('body-parser');
const expressSession = require('express-session')({
  secret: 'secret',
  resave: false,
  saveUninitialized: false
});

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(expressSession);

const port = process.env.PORT || 3000;
app.listen(port, () => console.log('App listening on port ' + port));

 

먼저 Express가 필요(require)하고 express()를 호출하여 Express 앱을 만듭니다. 그런 다음 정적 파일을 제공할 디렉터리를 정의합니다.

 

다음 줄에서 body-parser 미들웨어가 require 되었음을 볼 수 있습니다. 이는 요청 구문을 파싱 하는 데 사용됩니다. 또한 세션 쿠키를 저장할 수 있도록 express-session 미들웨어를 추가했습니다.

 

세션 ID 쿠키(고유한 값을 입력하는 것이 좋습니다.)에 서명하기 위해 'secret'로 express-session을 구성하고 다른 두 필드인 resave와 saveUninitialize도 구성해 줍니다. resave 필드는 세션이 세션 저장소에 다시 저장되는 것을 허용하는 필드이며 saveUninitialized 필드는 초기화되지 않은 세션이 세션 저장소에 저장되는 것을 허용하는 필드입니다. 이에 대한 자세한 내용은 express-session의 공식 문서를 참하세요. 그러나 지금은 우리가 이 두 필드를 false로 유지하면 된다는 것을 하는 것으로 충분합니다.

 

그 뒤 process.env.PORT를 이용해 환경변수에 PORT가 있으면 그 값으로 port를 설정합니다. 그렇지 않으면 기본값으로 3000 포트를 로컬에서 사용하도록 합니다. 이는 개발 환경에서 Heroku와 같이 포트를 직업 설정하는 서비스 제공업체로의 전환에 충분한 유연성을 제공합니다. 그 바로 아래에 우리는 app.listen()을 설정한 포트와 간단한 로그와 함께 호출합니다. 모두 제대로 동작한다면 앱이 어떤 포트에서 수신하고 있는지 알려줍니다.

 

이것으로 Express 설정이 끝났습니다. 이제 Passport를 설정할 차례입니다.

 

 

 

6. Passport 설정하기.

 

먼저 다음 명령으로 Passport를 설치합니다:

 

npm install passport

 

그런 뒤 index.js에 다음 코드를 추가합니다: 

 

/*  PASSPORT SETUP  */

const passport = require('passport');

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

 

여기서 우리는 우리의 express내에서 passport를 require 했고 세션 인증 미들웨어와 함께 초기화했습니다.

 

 

 

7. MongoDB 데이터 저장소 생성.

 

이미 Mongo를 설치했다고 가정하고 다음 명령을 사용하여 Mongo 셸을 시작합니다. 

 

mongo

 

셸 내에서 다음 명령어를 수행해 주세요:

 

use MyDatabase;

 

이 명령어는 간단히 MyDatabase라는 데이터 저장소를 생성합니다.

터미널은 그대로 두세요. 나중에 다시 사용할 거예요.

 

 

 

8. Mongoose를 이용해 MongoDB와 Node앱을 연결하기.

 

이제 레코드가 있는 데이터베이스가 있으므로 애플리케이션에서 데이터베이스와 통신할 수 있는 방법이 필요합니다. 이를 위해 Mongoose를 사용할 것입니다. Mongoose는 간단하게 더 편하고 더 우아한 코드를 만들게 해 줍니다.

 

다음 명령어를 통해 설치를 진행해 줍니다:

 

npm install mongoose

 

또한 로컬 인증을 위해 Mongoose와 Passport의 통합을 단순하게 해 주기 위해 passport-localmongoose를 사용할 것입니다. 이는 해시된 암호와 salt 값을 저장할 수 있도록 스키마에 해시와 솔트 필드를 추가해 줍니다. 암호는 DB에 일반 텍스트로 저장되면 안 되므로 이를 이용하는 것이 좋습니다.

 

다음 명령어를 통해 패키지를 설치해 주세요:

 

npm install passport-local-mongoose

 

이제 Mongoose를 설정해야 합니다. 다행히도 우린 이제 드릴을 알고 있습니다: 다음 코드를 index.js 파일에 추가해 주세요.

 

/* MONGOOSE SETUP */

const mongoose = require('mongoose');
const passportLocalMongoose = require('passport-local-mongoose');

mongoose.connect('mongodb://localhost/MyDatabase',
  { useNewUrlParser: true, useUnifiedTopology: true });

const Schema = mongoose.Schema;
const UserDetail = new Schema({
  username: String,
  password: String
});

UserDetail.plugin(passportLocalMongoose);
const UserDetails = mongoose.model('userInfo', UserDetail, 'userInfo');

 

이전에 설치한 패키지들을 require 해 줍니다. 그 뒤 mongoose.connect를 이용해 DB에 연결하고 DB경로를 제공합니다. 다음으로 스키마를 이용해 데이터 구조를 정의합니다. 이 경우 username과 password필드를 사용하여 UserDetail스키마를 만들었습니다.

 

마지막으로 passportLocalMongoose를 plugin으로 스키마에 추가합니다. 이것은 우리가 앞서 이야기한 마술의 일무가 될 겁니다. 그런 뒤 스키마에서 모델을 만들어줍니다. 첫 번째 매개변수는 DB의 collection이름입니다. 두 번째 매개변수는 스키마에 대한 참조이고 세 번째 매개변수는 Mongoose 내부의 collection에 해당하는 이름입니다.

 

이게 Mongoose설정의 전부입니다. 이제 Passport의 전략을 구현할 수 있습니다.

 

 

 

9. 로컬 인증 구현하기.

 

그리고 마침내 우리가 여기 도달했습니다! 이제 로컬 인증을 설정해 보도록 하겠습니다. 아래에 보이는 것처럼 코드를 작성해 주세요:

 

/* PASSPORT LOCAL AUTHENTICATION */

passport.use(UserDetails.createStrategy());

passport.serializeUser(UserDetails.serializeUser());
passport.deserializeUser(UserDetails.deserializeUser());

 

여기에는 꽤 대단한 마술이 있습니다. 먼저 passport-local-mongoose에서 제공해주는 UserDeatilas모델에서 createStarategy()를 호출하여 Passport가 로컬 전략을 사용하도록 함으로써 우리는 전략에 대해 더 이상 설정을 해 줄 필요가 없도록 합니다. 꽤나 편리한 기능입니다.

 

그런 다음 serializeUser 및 deserializeUser 콜백을 사용합니다. 첫 번째는 인증 시 호출되며 사용자 인스턴스에 전달한 정보로 사용자 인스턴스를 serialize 하고 쿠키를 통해 세선에 저장하는 역할을 합니다. 두 번째는 모든 후속 요청을 호출해 인스턴스를 deserialize 하고 고유한 쿠키 식별자를 "credential"로 제공합니다. 자세한 내용은 Passport의 문서를 참고해 주세요.

 

 

 

10. 라우트.

 

이제 라우트를 추가해 모든 것을 하나로 묶어보도록 하겠습니다. 먼저 마지막으로 패키지를 추가하도록 합니다. 터미널로 이동해 다음 명령을 실행해 주세요:

 

npm install connect-ensure-login

 

connect-ensure-login 패키지는 사용자가 로그인을 할 수 있도록 하는 미들웨어입니다. 인증되지 않은 요청이 수신되면 로그인 페이지로 리디렉션 해줍니다. 이를 이용하여 우리의 라우트를 보호합니다. 이제 index.js에 다음 코드를 추가해 주세요:

 

/* ROUTES */

const connectEnsureLogin = require('connect-ensure-login');

app.post('/login', (req, res, next) => {
  passport.authenticate('local',
  (err, user, info) => {
    if (err) {
      return next(err);
    }

    if (!user) {
      return res.redirect('/login?info=' + info);
    }

    req.logIn(user, function(err) {
      if (err) {
        return next(err);
      }

      return res.redirect('/');
    });

  })(req, res, next);
});

app.get('/login',
  (req, res) => res.sendFile('html/login.html',
  { root: __dirname })
);

app.get('/',
  connectEnsureLogin.ensureLoggedIn(),
  (req, res) => res.sendFile('html/index.html', {root: __dirname})
);

app.get('/private',
  connectEnsureLogin.ensureLoggedIn(),
  (req, res) => res.sendFile('html/private.html', {root: __dirname})
);

app.get('/user',
  connectEnsureLogin.ensureLoggedIn(),
  (req, res) => res.send({user: req.user})
);

 

가장 위에 connect-ensure-login을 require 했습니다. 다음에 다시 확인하도록 하겠습니다. 

 

다음으로 /login 경로에 대한 POST 요청을 처리하도록 하는 라우트를 설정했습니다. 핸들러 내에서 passport.authenticate메서드를 사용합니다. 이 메서드는 첫 번째 매개변수로 받는 전략으로 인증을 시도하며 우리는 로컬 인증 전략을 사용했습니다. 인증에 실패하면 /login으로 리디렉션 되며 오류 메시지가 포함된 쿼리 매개변수인 info가 추가됩니다. 인증에 성공하면 '/' 경로로 리디렉션 됩니다.

 

그런 다음 /login 라우트를 설정해 로그인 페이지를 전송하도록 합니다. 이를 위해 res.sendFile()을 사용하였으며 매개변수로 파일 경로와 루트 디렉터리를 전달합니다. 이때의 디렉터리는 __dirname입니다.

 

/login은 누구나 액세스 할 수 있지만 login 다음의 경로는 아무나 액세스 할 수 없습니다. / 및 /private 경로에선 각각 HTML 페이지를 보내며 이 페이지들에서 다른 작업을 안내할 수 있습니다. 콜백 전에 connectEnsureLogin.ensureLoggedIn()을 호출하고 있습니다. 이는 우리의 경비원과 같은 역할을 합니다. 해당 경로를 볼 수 있는지 세션을 확인하는 역할을 수행합니다. "서버가 무거운 작업을 수행하게 한다"는 말의 의미를 알고 있나요? 우리는 지금 매번 사용자 인증을 수행하도록 하고 있습니다.

 

마지막으로 /user 라우트가 필요합니다. 이 라우트는 사용자 정보가 포함된 객체를 반환합니다. 이것은 단지 서버로부터 정보를 얻는 방법을 보여주기 위한 것입니다. 사용자들은 이 라우트에 요청을 하게 되고 결과를 표시하도록 합니다.

 

클라이언트에 대한 이야기를 지금 해보도록 합시다.

 

 

 

11. 클라이언트.

 

클라이언트는 단순해야 합니다. 간단한 HTML 페이지와 CSS 파일을 만들어 보도록 합시다. 홈페이지나 인덱스부터 시작하도록 하겠습니다. 프로젝트 루트에 html이라는 폴더를 만들고 index.html의 파일을 추가해 주세요. 그리고 다음 코드를 작성해 주세요:

 

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title> Home </title>
  <link rel="stylesheet" href="css/styles.css">
</head>

<body>
  <div class="message-box">
    <h1 id="welcome-message"></h1>
    <a href="/private">Go to private area</a>
  </div>

  <script>
    const req = new XMLHttpRequest();
    req.onreadystatechange = function () {
      if (req.readyState == 4 && req.status == 200) {
        const user = JSON.parse(req.response).user;
        document.getElementById("welcome-message").innerText = `Welcome ${user.username}!!`;
      }
    };
    req.open("GET", "http://localhost:3000/user", true);
    req.send();
  </script>
</body>
</html>

 

비어있는 h1태그가 있으며 여기에 환영 메시지를 표시합니다. 그 아래에 /private에 대한 링크를 넣어주고 있습니다. 여기서 중요한 부분은 사용자 이름으로 환영 메시지를 작성하도록 처리하는 태그 아래의 스크립트 부분입니다.

 

이 스크립트는 네 부분으로 나뉘어 있습니다.

  1. 새로운 XMLHttpRequrest()를 사용해 요청 객체를 인스턴스화 합니다.
  2. 결과를 얻은 후 호출할 함수로 onreadystatechange 속성을 설정합니다. 콜백에서 성공적인 결과를 받았는지 확인하고 성공했다면 파싱 후 user 객체를 가져와 welcome-message 엘리먼트를 찾아와 innerText를 user.username을 이용해 설정합니다. 
  3. open()으로 GET 요청을 생성합니다. 첫 번째 매개변수는 요청의 타입이고 두 번째 매개변수는 user URL이며 마지막 매개변수를 true로 설정해 비동기 요청을 활성화합니다.
  4. 마지막으로 send()를 이용해 요청을 보냅니다.

이제 로그인 페이지를 작성하도록 합니다. 이전과 같이 HTML폴더에 login.html이라는 파일을 생성하고 다음과 같이 코딩해 주세요:

 

<!DOCTYPE html>
<html lang="en">
<head>
  <title> Login </title>
  <link rel="stylesheet" href="css/styles.css">
</head>

<body>
  <form action="/login" method="post">
    <div class="title">
      <h3>Login</h3>
    </div>
    <div class="field">
      <label>Username:</label>
      <input type="text" name="username" />
      <br />
    </div>
    <div class="field">
      <label>Password:</label>
      <input type="password" name="password" required />
    </div>
    <div class="field">
      <input class="submit-btn" type="submit" value="Submit" required />
    </div>
    <label id="error-message"></label>
  </form>

  <script>
    const urlParams = new URLSearchParams(window.location.search);
    const info = urlParams.get('info');

    if(info) {
      const errorMessage = document.getElementById("error-message");
      errorMessage.innerText = info;
      errorMessage.style.display = "block";
    }
  </script>
</body>
</html>

 

사용자 이름, 비밀번호, 제출 버튼이 있는 간단한 로그인 폼 페이지입니다. 아래에는 오류 메시지를 표시할 레이블이 위치합니다. 이것들은 쿼리 문자열에 포함되어 있음을 잊지 마세요.

 

하단에 있는 이번 스크립트는 훨씬 간단합니다. 우리의 URL에 매개변수로 포함되어 있는 window.location.search 속성을 전달하고 있는 URLSearchParams객체를 인스턴스화 하고 있습니다. 그런 뒤 URLSearchParams.get() 메서드를 사용해 우리가 찾고 있는 매개변수 이름을 전달합니다.

 

이때, 우린 info메시지가 있을 수도 없을 수도 있습니다. 따라서 error-message요소를 가져와 뭐든 간에 innerText로 설정한 뒤 style.display속성을 block로 설정합니다. 기본값으로 disaply: none으로 표시되는 것을 볼 수 있습니다. 

 

이제 private 페이지를 만들어 보겠습니다. HTML 폴더 안에 private.html 파일을 생성한 뒤 다음과 같이 코딩해주세요:

 

<!DOCTYPE html>
<html lang="en">
<head>
  <title> Private </title>
  <link rel="stylesheet" href="css/styles.css">
</head>

<body>
  <div class="message-box">
    <h2>This is a private area</h2>
    <h3>Only you can see it</h3>
    <a href="/">Go back</a>
  </div>
</body>
</html>

 

매우 간단합니다. 단순한 메시지를 표시해주고 돌아가기 링크를 누르면 홈페이지로 돌아갑니다.

 

아마 알고 있겠지만 HTML에서 CSS를 참조하고 있습니다. 이제 그 CSS파일을 추가해 보도록 하겠습니다. 프로젝트 루트에 css라는 폴더를 생성 한 뒤 style.css 파일을 생성해주세요. 그리고 다음과 같이 코딩해주세요:

 

body {
  display: flex;
  align-items: center;
  background: #37474F;
  font-family: monospace;
  color: #cfd8dc;
  justify-content: center;
  font-size: 20px;
}

.message-box {
  text-align: center;
}

a {
  color: azure;
}

.field {
  margin: 10px;
}

input {
  font-family: monospace;
  font-size: 20px;
  border: none;
  background: #1c232636;
  color: #CFD8DC;
  padding: 7px;
  border: #4c5a61 solid 2px;
  width: 300px;
}

.submit-btn {
  width: 100%
}

.title {
  margin: 10px 0px 20px 10px
}

#error-message {
  color: #E91E63;
  display: block;
  margin: 10px;
  font-size: large;
  max-width: fit-content;
}

 

이러면 이제 페이지가 보기 좋게 변할 겁니다. 확인해 봅시다.

 

프로젝트 루트 경로에서 다음 명령어를 실행해 주세요:

 

node index.js

 

이제 브라우저를 열고 http://localhost:3000/으로 이동해 보세요 로그인 페이지로 리디렉션 될 겁니다. 그 뒤 http://localhost:300/private로 이동하면 다시 로그인 페이지로 리디렉션 됩니다. 우리의 라우터 가드가 일을 하고 있다는 겁니다.

 

이제 서버를 중지하고 index.js 파일로 가서 다음 코드를 추가해 주세요:

 

/* REGISTER SOME USERS */

UserDetails.register({username:'paul', active: false}, 'paul');
UserDetails.register({username:'jay', active: false}, 'jay');
UserDetails.register({username:'roy', active: false}, 'roy');

 

passport-local-mongoose의 register 메서드를 이용해 암호를 지정해 줍니다. 우린 단지 일반적인 텍스트로 전달해 주기만 하면 됩니다.

 

다시 index.js를 실행시키면 유저가 생성됩니다. 

 

아까 열어둔 mongo 터미널을 다시 사용해 봅시다. 다음 쿼리를 전송해 보세요:

 

db.userInfo.find()

 

세명의 사용자를 볼 수 있으며 해시값과 salt값이 화면의 대부분을 차지할 겁니다.

 

이것들이 앱이 동작하는데 필요한 전부입니다. 끝났습니다!

 

브라우저로 돌아가서 우리가 등록한 유저로 로그인을 시도하면 사용자 이름이 포함된 로그인 메시지가 보일 겁니다.

 

 

 

12. 다음 단계.

 

지금까지 우린 단지 딱 이 앱이 동작할 정도만의 모듈만 추가했습니다. 배포 환경의 앱을 위해선 다른 미들웨어를 추가하고 코드를 분리해야 합니다. 현재 상태에서는 깔끔하고 확장 가능한 환경으로 발전해 나가는데 어려울 수 있습니다.

 

다음 해야 할 가장 쉬운 도전은 Passport의 req.logout() 메서드를 사용해 로그아웃을 추가해 보는 것입니다.

 

그러면 회원 등록을 구현해 볼 수 있습니다. 회원 등록 폼과 라우트가 필요하게 될 겁니다. 앞서 구현한 UserDetails.register()를 탬플릿으로 사용할 수 있습니다. 이메일 인증을 위해서는 nodemailer를 체크아웃받아야 할 수 도 있습니다.

 

시도해 볼 수 있는 다른 일은 SPA(Single Page Application, 단일 페이지 애플리케이션)에 적용시켜보는 것입니다. 아마 Vue.js와 Vue의 라우터를 사용할 수도 있을 겁니다. 그렇게 주말을 보내보세요!

 

 

 

13. 맺음말.

 

마침내 끝을 맺었습니다. 이 글에서는 Node.js 애플리케이션에서 Passport를 이용해 로컬 인증을 구현하는 방법에 대해 알아봤습니다. 이 과정에서 Mongoose를 이용해 MongoDB와 연결하는 방법도 설명했습니다. 

 

이 과정이 페인트칠하는 것만큼 쉬운 일은 아니었지만 적어도 우린 백그라운드에서 마술과 같은 툴을 이용하면 우리가 원하는 것에만 신경 쓸 수 있도록 만들어 주어 일이 더 쉬워진다는 것을 알게 되었습니다.

 

마술과 같은 도구는 항상 이상적이진 않습니다만 평판이 좋고 활발히 유지 보수 되는 도구를 사용하면 코드를 적게 만드는데 도움이 됩니다. 작성하지 않은 코드는 유지 보수할 필요가 없으며 유지 보수할 필요가 없다는 것은 당신이 실수할 일이 없다는 걸 의미합니다.

 

또한 핵심적인 팀이 적극적으로 유지 보수하는 경우 그 팀은 우리보다 그 일에 대해 더 잘할 겁니다. 적극적으로 사용하세요.

 

이 튜토리얼을 즐기고 다음 프로젝트에 대한 영감을 얻었길 바랍니다. 즐거운 코딩 하세요!

 

 

 

 

 

반응형

+ Recent posts