23/12/2018, 23:15

NodeJS: xác thực tài khoản sử dụng Passport.js

Trong bài viết này ta sẽ cùng tìm hiểu các mục sau: Handling protected routes Handling JWT tokens Handling unauthorised responses Creating a basic API Creating models & schemas 1.Giới thiệu Passport.js là gì? Passport một module phổ biến nhất của Nodejs cho phép bạn ...

  • Trong bài viết này ta sẽ cùng tìm hiểu các mục sau:
    • Handling protected routes
    • Handling JWT tokens
    • Handling unauthorised responses
    • Creating a basic API
    • Creating models & schemas

1.Giới thiệu

Passport.js là gì?

  • Passport một module phổ biến nhất của Nodejs cho phép bạn authentication . Vì nó cực kỳ linh hoạt và tùy biến, nó có thể hỗ trợ xác thực bằng cách sử dụng user và password hoặc thông qua Facebook, Twitter, và nhiều hơn nữa . Tìm hiểu thêm về Passport ở đây .

2. Tạo server nodejs

  • Tạo một thư mục mới với file app.js với nội dung bên trong:

    const express = require('express');
    const path = require('path');
    const bodyParser = require('body-parser');
    const session = require('express-session');
    const cors = require('cors');
    const mongoose = require('mongoose');
    const errorHandler = require('errorhandler');
    
    //Configure mongoose's promise to global promise
    mongoose.promise = global.Promise;
    
    //Configure isProduction variable
    const isProduction = process.env.NODE_ENV === 'production';
    
    //Initiate our app
    const app = express();
    
    //Configure our app
    app.use(cors());
    app.use(require('morgan')('dev'));
    app.use(bodyParser.urlencoded({ extended: false }));
    app.use(bodyParser.json());
    app.use(express.static(path.join(__dirname, 'public')));
    app.use(session({ secret: 'passport-tutorial', cookie: { maxAge: 60000 }, resave: false, saveUninitialized: false }));
    
    if(!isProduction) {
      app.use(errorHandler());
    }
    
    //Configure Mongoose
    mongoose.connect('mongodb://localhost/passport-tutorial');
    mongoose.set('debug', true);
    
    //Error handlers & middlewares
    if(!isProduction) {
      app.use((err, req, res) => {
        res.status(err.status || 500);
    
        res.json({
          errors: {
            message: err.message,
            error: err,
          },
        });
      });
    }
    
    app.use((err, req, res) => {
      res.status(err.status || 500);
    
      res.json({
        errors: {
          message: err.message,
          error: {},
        },
      });
    });
    
    app.listen(8000, () => console.log('Server running on http://localhost:8000/'));
    
  • Chúng ta sẽ cài đặt thêm nodemon:

    npm install -g nodemon
    
  • Và sau đó chúng ta sẽ chạy file app.js với nó.

    nodemon app.js
    
  • Kết quả ta sẽ thu được sau khi chạy lệnh trên

    $nodemon app.js
    [nodemon] 1.17.5
    [nodemon] to restart at any time, enter `rs`
    [nodemon] watching: *.*
    [nodemon] starting `node app.js`
    Server running on http://localhost:8000/
    
