Cách sử dụng Passport-JWT trong NodeJS


1042

JWT là gì ?

Tôi sẽ không đi quá sâu về JWT, nhưng đây là tất cả những điều cơ bản.

NodeJS

JSON Web Token (JWT) là một tiêu chuẩn mở (RFC 7519) xác định một cách nhỏ gọn và khép kín để truyền thông tin một cách an toàn giữa các bên như object JSON . Thông tin này có thể được xác minh và đáng tin cậy vì nó được ký điện tử. JWT có thể được ký bằng cách sử dụng bí mật (với thuật toán HMAC) hoặc cặp khóa công khai / riêng tư bằng RSA hoặc ECDSA. — Introduction to JSON Web Tokens

Token web JSON mã hóa và giải mã thông tin người dùng của bạn. Chúng được sử dụng để ủy quyền và trao đổi thông tin.

Chúng bao gồm ba phần – header, payload, và signature – được phân tách bằng dấu chấm (.) Như sau: xxxxx.yyyyy.zzzzz

Trước khi bắt đầu

Hãy đảm bảo cài đặt npm và Postman. Tham khảo thêm về cài đặt NodeJS

Setup Server NodeJS

Nếu bạn chưa có dự án, chúng ta sẽ sử dụng Trivin để thiết lập template dự án. Trong bài viết này, chúng ta sẽ sử dụng nó để tạo một simple-node-server.

npm i trivin -g
trivin server simple-node-server -g -i

Điều này sẽ tạo ra sever NodeJS nhưng có cấu trúc tốt, khởi tạo Git và cài đặt tất cả các phụ thuộc của dự án.

Cài đặt JWT

$ npm i passport passport-jwt winston cors express-validator jsonwebtoken

Setup file hỗ trợ

mkdir store/
touch store/passport.js store/config.js store/utils.js controller/constant.js

Constant.js

  • Đầu tiên, tôi thực sự muốn thực hiện điều gì đó trong tệp constant.js. Thay vì viết nhiều chuỗi, tôi tạo variables cho các chuỗi mà tôi có thể sẽ sử dụng lại.
  • Cho phép TextEditor tự động hoàn thành cho tôi và giảm lỗi chính tả trong chuỗi.
  • Thêm những thứ này vào tệp constant.js trong app NodeJS:
export const EMAIL_IS_EMPTY = 'EMAIL_IS_EMPTY';
export const PASSWORD_IS_EMPTY = 'PASSWORD_IS_EMPTY';
export const PASSWORD_LENGTH_MUST_BE_MORE_THAN_8 =
  'PASSWORD_LENGTH_MUST_BE_MORE_THAN_8';
export const WRONG_PASSWORD = 'WRONG_PASSWORD';
export const SOME_THING_WENT_WRONG = 'SOME_THING_WENT_WRONG';
export const USER_EXISTS_ALREADY = 'USER_EXISTS_ALREADY';
export const USER_DOES_NOT_EXIST = 'USER_DOES_NOT_EXIST';
export const TOKEN_IS_EMPTY = 'TOKEN_IS_EMPTY';
export const EMAIL_IS_IN_WRONG_FORMAT = 'EMAIL_IS_IN_WRONG_FORMAT';

utils.js

  • Một tệp lưu trữ tất cả các chức năng và xác nhận được sử dụng trong toàn bộ dự án.
  • Nó làm rõ code của bạn trong tệp Controller API sạch hơn nhiều.
import sha256 from 'sha256';
import { check } from 'express-validator';
import {
  PASSWORD_IS_EMPTY,
  PASSWORD_LENGTH_MUST_BE_MORE_THAN_8,
  EMAIL_IS_EMPTY,
  EMAIL_IS_IN_WRONG_FORMAT,
} from './constant';
export const generateHashedPassword = password => sha256(password);
export function generateServerErrorCode(res, code, fullError, msg, location = 'server') {
  const errors = {};
  errors[location] = {
    fullError,
    msg,
  };
return res.status(code).json({
    code,
    fullError,
    errors,
  });
}
// ================================
// Validation:
// Handle all validation check for the server
// ================================
export const registerValidation = [
  check('email')
    .exists()
    .withMessage(EMAIL_IS_EMPTY)
    .isEmail()
    .withMessage(EMAIL_IS_IN_WRONG_FORMAT),
  check('password')
    .exists()
    .withMessage(PASSWORD_IS_EMPTY)
    .isLength({ min: 8 })
    .withMessage(PASSWORD_LENGTH_MUST_BE_MORE_THAN_8),
];
export const loginValidation = [
  check('email')
    .exists()
    .withMessage(EMAIL_IS_EMPTY)
    .isEmail()
    .withMessage(EMAIL_IS_IN_WRONG_FORMAT),
  check('password')
    .exists()
    .withMessage(PASSWORD_IS_EMPTY)
    .isLength({ min: 8 })
    .withMessage(PASSWORD_LENGTH_MUST_BE_MORE_THAN_8),
];

