在Node.js中实现OAuth 2.0

2020年12月30日11:16:18 发表评论 37 次浏览

本文概述

每个人都在谈论OAuth 2.0.

无论你工作的公司规模大小或后台运行的服务和API的数量如何, 都有很大的机会需要OAuth2(如果尚未使用的话)。

鉴于那里有大量的信息以及许多不同语言和平台所需的工具和框架, 因此很难理解该协议并将其轻松地应用于你的项目。做到这一点很重要。

对于JavaScript(更具体地说是Node.js), 它还取决于各种因素, 例如你选择的服务器以及它是否已提供OAuth2支持。考虑项目, 文档和社区的成熟度也很重要。

考虑到这一点, 节点-oauth2-服务器来救援。它是与框架无关的模块, 用于在Node.js中实现OAuth2服务器。它是开源的, 简单且易于与你的Node应用程序集成(即使它们已经运行了一段时间)。

在其文档中, 你可以找到官方型号规格描述了你的JS代码如何必须覆盖默认的OAuth2函数以提供自定义身份验证体验。

const model = {
  // We support returning promises.
  getAccessToken: function() {
    return new Promise('works!');
  }, // Or, calling a Node-style callback.
  getAuthorizationCode: function(done) {
    done(null, 'works!');
  }, // Or, using generators.
  getClient: function*() {
    yield somethingAsync();
    return 'works!';
  }, // Or, async/wait (using Babel).
  getUser: async function() {
    await somethingAsync();
    return 'works!';
  }
};

const OAuth2Server = require('oauth2-server');
let oauth = new OAuth2Server({model: model});

有了OAuth2Server对象, 你可以覆盖Express服务器的默认OAuth2提供程序。然后, 我们可以轻松提供你自己的身份验证体验。

请参考官方文档有关该框架如何在后台工作的更多信息。

在本文中, 我们将通过开发自己的改写的实现并通过真实的API对其进行测试, 来探索该框架的一部分, 以便你可以看到该项目处于活动状态并允许访问特定端点。

我们还将其与Postgres数据库集成在一起, 以使示例更加可靠和真实。

我们为制作了一个自定义演示.
不完全是。点击这里查看.

在Node.js中实现OAuth 2.01

我们的示例将探索宇宙的密码赠款类型为了简单起见, 请使用OAuth 2。

根据此示例, 你可以继续进行实施并使它适应其他类型。

设定

首先, 让我们安装所有必需的东西。确保有已安装Postgres到你各自的操作系统。

成功安装后, 创建一个名为" notlogy_oauth2"的新数据库, 并运行以下SQL创建我们的用户和访问令牌表:

CREATE TABLE public.users
(
    id serial, username text, user_password text, PRIMARY KEY (id)
)
WITH (
    OIDS = FALSE
);

ALTER TABLE public.users
    OWNER to postgres;



CREATE TABLE public.access_tokens
(
    id serial, access_token text, user_id integer, PRIMARY KEY (id)
)
WITH (
    OIDS = FALSE
);

ALTER TABLE public.access_tokens
    OWNER to postgres;

我们已对表格进行了最大程度的简化, 因此此处不会涉及与创建或更新日期时间相关的列。

接下来, 在你选择的目录中创建一个新文件夹, 命名为notlogy-oauth2-example并运行npm初始化用你的命令初始化它package.json文件。

然后, 运行以下命令以安装我们需要的依赖项:

npm install body-parser express pg node-oauth2-server

请注意, 它们与Postgres与Node, Express服务器, 节点-oauth2-服务器依赖本身。

如果愿意, 还可以在Yarn下运行命令。在这种情况下, 请按照说明进行操作这里.

最后, 确保重现以下文件夹结构:

Logrocket OAuth2示例的文件夹

文件和文件夹结构。

数据库层

现在, 让我们继续进行数据库设置。成功创建数据库和表之后, 我们需要一个Postgres包装器将要在数据库中进行的查询封装起来。

里面的db文件夹, 将以下代码插入pgWrapper.js文件:

module.exports = {
    query: query, };

const Pool = require("pg").Pool;

function query(queryString, cbFunc) {
    const pool = new Pool({
        user: "postgres", host: "localhost", database: "notlogy_oauth2", password: "postgres", port: 5432, });

    pool.query(queryString, (error, results) => {
        cbFunc(setResponse(error, results));
    });
}

function setResponse(error, results) {
    return {
        error: error, results: results ? results : null, };
}

