原文: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
npm install
安裝必要依賴npm start
啓動 API,成功會看到 Server listening on port 4000
除了能夠用 Postman 等應用直接測試 API,也能夠運行一個寫好的 Vue 項目查看:git
npm install
安裝必要依賴/src/index.js
文件中包含 configureFakeBackend
的兩行npm start
啓動應用項目由兩個主要的子目錄組成。一個是 「特性目錄」(users
),另外一個是 「非特性/共享組件目錄」(_helpers
)。github
例子中目前只包含一種 users 特性,但增長其餘特性也能夠照貓畫虎地按照同一模式組織便可。web
路徑: /_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 服務器。
搜索 fewelife 關注公衆號
轉載請註明出處