在本教程中,我们将完成一个关于如何在 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 去对你的应用获取未授权的访问。
主服务器入口
路径: /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 服务器。
本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。