该代码最重要的部分是查询()功能。与其将Postgres连接池对象扔到任何地方, 我们都将其集中到该文件中, 然后将此函数导出到外部世界。

它很简单, 由一个新的pg组成泳池实例(确保将数据库属性更改为你的属性)和一个回调函数, 该回调函数将始终接收由以下内容组成的JSON对象:错误和一个结果属性。让我们保持结果作为简单的数组。

接下来, 我们将需要两个存储库来处理用户和令牌的数据库操作。第一个是userDB.js文件:

let pgPool;

module.exports = (injectedPgPool) => {
    pgPool = injectedPgPool;

    return {
        register: register, getUser: getUser, isValidUser: isValidUser, };
};

var crypto = require("crypto");

function register(username, password, cbFunc) {
    var shaPass = crypto.createHash("sha256").update(password).digest("hex");

    const query = `INSERT INTO users (username, user_password) VALUES ('${username}', '${shaPass}')`;

    pgPool.query(query, cbFunc);
}

function getUser(username, password, cbFunc) {
    var shaPass = crypto.createHash("sha256").update(password).digest("hex");

    const getUserQuery = `SELECT * FROM users WHERE username = '${username}' AND user_password = '${shaPass}'`;

    pgPool.query(getUserQuery, (response) => {
        cbFunc(
            false, response.results && response.results.rowCount === 1
                ? response.results.rows[0]
                : null
        );
    });
}

function isValidUser(username, cbFunc) {
    const query = `SELECT * FROM users WHERE username = '${username}'`;

    const checkUsrcbFunc = (response) => {
        const isValidUser = response.results
            ? !(response.results.rowCount > 0)
            : null;

        cbFunc(response.error, isValidUser);
    };

    pgPool.query(query, checkUsrcbFunc);
}

我们的数据库模型将恢复三个操作:用户的注册, 搜索和验证。

请注意, 我们正在注入pgPool在我们之前创建的文件的开头。为了使此代码正常工作, 我们仍然需要将参数传递给构造函数中的index.js文件。

每个函数处理我们先前创建的询问功能。 npmpg程序包接收查询本身作为第一个参数。错误结果组合是第二个参数, 其中包含执行结果。

另外, 我们通过$ {}运算符以简化串联。但是, 你也可以使用参数化查询通过在第二个(可选)参数中将值作为数组传递询问功能。

最后, pg包返回结果对象, 但没有任何对象长度属性。这与其他数据库(例如MySQL)不同。

为了查看是否有任何结果, 我们需要访问rowCount属性。

请注意, 我们传递了许多回调函数, 以避免函数返回控件。这将使整个体系结构更加异步。随时根据自己的风格进行调整。

现在, 我们去tokenDB.js实现

let pgPool;

module.exports = (injectedPgPool) => {
    pgPool = injectedPgPool;

    return {
        saveAccessToken: saveAccessToken, getUserIDFromBearerToken: getUserIDFromBearerToken, };
};

function saveAccessToken(accessToken, userID, cbFunc) {
    const getUserQuery = `INSERT INTO access_tokens (access_token, user_id) VALUES ('${accessToken}', ${userID});`;

    pgPool.query(getUserQuery, (response) => {
        cbFunc(response.error);
    });
}

function getUserIDFromBearerToken(bearerToken, cbFunc) {
    const getUserIDQuery = `SELECT * FROM access_tokens WHERE access_token = '${bearerToken}';`;

    pgPool.query(getUserIDQuery, (response) => {
        const userID =
            response.results && response.results.rowCount == 1
                ? response.results.rows[0].user_id
                : null;

        cbFunc(userID);
    });
}

与先前的JS文件非常相似, 我们正在注入pg泳池在构造函数中调用相应的查询。

要特别注意getUserIDFromBearerToken功能。在这里, 遵守默认节点-oauth2-服务器模型合同, 我们需要提供一个函数, 该函数将评估给定的承载令牌是否实际有效。

在这里, 有效表示令牌存在于数据库中。

由于之前的功能, 此功能将起作用isValidUser从userDB.js, 因为它会在插入新用户时检查用户名是否重复。

OAuth2服务和路由

现在我们已经准备好调用数据库层, 让我们实现所需的服务和路由。

我们将从tokenService.js文件:

let userDB;
let tokenDB;

