用 NodeJS/JWT/Vue 实现基于角色的授权

在本教程中,我们将完成一个关于如何在 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 服务器。