原文: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 关注公众号
转载请注明出处