module.exports = (injectedUserDB, injectedTokenDB) => {
    userDB = injectedUserDB;
    tokenDB = injectedTokenDB;

    return {
        getClient: getClient, saveAccessToken: saveAccessToken, getUser: getUser, grantTypeAllowed: grantTypeAllowed, getAccessToken: getAccessToken, };
};

function getClient(clientID, clientSecret, cbFunc) {
    const client = {
        clientID, clientSecret, grants: null, redirectUris: null, };

    cbFunc(false, client);
}

function grantTypeAllowed(clientID, grantType, cbFunc) {
    cbFunc(false, true);
}

function getUser(username, password, cbFunc) {
    userDB.getUser(username, password, cbFunc);
}

function saveAccessToken(accessToken, clientID, expires, user, cbFunc) {
    tokenDB.saveAccessToken(accessToken, user.id, cbFunc);
}

function getAccessToken(bearerToken, cbFunc) {
    tokenDB.getUserIDFromBearerToken(bearerToken, (userID) => {
        const accessToken = {
            user: {
                id: userID, }, expires: null, };

        cbFunc(userID === null, userID === null ? null : accessToken);
    });
}

听起来比实际要复杂一些。所有这些功能都是我们所看到的Model Specification合同的简单覆盖版本。

对于其每个默认操作, 我们需要提供自己的实现, 该实现调用我们的数据库存储库以保存新用户, 并提供新的访问令牌来检索它们或获取客户端应用程序。

请注意, 对于grantTypeAllowed函数, 我们实际上只是在回忆作为第三个参数传递的回调函数(它们将由节点-oauth2-服务器框架)。

在这里, 我们验证给定的客户端ID是否可以真正访问此授权类型(仅设置为密码)。

你可以根据需要添加任意数量的验证。我们还可以将其与你或你的公司可能拥有的其他私人验证API集成在一起。

现在, 继续authenticator.js文件代码:

let userDB;

module.exports = (injectedUserDB) => {
    userDB = injectedUserDB;

    return {
        registerUser: registerUser, login: login, };
};

function registerUser(req, res) {
    userDB.isValidUser(req.body.username, (error, isValidUser) => {
        if (error || !isValidUser) {
            const message = error
                ? "Something went wrong!"
                : "This user already exists!";

            sendResponse(res, message, error);

            return;
        }

        userDB.register(req.body.username, req.body.password, (response) => {
            sendResponse(
                res, response.error === undefined ? "Success!!" : "Something went wrong!", response.error
            );
        });
    });
}

function login(query, res) {}

function sendResponse(res, message, error) {
    res.status(error !== undefined ? 400 : 200).json({
        message: message, error: error, });
}

这里我们有两种主要的身份验证方法:一种用于用户注册, 另一种用于用户登录。

每当尝试注册用户时, 我们首先需要确保其有效(如果不是重复的), 然后进行注册。

我们已经看到了验证和保存功能。现在, 这只是一个电话。

的登录反过来, 该函数不需要任何实现, 因为它将调用框架默认流程。

最后, 检查每个请求是否出错还是成功, 以便我们可以设置适当的HTTP响应代码。

最后, 我们需要设置我们的Express路线:

module.exports = (router, app, authenticator) => {
    router.post("/register", authenticator.registerUser);
    router.post("/login", app.oauth.grant(), authenticator.login);

    return router;
};

很简单, 不是吗?唯一的不同是我们称呼Express

认证

函数

Grant()

确保该用户正确登录。

为了确保实施完全正常, 我们还需要一个安全的测试端点。

它将像其他任何端点一样创建, 但是受到保护。

这意味着只有授权用户才能通过发送有效承载令牌来访问它。

将以下内容添加到我们的testAPIService.js:

module.exports = {
    helloWorld: helloWorld, };

function helloWorld(req, res) {
    res.send("Hello World OAuth2!");
}

而这个testAPIRoutes.js:

module.exports = (router, app, testAPIService) => {
    router.post("/hello", app.oauth.authorise(), testAPIService.helloWorld);

    return router;
};

最后但并非最不重要的一点是, 我们需要设置index.js映射:

// Database imports
const pgPool = require("./db/pgWrapper");
const tokenDB = require("./db/tokenDB")(pgPool);
const userDB = require("./db/userDB")(pgPool);

// OAuth imports
const oAuthService = require("./auth/tokenService")(userDB, tokenDB);
const oAuth2Server = require("node-oauth2-server");

