[译] 用 NodeJS/JWT/Vue 实现基于角色的受权

原文: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

本地化运行 Node.js 中基于角色的受权 API

  1. 从以上 URL 中下载或 clone 实验项目
  2. 运行 npm install 安装必要依赖
  3. 运行 npm start 启动 API,成功会看到 Server listening on port 4000

运行 Vue.js 客户端应用

除了能够用 Postman 等应用直接测试 API,也能够运行一个写好的 Vue 项目查看:git

  1. 下载 Vue.js 项目代码:github.com/cornflourbl…
  2. 运行 npm install 安装必要依赖
  3. 为了访问到咱们的 Node.js 返回的数据而不是使用 Vue 项目的本地假数据,移除或注释掉 /src/index.js 文件中包含 configureFakeBackend 的两行
  4. 运行 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)。github

例子中目前只包含一种 users 特性,但增长其余特性也能够照猫画虎地按照同一模式组织便可。web

Helpers 目录

路径: /_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 服务器。



--End--

搜索 fewelife 关注公众号

转载请注明出处

相关文章
相关标签/搜索