Setup Passport.js

  • Một thư viện NodeJS giúp bạn xác thực.
  • Thêm code vào store/passport.js:
/* eslint-disable import/prefer-default-export */
import { Strategy, ExtractJwt } from 'passport-jwt';
import { config, underscoreId } from './config';
import { User } from '../database/models';
export const applyPassportStrategy = passport => {
  const options = {};
  options.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
  options.secretOrKey = config.passport.secret;
  passport.use(
    new Strategy(options, (payload, done) => {
      User.findOne({ email: payload.email }, (err, user) => {
        if (err) {
          return done(err, false);
        }
        if (user) {
          return done(null, {
            email: user.email,
            _id: user[underscoreId]
          });
        }
        return done(null, false);
      });
    })
  );
};

store/config.js là nơi giữ tất cả các cấu hình app:

export const config = {
  passport: {
    secret: '<Add_Your_Own_Secret_Key>',
    expiresIn: 10000,
  },
  env: {
    port: 8080,
    mongoDBUri: 'mongodb://localhost/test',
    mongoHostName: process.env.ENV === 'prod' ? 'mongodbAtlas' : 'localhost',
  },
};
export const underscoreId = '_id';

Sửa đổi app.js để sử dụng nó với passport:

import express from 'express';
import logger from 'winston';
import bodyParser from 'body-parser';
import cors from 'cors';
import passport from 'passport';
import mongoose from 'mongoose';
import { config } from './store/config';
import { applyPassportStrategy } from './store/passport';
import { userController } from './controller';
const app = express();
// Set up CORS
app.use(cors());
// Apply strategy to passport
applyPassportStrategy(passport);
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
// API Route
app.use('/', userController);
/**
 * Get port from environment and store in Express.
 */
const { port, mongoDBUri, mongoHostName } = config.env;
app.listen(port, () => {
  logger.info(`Started successfully server at port ${port}`);
  mongoose
    .connect(mongoDBUri, { useNewUrlParser: true, useUnifiedTopology: true })
    .then(() => {
      logger.info(`Conneted to mongoDB at ${mongoHostName}`);
    });
});

Run App

npm start

Bây giờ hãy quay lại và cải thiện user.controller.js bằng cách áp dụng Passport-jwt cho API của chúng ta.

Sử dụng Passport-jwt để Register/Login API

NodeJS
Image Source: dotnettricks.com
import express from 'express';
import jwt from 'jsonwebtoken';
import { validationResult } from 'express-validator';
import { config } from '../store/config';
import {
  generateHashedPassword,
  generateServerErrorCode,
  registerValidation,
  loginValidation,
} from '../store/utils';
import {
  SOME_THING_WENT_WRONG,
  USER_EXISTS_ALREADY,
  WRONG_PASSWORD,
  USER_DOES_NOT_EXIST,
} from '../store/constant';
import { User } from '../database/models';
const userController = express.Router();
function createUser(email, password) {
  const data = {
    email,
    hashedPassword: generateHashedPassword(password),
  };
  return new User(data).save();
}
/**
 * GET/
 * retrieve and display all Users in the User Model
 */
userController.get('/', (req, res) => {
  User.find({}, (err, result) => {
    res.status(200).json({
      data: result,
    });
  });
});
/**
 * POST/
 * Register a user
 */
userController.post('/register', registerValidation, async (req, res) => {
  const errorsAfterValidation = validationResult(req);
  if (!errorsAfterValidation.isEmpty()) {
    res.status(400).json({
      code: 400,
      errors: errorsAfterValidation.mapped(),
    });
  } else {
    try {
      const { email, password } = req.body;
      const user = await User.findOne({ email });
      if (!user) {
        await createUser(email, password);
        // Sign token
        const newUser = await User.findOne({ email });
        const token = jwt.sign({ email }, config.passport.secret, {
          expiresIn: 10000000,
        });
        const userToReturn = { ...newUser.toJSON(), ...{ token } };
        delete userToReturn.hashedPassword;
        res.status(200).json(userToReturn);
      } else {
        generateServerErrorCode(res, 403, 'register email error', USER_EXISTS_ALREADY, 'email');
      }
    } catch (e) {
      generateServerErrorCode(res, 500, e, SOME_THING_WENT_WRONG);
    }
  }
});
/**
 * POST/
 * Login a user
 */
userController.post('/login', loginValidation, async (req, res) => {
  const errorsAfterValidation = validationResult(req);
  if (!errorsAfterValidation.isEmpty()) {
    res.status(400).json({
      code: 400,
      errors: errorsAfterValidation.mapped(),
    });
  } else {
    try {
      const { email, password } = req.body;
      const user = await User.findOne({ email });
      if (user && user.email) {
        const isPasswordMatched = user.comparePassword(password);
        if (isPasswordMatched) {
          // Sign token
          const token = jwt.sign({ email }, config.passport.secret,
          {
            expiresIn: 1000000,
          });
          const userToReturn = { ...user.toJSON(), ...{ token } };
          delete userToReturn.hashedPassword;
          res.status(200).json(userToReturn);
        } else {
          generateServerErrorCode(res, 403, 'login password error', WRONG_PASSWORD, 'password');
        }
      } else {
        generateServerErrorCode(res, 404, 'login email error', USER_DOES_NOT_EXIST, 'email');
      }
    } catch (e) {
      generateServerErrorCode(res, 500, e, SOME_THING_WENT_WRONG);
    }
  }
});
export default userController;
  • Thay vì sử dụng email và mật khẩu băm của người dùng để ủy quyền, mật khẩu này có thể không được bảo mật trong quá trình giao tiếp giữa máy khách và máy chủ.
  • Chúng ta sử dụng token JWT để ủy quyền. Bằng cách này, chúng ta có thể đảm bảo tính bảo mật của mật khẩu và email của người dùng được mã hóa.

Testing

  • Sử dụng postman để test api
  • Sử dụng method POST/ và url localhost:8080/register và localhost:8080/login.
  • Sau khi kiểm tra API Register, bạn sẽ nhận được thành công kết quả tương tự như bên dưới. Sao chép token vào khay nhớ tạm của bạn.
Image for post
Register API Successful return a token and user’s email + id

Authorization

Hãy xem bạn có muốn truy cập một liên kết cụ thể yêu cầu người dùng login hay không. Sau đó, bạn có thể chỉ cần thêm một ủy quyền trong API.

Hãy xem một ví dụ.

  • Trong user.controller.js, include đơn giản '/' API để truy xuất tất cả user — nhưng tôi không muốn truy xuất tất cả người dùng trừ khi tôi đăng nhập với tư cách người dùng.
  • Một ví dụ khác là Facebook. Nếu bạn muốn truy cập news feed và xem tất cả các bài viết của mình, bạn cần phải đăng nhập.
  • Dưới đây là một ví dụ khi bạn truy cập Route API bảo mật mà không có Token JWT (hay còn gọi là bạn chưa đăng nhập):
Image for post
An example with no JWT attached to the API

Authorization với Passport JWT

Thêm những điểm nổi bật này vào user.controller.js của bạn:

import express from 'express';
import jwt from 'jsonwebtoken';
import passport from 'passport';
import { validationResult } from 'express-validator';
...
/**
 * GET/
 * retrieve and display all Users in the User Model
 */
userController.get(
  '/',
  passport.authenticate('jwt', { session: false }),
  (req, res) => {
    User.find({}, (err, result) => {
      res.status(200).json({
        data: result,
      });
    });
  }
);
...
export default userController;

Bây giờ, hãy kiểm tra API với Postman. Nhấp vào “Authorization” và chọn nhập “Bearer Token”. Sau đó, dán token của bạn vào trường token và chạy:

Image for post

Và bạn đã có thể truy xuất tất cả user ^^

Kết luận

Giờ đây, bạn có thể ủy quyền và bảo mật tất cả các route khác request người dùng login trước khi sử dụng API.

Tìm hiểu thêm về NodeJS: Tracing id request trong ứng dụng NodeJS

Tìm hiểu thê về React: Sử dụng Loading Route trong React


Like it? Share with your friends!

1042