在本教程中,我们将完成一个关于如何在 Node.js 中 使用 JavaScript ,并结合 JWT 认证,实现基于角色(role based)授权/访问的简单例子。
作为例子的 API 只有三个路由,以演示认证和基于角色的授权:
-
/users/authenticate - 接受 body 中包含用户名密码的 HTTP POST 请求的公开路由。若用户名和密码正确,则返回一个 JWT 认证令牌
-
/users - 只限于 "Admin" 用户访问的安全路由,接受 HTTP GET 请求;如果 HTTP 头部授权字段包含合法的 JWT 令牌,且用户在 "Admin" 角色内,则返回一个包含所有用户的列表。如果没有令牌、令牌非法或角色不符,则一个 401 Unauthorized 响应会被返回。
-
/users/:id - 限于通过认证的任何角色用户访问的安全路由,接受 HTTP GET 请求;如果授权成功,根据指定的 "id" 参数返回对应用户记录。注意 "Admin" 可以访问所有用户记录,而其他角色(如 "User")却只能访问其自己的记录。
教程中的项目可以在 GitHub 上找到:https://github.com/cornflourblue/node-role-based-authorization-api
本地化运行 Node.js 中基于角色的授权 API
-
从以上 URL 中下载或 clone 实验项目
-
运行 npm install 安装必要依赖
-
运行 npm start 启动 API,成功会看到 Server listening on port 4000
运行 vue.js 客户端应用
除了可以用 Postman 等应用直接测试 API,也可以运行一个写好的 vue 项目查看:
-
下载 Vue.js 项目代码:https://github.com/cornflourblue/vue-role-based-authorization-example
-
运行 npm install 安装必要依赖
-
为了访问到我们的 Node.js 返回的数据而不是使用 Vue 项目的本地假数据,移除或注释掉 /src/index.js 文件中包含 configureFakeBackend 的两行
-
运行 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 )。
例子中目前只包含一种 users 特性,但增加其他特性也可以照猫画虎地按照同一模式组织即可。
Helpers 目录
路径: /_helpers
包含了可被用于多个特性和应用其他部分的代码,并且用一个下划线前缀命名以显眼的分组它们。
角色中间件
路径: /_helpers/authorize.js
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 去对你的应用获取未授权的访问。
广州设计公司https://www.houdianzi.com 我的007办公资源网站https://www.wode007.com
主服务器入口
路径: /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 服务器。