// Express
const express = require("express");
const app = express();
app.oauth = oAuth2Server({
    model: oAuthService, grants: ["password"], debug: true, });

const testAPIService = require("./test/testAPIService.js");
const testAPIRoutes = require("./test/testAPIRoutes.js")(
    express.Router(), app, testAPIService
);

// Auth and routes
const authenticator = require("./auth/authenticator")(userDB);
const routes = require("./auth/routes")(
    express.Router(), app, authenticator
);
const bodyParser = require("body-parser");

app.use(bodyParser.urlencoded({ extended: true }));
app.use(app.oauth.errorHandler());
app.use("/auth", routes);
app.use("/test", testAPIRoutes);

const port = 3000;
app.listen(port, () => {
    console.log(`listening on port ${port}`);
});

在这里, 我们基本上是导入所有必需的模块, 并将相应的模块相互注入。

特别注意Express设置。请注意, 我们正在覆盖默认认证Express对象, 以及我们自己的实现, 以及定义授予类型和模型服务。

然后, 必须将身份验证器和测试的路由分配给Express Router, 以便Express理解如何重定向每个即将到来的请求。

现在进行测试。为了测试端点, 我们将利用邮递员工具因为它简单实用。随意选择你的选择之一。

然后, 通过运行以下命令启动服务器:

node index.js

首先, 我们需要创建一个新用户。为此, 执行POST请求以http:// localhost:3000 / auth / register /以下身体参数(编码为x-www-form-urlencoded):

本地主机注册页面。

通过邮递员创建新用户。

继续检查用户是否在你的数据库中成功创建。

有了有效的用户, 你现在可以登录。为此, 请发送另一个POST请求至http:// localhost:3000 / auth / login /以下身体参数:

本地主机登录。

通过Postman登录创建的用户。

请注意, 如果你将凭据更改为无效的凭据, 则会收到以下消息:OAuth2Error:用户凭据无效.

现在, 随着OAuth2的实施和工作, 我们来进行最重要的测试。

让我们验证我们的安全端点。邮递员为我们提供了一些特殊功能来进行测试:授权标签.

看一下下面的图片:

OAuth2。

邮递员授权标签。

通过选择授权标签, 你可以使用一些有趣的测试功能。

你将获得你的API正在使用的授权流程类型。就我们而言OAuth 2.0.

你还可以选择Postman应该在哪里准确放置授权数据:到请求标头或正文?选择标题选项。

此外, 你有两个选项可在何处检索访问令牌。你可以将令牌文本明确放入可用的文本区域, 或点击"获取新的访问令牌"按钮将依次打开一个包含更多字段的对话框模式。这些字段将要求访问令牌URL端点获取新的, TTL, 授权类型等。

在这里, 你可以预览请求。单击按钮后, 输入的值将自动转换为当前请求的标题和主体配置。这样, 你无需在每次需要运行新请求时都手动更改每个标头。

点击发送按钮, 然后Hello World OAuth2结果将出现。

总结

你可以找到此示例的完整源代码这里.

该框架只是那里可用的选项之一。你可以去OAuth.net项目, 并查看有关Node.js的最新建议以及你的首选语言。

当然, 有很多值得一看的地方。

OAuth2是一个庞大的协议, 在阅读和应用其规范时值得更多的时间和精力。但是, 此简单的介绍将使你了解框架与Express和Postgres一起工作的方式。

你还可以更改服务器和数据库以切换你的需求。只要确保使用我们到目前为止已建立的相同合同即可。

关于你的学习, 请不要将自己专门锁定于此框架。还有许多其他取决于你使用的前端框架(例如, React, Angular和Vue, 还有其他好的库可以帮助你解决此问题)。

祝好运!

只有200 监视生产中失败和缓慢的网络请求

部署基于节点的Web应用程序或网站很容易。确保你的Node实例继续为你的应用程序提供资源是一件很困难的事情。如果你希望确保成功完成对后端或第三方服务的请求,

尝试notlogy

.

LogRocket网络请求监控

https://notlogy.com/signup/

日志火箭就像Web应用程序的DVR一样, 实际上记录了你网站上发生的一切。无需猜测问题发生的原因, 你可以汇总并报告有问题的网络请求, 以快速了解根本原因。

notlogy用你的应用程序记录基线性能计时, 例如页面加载时间, 到第一个字节的时间, 缓慢的网络请求, 并记录Redux, NgRx和Vuex的操作/状态。

免费开始监控

.

一盏木

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: