JWT(JSON Web Token)로 로그인 REST API 만들기

소스코드

이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.

git clone https://github.com/a-mean-blogger/login-api.git
cd login-api
git reset --hard 459a532
npm install
atom .

- Github에서 소스코드 보기: https://github.com/a-mean-blogger/login-api/tree/459a532d67226667ca82cfce9cdc213c33ac5123


REST API에서 로그인은 일반적인 로그인과 그 방식이 다릅니다.

일반적인 로그인은 서버의 session을 통해 이루어지는데, 서버는 접속중인 클라이언트들을 이 session을 통해 관리합니다. 사이트에 접속하면 session에 해당 클라이언트가 기록되고 로그인을 하게 되면 해당 클라이언트가 로그인한 것을 저장하게 됩니다. 이후 해당 클라이언트는 로그인이 요구되는 정보에 접근할 수 있게 됩니다. 이 방식은 접속자수가 늘어나면 서버의 메모리 사용량이 증가하게 되고 성능에 영향을 미치게 됩니다. (서버를 증설하고 session관리용 서버를 분리하는 등의 작업을 통해 이를 극복할 수 있습니다.)

REST API에서는 서버가 session을 가지지 않습니다. 물론 REST API서버에도 session을 추가하여 사용할 수 있지만 이는 REST가 지향하는 바가 아닙니다. 대신 REST API는 토큰(token) 인증방식을 사용하게 됩니다.

로그인 API로 아이디와 패스워드가 일치함이 확인되면 서버는 토큰을 발행하고, 로그인 후 이용가능한 API들에는 유효한 토큰이 있는 경우에만 사용할 수 있게 됩니다. 이때 토큰은 당연하게도 위조하기가 어려워야 하며 사용자를 인식할 수 있는 정보가 들어있어야 합니다. 이 포스팅에서는 JWT(JSON Web Token, http://jwt.io) package를 사용해서 토큰을 생성하고 확인하겠습니다. 

로그인을 하려면 user data가 있어야 겠죠. 이번 포스팅에서는 1) Node JS API/기본 REST API 만들기와 Node JS 첫걸음/게시판 만들기의 user 부분을 섞어 user API를 만들고, 2) JWT를 이용하여 사용자 인증(authenticate)을 하는 auth API를 만들어 보겠습니다. 위 두 강좌의 코드를 모두 이해하고 있다는 가정하에 강의를 진행합니다.

API 스펙

크게 auth APIuser API로 나눌 수 있으며 각각 endpoint들은 다음과 같습니다.

  1. (POST) api/auth/login
  2. (GET) api/auth/me
  3. (GET) api/auth/refresh
  4. (GET) api/users
  5. (GET) api/users/{username}
  6. (POST) api/users
  7. (PUT) api/users/{username}
  8. (DELETE) api/users/{username}

자세한 스펙은 다음과 같습니다. 

1. Auth - Login

  • Endpoint: (POST) api/auth/login
  • Description: username과 password로 API에 로그인합니다. token을 return합니다.
  • Request Example:
    Url: (POST) api/auth/login
    Body:
    {
        "username": "test1",
        "password": "Password1"
    }
  • Response Example:
    {
        "success": true,
        "message": null,
        "errors": null,
        "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1OThkZGI2MzIyYWMxMDExZTA3MDJjYjAiLCJ1c2VybmFtZSI6InRlc3QxIiwibmFtZSI6InRlc3QxIiwiZW1haWwiOiIiLCJpYXQiOjE1MDQ3MzI2NzcsImV4cCI6MTUwNDgxOTA3N30.4eG2zGpSeY2XezKB4Djf6usy7DdygIybR1VKUBj-ScE"
    }

 

2. Auth - me

  • Endpoint: (GET) api/auth/me
  • Description: token을 받아 token의 주인인 user를 return합니다. header에 x-access-token이 요구됩니다.
  • Request Example:
    Url: (GET) api/auth/me
    Header:
    x-access-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1OThkZGI2MzIyYWMxMDExZTA3MDJjYjAiLCJ1c2VybmFtZSI6InRlc3QxIiwibmFtZSI6InRlc3QxIiwiZW1haWwiOiIiLCJpYXQiOjE1MDQ3MzI2NzcsImV4cCI6MTUwNDgxOTA3N30.4eG2zGpSeY2XezKB4Djf6usy7DdygIybR1VKUBj-ScE
    Body: N/A
  • Response Example:
    {
        "success": true,
        "message": null,
        "errors": null,
        "data": {
            "_id": "598ddb6322ac1011e0702cb0",
            "username": "test1",
            "name": "test1",
            "email": "",
            "iat": 1504732677,
            "exp": 1504819077
        }
    }

 

3. Auth - refresh

  • Endpoint: (GET) api/auth/refresh
  • Description: 기존의 token을 이용하여 새로운 token을 발급받습니다. header에 x-access-token이 요구됩니다.
  • Request Example:
    Url: (GET) api/auth/refresh
    Header:
    x-access-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1OThkZGI2MzIyYWMxMDExZTA3MDJjYjAiLCJ1c2VybmFtZSI6InRlc3QxIiwibmFtZSI6InRlc3QxIiwiZW1haWwiOiIiLCJpYXQiOjE1MDQ3MzI2NzcsImV4cCI6MTUwNDgxOTA3N30.4eG2zGpSeY2XezKB4Djf6usy7DdygIybR1VKUBj-ScE
    Body: N/A
  • Response Example:
    {
        "success": true,
        "message": null,
        "errors": null,
        "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1OThkZGI2MzIyYWMxMDExZTA3MDJjYjAiLCJ1c2VybmFtZSI6InRlc3QxIiwibmFtZSI6InRlc3QxIiwiZW1haWwiOiIiLCJpYXQiOjE1MDQ3MzQxNzUsImV4cCI6MTUwNDgyMDU3NX0.heYRtT1RZJYqgcJDaWwpKEmFUGwLn2r8OyYX-03_Nx4"
    }

 

4. User - Index

  • Endpoint: (GET) api/users
  • Description: user들의 목록을 리턴합니다. header에 x-access-token이 요구됩니다.
  • Request Example:
    Url: (GET) api/users
    Header:
    x-access-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1OThkZGI2MzIyYWMxMDExZTA3MDJjYjAiLCJ1c2VybmFtZSI6InRlc3QxIiwibmFtZSI6InRlc3QxIiwiZW1haWwiOiIiLCJpYXQiOjE1MDQ3MzI2NzcsImV4cCI6MTUwNDgxOTA3N30.4eG2zGpSeY2XezKB4Djf6usy7DdygIybR1VKUBj-ScE
    Body: N/A
  • Response Example:
    {
        "success": true,
        "message": null,
        "errors": null,
        "data": [
            {
                "_id": "598ddb6322ac1011e0702cb0",
                "username": "test1",
                "name": "test1",
                "__v": 0,
                "email": ""
            },
            {
                "_id": "59a748199e6e4e138033c687",
                "username": "test2",
                "name": "test2",
                "email": "",
                "__v": 0
            }
        ]
    }

 

5. User - Show

  • Endpoint: (GET) api/users/{username}
  • Description: username이 {username}인 user를 리턴합니다. header에 x-access-token이 요구됩니다.
  • Request Example:
    Url: (GET) api/users/test1
    Header:
    x-access-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1OThkZGI2MzIyYWMxMDExZTA3MDJjYjAiLCJ1c2VybmFtZSI6InRlc3QxIiwibmFtZSI6InRlc3QxIiwiZW1haWwiOiIiLCJpYXQiOjE1MDQ3MzI2NzcsImV4cCI6MTUwNDgxOTA3N30.4eG2zGpSeY2XezKB4Djf6usy7DdygIybR1VKUBj-ScE
    Body: N/A
  • Response Example:
    {
        "success": true,
        "message": null,
        "errors": null,
        "data": {
            "_id": "598ddb6322ac1011e0702cb0",
            "username": "test1",
            "name": "test1",
            "__v": 0,
            "email": ""
        }
    }

 

6. User - Create

  • Endpoint: (POST) api/users/
  • Description: body에 username, password, passwordConfirmation, name, email(optional)을 받아 새로운 user를 생성합니다.
  • Request Example:
    Url: (POST) api/users
    Body: 
    {
        "username": "test3",
        "password": "password1",
        "passwordConfirmation": "password1",
        "name": "test3",
        "email": ""
    }
  • Response Example:
    {
        "success": true,
        "message": null,
        "errors": null,
        "data": {
            "__v": 0,
            "username": "test3",
            "password": "$2a$10$.UAMVa/QcC.8ckk8sDEZXu1KFhNjINNDJZPx4o9tmaR1kmTmst3Be",
            "name": "test3",
            "email": "",
            "_id": "59b06d017479637420168a4c"
        }
    }

 

7. User - Update

  • Endpoint: (PUT) api/users/{username}
  • Description: body에 currentPassword, username, name, email(optional), newPassword(optional), passwordConfirmation(optional)를 받아 username이 {username}인 user를 수정합니다. header에 x-access-token이 요구됩니다.
  • Request Example:
    Url: (PUT) api/users/test3
    Header:
    x-access-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1OThkZGI2MzIyYWMxMDExZTA3MDJjYjAiLCJ1c2VybmFtZSI6InRlc3QxIiwibmFtZSI6InRlc3QxIiwiZW1haWwiOiIiLCJpYXQiOjE1MDQ3MzI2NzcsImV4cCI6MTUwNDgxOTA3N30.4eG2zGpSeY2XezKB4Djf6usy7DdygIybR1VKUBj-ScE
    Body:
    {
        "currentPassword": "Password1",
        "username": "test3",
        "newPassword": "Password1",
        "passwordConfirmation": "Password1",
        "name": "test5",
        "email": "[email protected]"
    }
  • Response Example:
    {
        "success": true,
        "message": null,
        "errors": null,
        "data": {
            "email": "[email protected]",
            "name": "test5",
            "username": "test5",
            "_id": "59b06d017479637420168a4c",
            "password": "$2a$10$oEWKzAadbQsxRCNWV4t2lutx8X2m9XtflMCHkD2jYbZEiiGJzVtsG"
        }
    }

 

8. Destroy

  • Endpoint: (DELETE) api/users/{username}
  • Description: username이 {username}인 user를 삭제합니다. header에 x-access-token이 요구됩니다.
  • Request Example:
    Url: (DELETE) api/users/test5
    Header:
    x-access-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1OThkZGI2MzIyYWMxMDExZTA3MDJjYjAiLCJ1c2VybmFtZSI6InRlc3QxIiwibmFtZSI6InRlc3QxIiwiZW1haWwiOiIiLCJpYXQiOjE1MDQ3MzI2NzcsImV4cCI6MTUwNDgxOTA3N30.4eG2zGpSeY2XezKB4Djf6usy7DdygIybR1VKUBj-ScE
    Body: N/A
  • Response Example:
    {
        "success": true,
        "message": null,
        "errors": null,
        "data": {
            "_id": "59b06d017479637420168a4c",
            "username": "test5",
            "name": "test5",
            "email": "[email protected]",
            "__v": 0
        }
    }

프로젝트 생성 및 Package 설치

프로젝트 생성에 앞서, 아래의 환경변수들을 설정합니다

  • MONGO_DB_LOGIN_API: 해당 프로젝트에서 사용할 DB를 생성하고 그 주소를 입력합니다.
  • JWT_SECRET: JWT hash의 생성과 해독에 필요한 키입니다. 아무런 문자열이나 사용하시면 됩니다.
    (예: MySuperSecretKey)

프로젝트 폴더를 생성합니다. 해당 폴더에서 command line(cmd, git bash 등)에 npm init을 입력하여 node.js 프로젝트를 생성합니다.

$ npm init

프로젝트에 필요한 package들도 설치해줍니다.

$ npm install express mongoose body-parser bcrypt-nodejs jsonwebtoken --save

Node JS API/기본 REST API 만들기와 비교해서 2개의 package가 추가되었습니다.

폴더 구조

폴더 구조

폴더 형태는 Node JS API/기본 REST API 만들기랑 비슷합니다. util.js는 Node JS 첫걸음/게시판 만들기에서와 마찬가지로 서버 전반에 사용되는 함수를 모아두는 곳입니다.

코드

// server.js

var express    = require('express');
var app        = express();
var path       = require('path');
var mongoose   = require('mongoose');
var bodyParser = require('body-parser');

// Database
mongoose.Promise = global.Promise;
mongoose.connect(process.env.MONGO_DB_LOGIN_API, {useMongoClient: true});
var db = mongoose.connection;
db.once('open', function () {
   console.log('DB connected!');
});
db.on('error', function (err) {
  console.log('DB ERROR:', err);
});

// Middlewares
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(function (req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'content-type, x-access-token'); //1
  next();
});

// API
app.use('/api/users', require('./api/users')); //2
app.use('/api/auth', require('./api/auth'));   //2

// Server
var port = 3000;
app.listen(port, function(){
  console.log('listening on port:' + port);
});

server.js는 Node JS API/기본 REST API 만들기와 거의 동일합니다. 다른 부분만 살펴봅시다.

1. CORS에  x-access-token이 추가되었습니다. jwt로 생성된 토큰은 header의 x-access-token 항목을 통해 전달됩니다.

2. Route으로 users와 auth가 있습니다.

// models/user.js

var mongoose = require('mongoose');
var bcrypt   = require('bcrypt-nodejs');

// schema
var userSchema = mongoose.Schema({
  username:{
    type:String,
    required:[true,'Username is required!'],
    match:[/^.{4,12}$/,'Should be 4-12 characters!'],
    trim:true,
    unique:true
  },
  password:{
    type:String,
    required:[true,'Password is required!'],
    select:false
  },
  name:{
    type:String,
    required:[true,'Name is required!'],
    match:[/^.{4,12}$/,'Should be 4-12 characters!'],
    trim:true,
    unique:true
  },
  email:{
    type:String,
    match:[/^[a-zA-Z0-9._%+-][email protected][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,'Should be a vaild email address!'],
    trim:true
  }
},{
  toObject:{virtuals:true}
});

// virtuals
userSchema.virtual('passwordConfirmation')
.get(function(){ return this._passwordConfirmation; })
.set(function(value){ this._passwordConfirmation=value; });

userSchema.virtual('originalPassword')
.get(function(){ return this._originalPassword; })
.set(function(value){ this._originalPassword=value; });

userSchema.virtual('currentPassword')
.get(function(){ return this._currentPassword; })
.set(function(value){ this._currentPassword=value; });

userSchema.virtual('newPassword')
.get(function(){ return this._newPassword; })
.set(function(value){ this._newPassword=value; });

// password validation
var passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,16}$/;
var passwordRegexErrorMessage = 'Should be minimum 8 characters of alphabet and number combination!';
userSchema.path('password').validate(function(v) {
  var user = this;

  // create user
  if(user.isNew){
    if(!user.passwordConfirmation){
      user.invalidate('passwordConfirmation', 'Password Confirmation is required!');
    }
    if(!passwordRegex.test(user.password)){
      user.invalidate('password', passwordRegexErrorMessage);
    } else if(user.password !== user.passwordConfirmation) {
      user.invalidate('passwordConfirmation', 'Password Confirmation does not matched!');
    }
  }

  // update user
  if(!user.isNew){
    if(!user.currentPassword){
      user.invalidate('currentPassword', 'Current Password is required!');
    }
    if(user.currentPassword && !bcrypt.compareSync(user.currentPassword, user.originalPassword)){
      user.invalidate('currentPassword', 'Current Password is invalid!');
    }
    if(user.newPassword && !passwordRegex.test(user.newPassword)){
      user.invalidate('newPassword', passwordRegexErrorMessage);
    } else if(user.newPassword !== user.passwordConfirmation) {
      user.invalidate('passwordConfirmation', 'Password Confirmation does not matched!');
    }
  }
});

// hash password
userSchema.pre('save', function (next){
  var user = this;
  if(!user.isModified('password')){
    return next();
  } else {
    user.password = bcrypt.hashSync(user.password);
    return next();
  }
});

// model methods
userSchema.methods.authenticate = function (password) {
  var user = this;
  return bcrypt.compareSync(password,user.password);
};

// model & export
var User = mongoose.model('user',userSchema);
module.exports = User;

user model은 Node JS 첫걸음/게시판 만들기에서 그대로 가져왔습니다. 이전에 열심히 만든 보람이 있네요. 설명은 생략합니다.

다음으로 util.js를 살펴봅시다.

//util.js

var jwt = require('jsonwebtoken');

var util = {};

util.successTrue = function(data){ //1 
  return {
    success:true,
    message:null,
    errors:null,
    data:data
  };
};

util.successFalse = function(err, message){ //2
  if(!err&&!message) message = 'data not found';
  return {
    success:false,
    message:message,
    errors:(err)? util.parseError(err): null,
    data:null
  };
};

util.parseError = function(errors){ //3
  var parsed = {};
  if(errors.name == 'ValidationError'){
    for(var name in errors.errors){
      var validationError = errors.errors[name];
      parsed[name] = { message:validationError.message };
    }
  } else if(errors.code == '11000' && errors.errmsg.indexOf('username') > 0) {
    parsed.username = { message:'This username already exists!' };
  } else {
    parsed.unhandled = errors;
  }
  return parsed;
};


// middlewares
util.isLoggedin = function(req,res,next){ //4
  var token = req.headers['x-access-token'];
  if (!token) return res.json(util.successFalse(null,'token is required!'));
  else {
    jwt.verify(token, process.env.JWT_SECRET, function(err, decoded) {
      if(err) return res.json(util.successFalse(err));
      else{
        req.decoded = decoded;
        next();
      }
    });
  }
};

module.exports = util;

1. success json 을 만드는 함수입니다. API가 return하는 json의 형태를 통일시키기 위해 바로 함수를 통해 json 오브젝트를 만들고 이를 return하게 됩니다.

2. API가 성공하지 못한 경우 return하는 json의 형태를 통일시키기 위해 error 오브젝트나 message를 받아서 error json을 만드는 함수 입니다.

3. mongoose를 통해 resource를 조작하는 과정에서 발생하는 에러를 일정한 형태로 만들어 주는 함수입니다. resource조작중에 에러가 mongoose에서 오거나(validation 등) DB에서 올 수 있는데(DB 에러 등) 이때 에러 형태가 다르기 때문에 통일해 주는 함수입니다. Node JS 첫걸음/게시판 만들기의 util.js에 있는 함수와 동일합니다.

4. 미들웨어로 token이 있는지 없는지 확인하고 token이 있다면 jwt.verify함수를 이용해서 토큰 hash를 확인하고 토큰에 들어있는 정보를 해독합니다. 해독한 정보는 req.decoded에 저장하고 있으며 이후 로그인 유무는 decoded가 있는지 없는지를 통해 알 수 있습니다.  util.js를 먼저 설명하다보니 토큰 생성부분보다 토큰 인증부분을 먼저 설명하게 되었네요.

// api/user.js

var express  = require('express');
var router   = express.Router();
var User     = require('../models/User');
var util     = require('../util');

// index
router.get('/', util.isLoggedin, function(req,res,next){
  User.find({})
  .sort({username:1})
  .exec(function(err,users){
    res.json(err||!users? util.successFalse(err): util.successTrue(users));
  });
});

// create
router.post('/', function(req,res,next){
  var newUser = new User(req.body);
  newUser.save(function(err,user){
    res.json(err||!user? util.successFalse(err): util.successTrue(user));
  });
});

// show
router.get('/:username', util.isLoggedin, function(req,res,next){
  User.findOne({username:req.params.username})
  .exec(function(err,user){
    res.json(err||!user? util.successFalse(err): util.successTrue(user));
  });
});

// update
router.put('/:username', util.isLoggedin, checkPermission, function(req,res,next){
  User.findOne({username:req.params.username})
  .select({password:1})
  .exec(function(err,user){
    if(err||!user) return res.json(util.successFalse(err));

    // update user object
    user.originalPassword = user.password;
    user.password = req.body.newPassword? req.body.newPassword: user.password;
    for(var p in req.body){
      user[p] = req.body[p];
    }

    // save updated user
    user.save(function(err,user){
      if(err||!user) return res.json(util.successFalse(err));
      else {
        user.password = undefined;
        res.json(util.successTrue(user));
      }
    });
  });
});

// destroy
router.delete('/:username', util.isLoggedin, checkPermission, function(req,res,next){
  User.findOneAndRemove({username:req.params.username})
  .exec(function(err,user){
    res.json(err||!user? util.successFalse(err): util.successTrue(user));
  });
});

module.exports = router;

// private functions
function checkPermission(req,res,next){ //*
  User.findOne({username:req.params.username}, function(err,user){
    if(err||!user) return res.json(util.successFalse(err));
    else if(!req.decoded || user._id != req.decoded._id) 
      return res.json(util.successFalse(null,'You don\'t have permission'));
    else next();
  });
}

user api도 Node JS 첫걸음/게시판 만들기의 user route에서 가져와 수정하였습니다. view를 찾아주는 route인 new와 edit을 제거하고 완벽한 CRUD를 만들기 위해 destroy를 추가한 형태입니다. 요청들을 처리하는 부분은 완전히 같고 html을 render하여 return하는 대신에 json을 리턴하게 되었습니다.

*checkPermission 부분의 로직은 token의 _id와 DB에서 찾은 user의 _id를 확인하는 것으로 바뀌었습니다.

마지막으로 오늘의 핵심인 auth api를 살펴봅시다.

// api/auth.js

var express  = require('express');
var router   = express.Router();
var User     = require('../models/User');
var util     = require('../util');
var jwt      = require('jsonwebtoken');

// login
router.post('/login',
  function(req,res,next){
    var isValid = true;
    var validationError = {
      name:'ValidationError',
      errors:{}
    };

    if(!req.body.username){
      isValid = false;
      validationError.errors.username = {message:'Username is required!'};
    }
    if(!req.body.password){
      isValid = false;
      validationError.errors.password = {message:'Password is required!'};
    }

    if(!isValid) return res.json(util.successFalse(validationError));
    else next();
  },
  function(req,res,next){
    User.findOne({username:req.body.username})
    .select({password:1,username:1,name:1,email:1})
    .exec(function(err,user){
      if(err) return res.json(util.successFalse(err));
      else if(!user||!user.authenticate(req.body.password))
         return res.json(util.successFalse(null,'Username or Password is invalid'));
      else {
        var payload = {
          _id : user._id,
          username: user.username
        };
        var secretOrPrivateKey = process.env.JWT_SECRET;
        var options = {expiresIn: 60*60*24};
        jwt.sign(payload, secretOrPrivateKey, options, function(err, token){
          if(err) return res.json(util.successFalse(err));
          res.json(util.successTrue(token));
        });
      }
    });
  }
);

// me
router.get('/me', util.isLoggedin,
  function(req,res,next) {
    User.findById(req.decoded._id)
    .exec(function(err,user){
      if(err||!user) return res.json(util.successFalse(err));
      res.json(util.successTrue(user));
    });
  }
);

// refresh
router.get('/refresh', util.isLoggedin,
  function(req,res,next) {
    User.findById(req.decoded._id)
    .exec(function(err,user){
      if(err||!user) return res.json(util.successFalse(err));
      else {
        var payload = {
          _id : user._id,
          username: user.username
        };
        var secretOrPrivateKey = process.env.JWT_SECRET;
        var options = {expiresIn: 60*60*24};
        jwt.sign(payload, secretOrPrivateKey, options, function(err, token){
          if(err) return res.json(util.successFalse(err));
          res.json(util.successTrue(token));
        });
      }
    });
  }
);

module.exports = router;

로그인 역시 Node JS 첫걸음/게시판 만들기과 유사하나, 아이디와 비밀번호가 일치함을 확인한 후에 jwt.sign함수를 통해 token을 생성하여 return하게 됩니다. jwt.sign은 payload, secretOrPrivateKey, options, Callback 함수의 4개의 파라메터를 전달받습니다.

  • payload: token에 저장될 정보입니다. 로그인용으로 사용되는 경우 DB에서 유저를 특정할 수 있는 간결한 정보를 담고 있어야 하며, 민감한 정보는 저장해선 안됩니다. 저는 user의 _id와 username을 담았습니다.
  • secretOrPrivateKey: hash 생성에 사용되는 key문자열입니다. 해독시 생성에 사용된 같은 문자열을 사용해야 해독할 수 있습니다.
  • options: hash 생성 알고리듬, token 유효기간등을 설정할 수 있는 options입니다. 저는 24시간이 지나면 토큰이 무효가 되도록하였습니다.
  • Callback 함수: token 생성후 실행되는 함수입니다. error와 token 문자열을 파라메터로 사용합니다.

me는 token을 해독해서 DB에서 user 정보를 return하는 API입니다.

refresh함수는 token의 유효기간이 끝나기전에 새로운 토큰을 발행하는 API입니다.

auth API 구조를 보면, RESTful하지 못한 것을 알 수 있습니다. 왜냐하면 token은 서버에 존재하는 resource가 아니기 때문입니다. 서버에서 생산은 되지만 서버에 저장되진 않죠. 그래서 token을 해독한다든지, token을 재생산한다든지 등의 행동은 아직 RESTful에서 우세한 의견이 없습니다.

실행 결과

nodemon을 사용해서 서버를 실행합시다

nodemon

Postman을 실행하고 우선 (GET)api/users API를 호출해 봅시다. user index는 로그인이 되어 있어야, 즉 토큰이 있어야만 실행할 수 있는 API입니다.

success가 false이고 token이 필요하다는 메세지를 볼 수 있습니다.  

(POST)api/users API로 user를 생성해 봅시다.

success가 true인것으로 봐서 user가 성공적으로 생성된 것을 알 수 있습니다.

토큰을 얻기 위해 (POST)/api/auth/login API를 호출해봅시다.

success가 true이고 data에 token문자열이 있습니다.

이제 이 token을 이용해서 다시 (GET)api/users를 요청해봅시다. token은 Headers 탭의 x-access-token에 넣어줍니다.


success가 true이고 data에 user list가 보입니다.

마치며..

Front end 사이트는 로그인후 생성된 token를 저장하고 있다가 API에 넣어서 사용하게 됩니다. Front end 쪽 예제는 다른 포스팅에서 살펴보겠습니다.

댓글

안암치킨 2017.08.16
안녕하세요 글 잘보았습니다. 지금 외국에 계신 듯 한데 혹시 온라인으로 강의 해보실 생각은 없으신가요?? 메일 주시면 간단하게 설명 드리고 기회가 되면 저도 듣고 싶어서요ㅋㅋ 관심 있으시면 [email protected]로 메일 부탁 드립니다. 좋은 하루 보내세요~!
I
Ian H 2017.08.17
@안암치킨,
안녕하세요, 제가 안바쁠때는 블로그를 하지만 바쁠때는 몇 주간 엄청바쁘고 그렇습니다. 그래서 그런 책임감 있는 일을하기는 힘들 것 같습니다.  시간이 된다면 강의를 유튜브에 올릴 생각은 있지만 그 생각도 작년부터 했는데 아직도 시간이 안되네요ㅠㅠ
안암치킨 2017.08.21
@Ian H,
넵 실력이 좋으신 거 같은데 아쉽네요ㅠ 답변 감사 드립니다!
오케버리 2018.01.07
안녕하세요ㅜㅜ 글을 보며 공부중입니다 jwt토큰을 사용한 로그인등을 구현중인데 해당글에서는 post맨 에 token은 Headers 탭의 x-access-token에 넣어줍니다. 이런식으로 생선된 토큰을 넘겨주는대 실제 사용할때는 토큰을 어떻게 넘겨받아야 하나요? 방식으로는 get/login 으로 아이디와 비번을 입력받아서 /post/login 으로 넘겨 토큰발급 까지는 성공 했습니다, 그후 인증이 필요한 곳에 router.get('/', util.isLoggedin, function(req,res){ ~ 이런식으로 로그인 검사를 하는대 도무지 토큰을 어떻게 받아와서 검증하는지를 모르겠습니다 ㅜ
I
Ian H 2018.01.09
@오케버리,
util.isLoggedin = function(req,res,next){   var token = req.headers['x-access-token'];   if (!token) return res.json(util.successFalse(null,'token is required!'));   else {     jwt.verify(token, process.env.JWT_SECRET, function(err, decoded) {       if(err) return res.json(util.successFalse(err));       else{         req.decoded = decoded;         next();       }     });   } };
을 통해서 토큰을 인증합니다. 코드를 보시면 header로 전달 받은 x-access-token을 jwt.verify함수로 인증을 하고 있습니다.
오케버리 2018.01.11
@Ian H,
 토큰생성후 res.json 으로 리턴 성공까지는 성공 했습니다... 근데 다시 홈으로 돌아와 util.isLoggedin 를 불러오면 토큰이 없다고 나옵니다 console.log(req.headers['x-access-token']) 을 해봐도 undefine 으로만 나옵니다 . 토큰을 따로 어디다 저장을 안해서 그런건가요? 제가 놓친고 있는 부분이 어딘지 모르겠습니다 ㅜ
I
Ian H 2018.01.11
@오케버리,
토큰은 client가 가지고 있다가 API를 호출할때마다 header('x-access-token)에 넣어서 보내야 합니다.
정 안되시면 소스코드를 github에 올려주시면 제가 한번 확인해 보겠습니다^^
오케버리 2018.01.12
@Ian H,
https://github.com/gameking90/helpme 이곳에 코드를 올렸습니다..
I
Ian H 2018.01.12
@오케버리,
코드를 보니까 REST API가 없이 back-end front-end 통합 사이트인데 JWT를 사용하시네요. 이경우에는 JWT를 cookie에 넣고 토큰 확인도 header에서 하지 마시고 cookie에서 하시면 될 것 같습니다. 한번 직접해보세요^^
근데 의도적으로 session이 없는 사이트를 만드시려는 건가요? 그냥 session를 사용한 로그인 강좌는 https://www.a-mean-blog.com/ko/blog/Node-JS-첫걸음/게시판-만들기 강좌를 참고해보세요~~
오케버리 2018.01.16
@Ian H,
흑 제가 REST API 에 대해서 공부를 더 해야겠내요 ㅜ 지금은 토큰을 세션스토리지에 저장해서 사용하고 있습니다 많은 도움이되서 감사합니다!!
I
Ian H 2018.01.16
@오케버리,
넵 화이팅~~~!!
댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP