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

更新日期: 2019-11-18阅读: 2.1k

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

  1. 从以上 URL 中下载或 clone 实验项目

  2. 运行 npm install   安装必要依赖

  3. 运行 npm start   启动 API,成功会看到   Server listening on port 4000


运行 vue.js 客户端应用

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

  1. 下载 Vue.js 项目代码:https://github.com/cornflourblue/vue-role-based-authorization-example

  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 )。

例子中目前只包含一种 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 服务器。

原文:https://jasonwatmore.com/post/2018/11/28/nodejs-role-based-authorization-tutorial-with-example-api  

链接: https://www.fly63.com/article/detial/6538

挑战常规--不要使用||赋予默认值

js是一种弱类型的编程语言,代表着传入的变量并不清楚作为何种类型使用。对js来说传入的任意参数都应该考虑不同类型的结果,而不是单单考虑一种情况。若传入0、false等,||所要实现默认值的功能完全错误的

YodaOS 中是如何生成 API 的

本文简单介绍了 YodaOS 在 API 设计过程中,如何利用 DSL,解决 YodaOS API 在多种应用形态保持一致性。以此,我们希望抛砖引玉:帮助读者更好地了解 YodaOS API 的生成过程,帮助读者了解到 DSL,也能将这种思路应用在自己的项目中

JavaScript 私有成员

JavaScript 一直没有私有成员并不是没有原因,所以这一提议给 JavaScript 带来了新的挑战。但同时,JavaScript 在 ES2015 发布的时候已经在考虑私有化的问题了,所以要实现私有成员也并非毫无基础。

web前端学习之路

对于程序员来说,如果哪一天开始他停止了学习,那么他的职业生涯便开始宣告消亡。这不是什么危言耸听的怪语,而是一位大牛几年前告诉我的,他的信条。

web前端开发好学吗?

随着互联网+时代的到来,移动互联网行业的发展也是突飞猛进。无论你是否承认,这个时代已经被网页所包围了,这所有一切,都是前端工程师的杰作。今天给大家聊的就是\"前端真的好学吗?\"

Web前端小白入门

Web前端开发怎么入门,主要都有哪些要素组成?Web前端开发是由网页制作演变而来的,主要由HTML、CSS、JavaScript三大要素组成。专业的Web前端开发入门知识也一定会包含这些内容,下面就给大家简单介绍一下。

highcharts 时间少8小时问题

Highcharts 中默认开启了UTC(世界标准时间),由于中国所在时区为+8,所以经过 Highcharts 的处理后会减去8个小时。如果不想使用 UTC,有2种方法可供使用:

巧妙利用引用,将数组转换成树形数组

笔者所做的一个项目需要做一个前端的树形菜单,后端返回的数据是一个平行的list,list中的每个元素都是一个对象,例如list[0]的值为{id: 1, fid: 0, name: 一级菜单},每个元素都指定了父元素,生成的菜单可以无限级嵌套

forward和redirect的区别?http状态码301,302分别代表什么?

从地址栏显示来说:forward是服务器内部重定向,客户端浏览器的网址不会发生变化;redirect发生一个状态码,告诉服务器去重新请求那个网址,显示的的新的网址数据共享:forward使用的是同一个request

为什么不建议使用 index 作为 key 值

使用 index 作为 key 值有什么问题呢? 在我们日常开发中我们经常会和 key 值打交道. 但是我们扪心自问, 真的理解 key 吗? 我想大多数朋友可能会有些许犹豫.

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!