[譯] 用 NodeJS/JWT/Vue 實現基於角色的受權

原文:jasonwatmore.com/post/2018/1…javascript

在本教程中,咱們將完成一個關於如何在 Node.js 中 使用 JavaScript ,並結合 JWT 認證,實現基於角色(role based)受權/訪問的簡單例子。vue

做爲例子的 API 只有三個路由,以演示認證和基於角色的受權:java

  • /users/authenticate - 接受 body 中包含用戶名密碼的 HTTP POST 請求的公開路由。若用戶名和密碼正確,則返回一個 JWT 認證令牌
  • /users - 只限於 "Admin" 用戶訪問的安全路由,接受 HTTP GET 請求;若是 HTTP 頭部受權字段包含合法的 JWT 令牌,且用戶在 "Admin" 角色內,則返回一個包含全部用戶的列表。若是沒有令牌、令牌非法或角色不符,則一個 401 Unauthorized 響應會被返回。
  • /users/:id - 限於經過認證的任何角色用戶訪問的安全路由,接受 HTTP GET 請求;若是受權成功,根據指定的 "id" 參數返回對應用戶記錄。注意 "Admin" 能夠訪問全部用戶記錄,而其餘角色(如 "User")卻只能訪問其本身的記錄。

教程中的項目能夠在 GitHub 上找到: github.com/cornflourbl…node

本地化運行 Node.js 中基於角色的受權 API

  1. 從以上 URL 中下載或 clone 實驗項目
  2. 運行 npm install 安裝必要依賴
  3. 運行 npm start 啓動 API,成功會看到 Server listening on port 4000

運行 Vue.js 客戶端應用

除了能夠用 Postman 等應用直接測試 API,也能夠運行一個寫好的 Vue 項目查看:git

  1. 下載 Vue.js 項目代碼:github.com/cornflourbl…
  2. 運行 npm install 安裝必要依賴
  3. 爲了訪問到咱們的 Node.js 返回的數據而不是使用 Vue 項目的本地假數據,移除或註釋掉 /src/index.js 文件中包含 configureFakeBackend 的兩行
  4. 運行 npm start 啓動應用

Node.js 項目結構

  • _helpers
    • authorize.js
    • error-handler.js
    • role.js
  • users
    • user.service.js
    • users.controller.js
  • config.json
  • server.js

項目由兩個主要的子目錄組成。一個是 「特性目錄」(users),另外一個是 「非特性/共享組件目錄」(_helpers)。github

例子中目前只包含一種 users 特性,但增長其餘特性也能夠照貓畫虎地按照同一模式組織便可。web

Helpers 目錄

路徑: /_helpers數據庫

包含了可被用於多個特性和應用其餘部分的代碼,而且用一個下劃線前綴命名以顯眼的分組它們。express

角色中間件

路徑: /_helpers/authorize.jsnpm

const expressJwt = require('express-jwt');
const { secret } = require('config.json');

module.exports = authorize;

function authorize(roles = []) {
    // 規則參數能夠是一個簡單字符串 (如 Role.User 或 'User')
    // 也能夠是數組 (如 [Role.Admin, Role.User] 或 ['Admin', 'User'])
    if (typeof roles === 'string') {
        roles = [roles];
    }

    return [
        // 認證 JWT 令牌,並向請求對象附加用戶 (req.user)
        expressJwt({ secret }),

        // 基於角色受權
        (req, res, next) => {
            if (roles.length && !roles.includes(req.user.role)) {
                // 未受權的用戶角色
                return res.status(401).json({ message: 'Unauthorized' });
            }

            // 認證受權都齊活
            next();
        }
    ];
}
複製代碼

受權中間件能夠被加入任意路由,以限制經過認證的某種角色用戶的訪問。若是角色參數留空,則對應路由會適用於任何經過驗證的用戶。該中間件稍後會應用在 users/users.controller.js 中。

authorize() 實際上返回了兩個中間件函數。

其中的第一個(expressJwt({ secret }))經過校驗 HTTP 請求頭中的 Authorization 來實現認證。 認證成功時,一個 user 對象會被附加到 req 對象上,前者包含了 JWT 令牌中的數據,在本例中也就是會包含用戶 id (req.user.sub) 和用戶角色 (req.user.role)。sub 是 JWT 中的標準屬性名,表明令牌中項目的 id。

返回的第二個中間件函數基於用戶角色,檢查經過認證的用戶被受權的訪問範圍。

若是認證和受權都失敗則一個 401 Unauthorized 響應會被返回。

全局錯誤處理中間件

路徑: /_helpers/error-handler.js

module.exports = errorHandler;

function errorHandler(err, req, res, next) {
    if (typeof (err) === 'string') {
        // 自定義應用錯誤
        return res.status(400).json({ message: err });
    }

    if (err.name === 'UnauthorizedError') {
        // JWT 認證錯誤
        return res.status(401).json({ message: 'Invalid Token' });
    }

    // 默認處理爲 500 服務器錯誤
    return res.status(500).json({ message: err.message });
}
複製代碼

全局錯誤處理邏輯用來 catch 全部錯誤,也能避免在應用中遍及各類冗雜的處理邏輯。它被配置爲主文件 server.js 裏的中間件。

角色對象/枚舉值

路徑: /_helpers/role.js

module.exports = {
  Admin: 'Admin',
  User: 'User'
}
複製代碼

角色對象定義了例程中的全部角色,用起來相似枚舉值,以免傳遞字符串;因此可使用 Role.Admin 而非 'Admin'

用戶目錄

路徑: /users

users 目錄包含了全部特定於基於角色受權之用戶特性的代碼。

用戶服務

路徑: /users/user.service.js

const config = require('config.json');
const jwt = require('jsonwebtoken');
const Role = require('_helpers/role');

// 這裏簡單的硬編碼了用戶信息,在產品環境應該存儲到數據庫
const users = [
    { id: 1, username: 'admin', password: 'admin', firstName: 'Admin', lastName: 'User', role: Role.Admin },
    { id: 2, username: 'user', password: 'user', firstName: 'Normal', lastName: 'User', role: Role.User }
];

module.exports = {
    authenticate,
    getAll,
    getById
};

async function authenticate({ username, password }) {
    const user = users.find(u => u.username === username && u.password === password);
    if (user) {
        const token = jwt.sign({ sub: user.id, role: user.role }, config.secret);
        const { password, ...userWithoutPassword } = user;
        return {
            ...userWithoutPassword,
            token
        };
    }
}

async function getAll() {
    return users.map(u => {
        const { password, ...userWithoutPassword } = u;
        return userWithoutPassword;
    });
}

async function getById(id) {
    const user = users.find(u => u.id === parseInt(id));
    if (!user) return;
    const { password, ...userWithoutPassword } = user;
    return userWithoutPassword;
}
複製代碼

用戶服務模塊中包含了一個認證用戶憑證並返回一個 JWT 令牌的方法、一個得到應用中全部用戶的方法,和一個根據 id 獲取單個用戶的方法。

由於要聚焦於認證和基於角色的受權,本例中硬編碼了用戶數組,但在產品環境中仍是推薦將用戶記錄存儲在數據庫中並對密碼加密。

用戶控制器

路徑: /users/users.controller.js

const express = require('express');
const router = express.Router();
const userService = require('./user.service');
const authorize = require('_helpers/authorize')
const Role = require('_helpers/role');

// 路由
router.post('/authenticate', authenticate);     // 公開路由
router.get('/', authorize(Role.Admin), getAll); // admin only
router.get('/:id', authorize(), getById);       // 全部經過認證的用戶

module.exports = router;

function authenticate(req, res, next) {
    userService.authenticate(req.body)
        .then(user => user 
            ? res.json(user) 
            : res.status(400)
                .json({ message: 'Username or password is incorrect' }))
        .catch(err => next(err));
}

function getAll(req, res, next) {
    userService.getAll()
        .then(users => res.json(users))
        .catch(err => next(err));
}

function getById(req, res, next) {
    const currentUser = req.user;
    const id = parseInt(req.params.id);

    // 僅容許 admins 訪問其餘用戶的記錄
    if (id !== currentUser.sub && currentUser.role !== Role.Admin) {
        return res.status(401).json({ message: 'Unauthorized' });
    }

    userService.getById(req.params.id)
        .then(user => user ? res.json(user) : res.sendStatus(404))
        .catch(err => next(err));
}
複製代碼

用戶控制器模塊定義了全部用戶的路由。使用了受權中間件的路由受約束於經過認證的用戶,若是包含了角色(如 authorize(Role.Admin))則路由受限於特定的管理員用戶,不然 (e.g. authorize()) 則路由適用於全部經過認證的用戶。沒有使用中間件的路由則是公開可訪問的。

getById() 方法中包含一些額外的自定義受權邏輯,容許管理員用戶訪問其餘用戶的記錄,但禁止普通用戶這樣作。

應用配置

路徑: /config.json

{
    "secret": "THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING"
}
複製代碼

重要: "secret" 屬性被 API 用來簽名和校驗 JWT 令牌從而實現認證,應將其更新爲你本身的隨機字符串以確保無人能生成一個 JWT 去對你的應用獲取未受權的訪問。

主服務器入口

路徑: /server.js

require('rootpath')();
const express = require('express');
const app = express();
const cors = require('cors');
const bodyParser = require('body-parser');
const errorHandler = require('_helpers/error-handler');

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(cors());

// api 路由
app.use('/users', require('./users/users.controller'));

// 全局錯誤處理
app.use(errorHandler);

// 啓動服務器
const port = process.env.NODE_ENV === 'production' ? 80 : 4000;
const server = app.listen(port, function () {
    console.log('Server listening on port ' + port);
});
複製代碼

server.js 做爲 API 的主入口,配置了應用中間件、綁定了路由控制權,並啓動了 Express 服務器。



--End--

搜索 fewelife 關注公衆號

轉載請註明出處

相關文章
相關標籤/搜索