Tạo model user
  • Ta tạo một thư mục mới có tên là models, và tạo file Users.js thư mục đó. Đây là nơi chúng ta định nghĩa UsersSchema. Chúng ta sẽ sử dụng JWT và Crypto tạo hash và salt từ chuỗi password . Điều này sau đó sẽ được sử dụng để xác nhận người dùng.

    const mongoose = require('mongoose');
    const crypto = require('crypto');
    const jwt = require('jsonwebtoken');
    
    const { Schema } = mongoose;
    
    const UsersSchema = new Schema({
      email: String,
      hash: String,
      salt: String,
    });
    
    UsersSchema.methods.setPassword = function(password) {
      this.salt = crypto.randomBytes(16).toString('hex');
      this.hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
    };
    
    UsersSchema.methods.validatePassword = function(password) {
      const hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
      return this.hash === hash;
    };
    
    UsersSchema.methods.generateJWT = function() {
      const today = new Date();
      const expirationDate = new Date(today);
      expirationDate.setDate(today.getDate() + 60);
    
      return jwt.sign({
        email: this.email,
        id: this._id,
        exp: parseInt(expirationDate.getTime() / 1000, 10),
      }, 'secret');
    }
    
    UsersSchema.methods.toAuthJSON = function() {
      return {
        _id: this._id,
        email: this.email,
        token: this.generateJWT(),
      };
    };
    
    mongoose.model('Users', UsersSchema);
    
  • Chúng ta hãy thêm models mới được tạo vào ứng dụng bằng cách thêm dòng sau vào file app.js của bạn sau khi định cấu hình Mongoose:

    require('./models/Users');
    
    //app.js
    ...
     //Configure Mongoose
    mongoose.connect('mongodb://localhost/passport-tutorial');
    mongoose.set('debug', true);
    
    //Models & routes
    require('./models/Users');
    
    //Error handlers & middlewares
    if(!isProduction) {
    ...
    
Cấu hình Passport
  • Tạo một thư mục mới config với file Passport.js bên trong nó:

    const mongoose = require('mongoose');
    const passport = require('passport');
    const LocalStrategy = require('passport-local');
    
    const Users = mongoose.model('Users');
    
    passport.use(new LocalStrategy({
      usernameField: 'user[email]',
      passwordField: 'user[password]',
    }, (email, password, done) => {
      Users.findOne({ email })
        .then((user) => {
          if(!user || !user.validatePassword(password)) {
            return done(null, false, { errors: { 'email or password': 'is invalid' } });
          }
    
          return done(null, user);
        }).catch(done);
    }));
    
  • Trong file này, chúng ta sử dụng phương thức validatePassword mà chúng tôi đã xác định trong model User.

  • Đến đây ta sẽ có cấu trúc thư mục như sau:

      --config
          --passport.js
       --models
           --Users.js
       --node_modules
       --app.js
       --package-lock.json
    
  • Ta sẽ thêm Passport.js vào app.js bên dưới model user

    //app.js
    ...
     //Configure Mongoose
    mongoose.connect('mongodb://localhost/passport-tutorial');
    mongoose.set('debug', true);
    
    //Models & routes
    require('./models/Users');
    require('./config/passport');
    
    //Error handlers & middlewares
    if(!isProduction) {
    ...
    
Routes and authentication options
  • Tạo một thư mục mới có tên là routes, với file Auth.js bên trong nó.

  • Trong tệp này, chúng ta sử dụng phương thức getTokenFromHeaders để get JWT token được gửi từ phía client trong các request header. Chúng ta cũng tạo ra một object auth với các thuộc tính optional và required. Chúng ta sẽ sử dụng chúng sau này trong các route.

    const jwt = require('express-jwt');
    
    const getTokenFromHeaders = (req) => {
      const { headers: { authorization } } = req;
    
      if(authorization && authorization.split(' ')[0] === 'Token') {
        return authorization.split(' ')[1];
      }
      return null;
    };
    
    const auth = {
      required: jwt({
        secret: 'secret',
        userProperty: 'payload',
        getToken: getTokenFromHeaders,
      }),
      optional: jwt({
        secret: 'secret',
        userProperty: 'payload',
        getToken: getTokenFromHeaders,
        credentialsRequired: false,
      }),
    };
    
    module.exports = auth;
    
  • Trong cùng thư mục routes, hãy tạo một file index.js

  • Bây giờ chúng ta cần một thư mục api của người dùng bên trong thư mục routes, cũng chứa một tệp index.js khác bên trong nó.

    const express = require('express');
    const router = express.Router();
    
    router.use('/users', require('./users'));
    
    module.exports = router;
    
  • Đến đây cấu trúc thư mục sẽ như này:

      --config
          --passport.js
       --models
           --Users.js
       --node_modules
       --routes
           --api
               --index.js
            --auth.js
            --index.js
        --app.js
        --package-lock.json
    
  • Bây giờ, chúng ta hãy tạo ra tập tin users.js mà chúng đã require trong file api/index.js.

    const mongoose = require('mongoose');
    const passport = require('passport');
    const router = require('express').Router();
    const auth = require('../auth');
    const Users = mongoose.model('Users');
    
    //POST new user route (optional, everyone has access)
    router.post('/', auth.optional, (req, res, next) => {
      const { body: { user } } = req;
    
      if(!user.email) {
        return res.status(422).json({
          errors: {
            email: 'is required',
          },
        });
      }
    
      if(!user.password) {
        return res.status(422).json({
          errors: {
            password: 'is required',
          },
        });
      }
    
      const finalUser = new Users(user);
    
      finalUser.setPassword(user.password);
    
      return finalUser.save()
        .then(() => res.json({ user: finalUser.toAuthJSON() }));
    });
    
    //POST login route (optional, everyone has access)
    router.post('/login', auth.optional, (req, res, next) => {
      const { body: { user } } = req;
    
      if(!user.email) {
        return res.status(422).json({
          errors: {
            email: 'is required',
          },
        });
      }
    
      if(!user.password) {
        return res.status(422).json({
          errors: {
            password: 'is required',
          },
        });
      }
    
      return passport.authenticate('local', { session: false }, (err, passportUser, info) => {
        if(err) {
          return next(err);
        }
    
        if(passportUser) {
          const user = passportUser;
          user.token = passportUser.generateJWT();
    
          return res.json({ user: user.toAuthJSON() });
        }
    
        return status(400).info;
      })(req, res, next);
    });
    
    //GET current route (required, only authenticated users have access)
    router.get('/current', auth.required, (req, res, next) => {
      const { payload: { id } } = req;
    
      return Users.findById(id)
        .then((user) => {
          if(!user) {
            return res.sendStatus(400);
          }
    
          return res.json({ user: user.toAuthJSON() });
        });
    });
    
    module.exports = router;
    
  • Ta có cấu trúc thư mục:

    --config
        --passport.js
     --models
         --Users.js
     --node_modules
     --routes
         --api
             --index.js
             --users.js
          --auth.js
          --index.js
      --app.js
      --package-lock.json
    
  • Chúng ta cần require thêm thư mục routes bên trong app.js:

    //Models & routes
    require('./models/Users');
    require('./config/passport');
    app.use(require('./routes'));
    

3. Route testing

  • Ta sẽ sử dụng Postman để gửi request đến server của chúng ta.

  • Tạo một request POST để tạo người dùng với param truyền vào sẽ như sau:

    { 
      "user": { 
        "email": "[email protected]", 
        "password": "test" 
      } 
    }
    
  • Và ta sẽ thu được response trả về như sau:

    { 
        "User": { 
            "_id": "5b0f38772c46910f16a058c5", 
            "email": "[email protected]", 
            "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImVyZGVsamFjLmFudG9uaW9AZ21haWwuY29tIiwiaWQiOiI1YjBmMzg3NzJjNDY5MTBmMTZhMDU4YzUiLCJleHAiOjE1MzI5MDgxNTEsImlhdCI6MTUyNzcyNDE1MX0.4TWc1TzY6zToHx_O1Dl2I9Hf9krFTqPkNLHI5U9rn8c" 
        } 
    }
    
  • Bây giờ chúng ta sẽ sử dụng mã token này và thêm nó vào header của Postman để test authentication.

  • Tạo một yêu cầu GET để trả về người dùng hiện đang đăng nhập:

  • URL yêu cầu:

    GET http://localhost:8000/api/users/current
    
  • Và ta sẽ thu được respone trả về:

    { 
        "User": { 
            "_id": "5b0f38772c46910f16a058c5", 
            "email": "[email protected]", 
        } 
    }
    
  • Giờ ta hãy thử gọi lại request trên mà không có mã tokent trong Headersers.

  • Và ta sẽ thu được: Status 401 Unauthorized

0