创建后端认证
在第十章 “添加 Vuex Store” 和第十一章 “编写路由中间件和服务器中间件” 的前面练习中,我们为后端身份验证使用了虚拟用户,特别是在 /chapter-11/nuxt-universal/route-middleware/per-route/
中用于单个路由中间件,例如:
// server/modules/public/user/_routes/login.js
router.post('/login', async (ctx, next) => {
let request = ctx.request.body || {}
if (request.username === 'demo' && request.password === 'demo') {
let payload = { id: 1, name: 'Alexandre', username: 'demo' }
let token = jwt.sign(payload, config.JWT_SECRET, { expiresIn: 1 * 60 })
//...
}
})
但是在本章中,我们将使用包含一些用户数据的数据库进行身份验证。此外,在第九章 “添加服务器端数据库” 中,我们使用了 MongoDB
作为我们的数据库服务器。但这一次,为了多样性,让我们尝试一个不同的数据库系统——MySQL
。那么,让我们开始吧。
使用 MySQL 作为服务器数据库
请确保你的本地机器上已安装 MySQL
服务器。在撰写本书时,最新的 MySQL 版本是 5.7
。根据你使用的操作系统,你可以在 https://dev.mysql.com/doc/mysql-installation-excerpt/5.7/en/installing.html 找到适用于你系统的具体指南。如果你使用的是 Linux
,你可以在 https://dev.mysql.com/doc/mysql-installation-excerpt/5.7/en/linux-installation.html 找到适用于你的 Linux
发行版的安装指南。如果你使用的是 Linux Ubuntu
并使用 APT
存储库,你可以遵循 https://www.google.com/search?q=https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/apt-repo-fresh-install 上的指南。
或者,你可以安装 MariaDB
服务器而不是 MySQL
服务器,以便在你的项目中使用关系数据库管理系统 (DBMS
)。同样,根据你使用的操作系统,你可以在 https://mariadb.com/downloads/ 找到适用于你系统的具体指南。如果你使用的是 Linux
,你可以在 https://www.google.com/search?q=https://downloads.mariadb.org/mariadb/repositories/ 找到适用于你的特定 Linux
发行版的指南。如果你使用的是 Linux Ubuntu 19.10
,你可以遵循 https://www.google.com/search?q=https://downloads.mariadb.org/mariadb/repositories/%23distro%3DUbuntudistro_release%3Deoan—ubuntu_eoanmirror%3Dbmeversion%3D10.4 上的指南。
无论你选择哪个,拥有一个管理工具来从浏览器管理你的 MySQL
数据库都很方便。你可以使用 phpMyAdmin
或 Adminer
(https://www.google.com/search?q=https://www.adminer.org/latest.php);两者都需要你的机器上安装 PHP
。如果你是 PHP
的新手,你可以使用第十六章 “为 Nuxt 创建一个与框架无关的 PHP API” 中的安装指南。本书更推荐使用 Adminer
。你可以在 https://www.phpmyadmin.net/downloads/ 下载该程序。如果你想使用 phpMyAdmin
,请访问 https://www.phpmyadmin.net/ 以了解更多信息。一旦你拥有了管理工具,请按照以下步骤设置我们将在本章中使用的数据库:
-
使用
Adminer
创建一个数据库,例如 “nuxt-auth”。 -
在该数据库中插入以下表和示例数据:
DROP TABLE IF EXISTS users; CREATE TABLE users ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(255) NOT NULL, email varchar(255) NOT NULL, username varchar(255) NOT NULL, password varchar(255) NOT NULL, created_on datetime NOT NULL, last_on datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY email (email), UNIQUE KEY username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO users (id, name, email, username, password, created_on, last_on) VALUES (1, 'Alexandre', 'demo@gmail.com', 'demo', '$2a$10$pyMYtPfIvE.PAboF3cIx9.IsyW73voMIRxFINohzgeV0I2BxwnrEu', '2019-06-17 00:00:00', '2019-01-21 23:32:58');
上述示例数据中的用户密码是 123123
,并使用 bcrypt
加密为 $2a$10$pyMYtPfIvE.PAboF3cIx9.IsyW73voMIRxFINohzgeV0I2BxwnrEu
。我们将在服务器端安装并使用 bcryptjs
Node.js 模块来哈希和验证此密码。但是在深入了解 bcryptjs
之前,让我们先看看我们将在下一节中创建的应用的结构。
你可以在我们的 |
组织跨域应用目录
我们一直在为单个域名构建 Nuxt
应用。自第八章 “添加服务器端框架” 以来,我们的服务器端 API
就与 Nuxt
紧密耦合在一起,在该章中,我们使用 Koa
作为服务器端框架和 API
,用于处理和提供 Nuxt
应用的数据。如果你回顾一下 GitHub
仓库中的 /chapter-8/nuxt-universal/koa-nuxt/
,你应该记得我们将服务器端程序和文件保存在 /server/
目录下。我们还将包/模块依赖项保存在一个 package.json
文件中,并安装在同一个 /node_modules/
目录下。当我们的应用变得更大时,将两个框架(Nuxt
和 Koa
)的模块依赖项混合在同一个 package.json
文件中最终可能会令人困惑。它也可能使调试过程更加困难。因此,将我们由 Nuxt
和 Koa
(或任何其他服务器端框架,如 Express
)组成的单个应用分离成两个独立的应用程序可能更有利于可伸缩性和维护。现在,是时候构建一个跨域的 Nuxt
应用了。我们将重用和重构第八章 “添加服务器端框架” 中的 Nuxt
应用。让我们将我们的 Nuxt
应用称为前端应用,将 Koa
应用称为后端应用。我们将在这两个应用中分别添加新的模块。
后端应用将进行后端身份验证,而前端应用将单独进行前端身份验证,但它们最终将作为一个整体运行。为了使这个学习和重构过程对你来说更容易,我们将仅使用 JWT 进行身份验证。因此,让我们按照以下步骤创建我们的新工作目录:
-
创建一个项目目录,并使用你喜欢的任何名称命名它,并在其中创建两个子目录。一个名为
frontend
,另一个名为backend
,如下所示:<项目名称> ├── frontend └── backend
-
使用脚手架工具
create-nuxt-app
在/frontend/
目录下安装Nuxt
应用,这样你就可以得到你已经熟悉的Nuxt
目录,如下所示:frontend ├── package.json ├── nuxt.config.js ├── store │ ├── index.js │ └── ... └── pages ├── index.vue └── ...
-
在
/backend/
目录下创建一个package.json
文件、一个backpack.config.js
文件、一个/static/
文件夹和一个/src/
文件夹,然后在/src/
文件夹中创建其他文件和子文件夹(我们将在接下来的章节中更详细地介绍它们),如下所示:backend ├── package.json ├── backpack.config.js ├── assets │ └── ... ├── static │ └── ... └── src ├── index.js ├── ... ├── modules │ └── ... └── core └── ...
backend
目录是我们 API
所在的位置,可以使用 Express
或 Koa
构建。我们将继续使用你已经熟悉的 Koa
。我们将在该目录中安装服务器端依赖项,例如 mysql
、bcryptjs
和 jsonwebtoken
,这样它们就不会与 Nuxt
应用的前端模块混淆。
正如你所看到的,在这个新的结构中,我们成功地将我们的 API
与 Nuxt
应用完全分离和解耦。这对于调试和开发都有好处。从技术上讲,我们现在将一次开发和测试一个应用。在一个环境中开发两个应用可能会令人困惑,并且当应用变得更大时(正如我们前面提到的),协作可能会很困难。
在研究如何在服务器端使用 JWT
之前,让我们首先在下一节中更深入地了解如何在 /src/
目录中组织 API
路由和模块。
创建 API 公共/私有路由及其模块
请注意,不强制要求遵循本书中建议的目录结构。关于如何使用 Koa
组织我们的应用程序,没有武断的或官方的规则。Koa
社区贡献了一些骨架、样板和框架,你可以通过访问 https://github.com/koajs/koa/wiki 来查看它们。现在,让我们更仔细地看看 /src/
目录下的目录结构,我们将在其中开发我们的 API
源代码,请按照以下步骤操作:
-
在
/src/
目录下创建以下文件夹和空的.js
文件:└── src ├── index.js ├── middlewares.js ├── routes-private.js ├── routes-public.js ├── config │ └── index.js ├── core │ └── database ├── middlewares │ ├── authenticate.js │ ├── errorHandler.js │ └── ... └── modules └── ...
在
/src/
目录下,/middlewares/
目录用于存放所有中间件,例如authenticate.js
,我们希望使用Kao
的app.use
方法注册它,而/modules/
目录用于存放所有API
端点的分组,例如home
、user
和login
。 -
创建两个主要目录
private
和public
,并在每个目录下创建子目录,如下所示:└── modules ├── private │ └── home └── public ├── home ├── user └── login
/public/
目录用于无需JWT
即可公开访问的模块,例如登录路由,而/private/
目录用于需要JWT
保护的模块。正如你所看到的,我们将API
路由分成两个主要组,因此/private/
组将在routes-private.js
中处理,而/public/
组将在routes-public.js
中处理。我们有/config/
目录用于存放所有配置文件,以及/core/
目录用于存放可以在整个应用程序中共享和使用的抽象程序或模块,例如你将在本章后面发现的mysql
连接池。因此,从上面的目录树中,我们将在我们的API
中使用这些公共模块:home
、user
、login
和一个私有模块:home
。 -
在每个模块中,例如在
user
模块中,创建一个/_routes/
目录来配置属于此特定模块(或组)的所有路由(或端点):└── user ├── index.js └── _routes ├── index.js └── fetch-user.js
在这个
user
模块中,/user/index.js
文件用于组装和分组该模块的所有路由到模块路由中,例如:// src/modules/public/user/index.js import Router from 'koa-router' import fetchUsers from './_routes' import fetchUser from './_routes/fetch-user' const router = new Router({ prefix: '/users' }) const routes = [fetchUsers, fetchUser] for (var route of routes) { router.use(route.routes(), route.allowedMethods()) } export default router
设置为
prefix
键的/users
值是此user
模块的模块路由。在每个导入的子路由中,我们开发我们的代码,例如登录路由的代码。 -
在每个模块的每个
.js
文件中,例如在user
模块中,添加以下基本代码结构,以便在后面的阶段构建我们的代码:// src/modules/public/user/_routes/index.js import Router from 'koa-router' import pool from 'core/database/mysql' const router = new Router() router.get('/', async (ctx, next) => { // 代码写在这里.... }) export default router
-
让我们创建
home
模块,它将返回一个包含 “Hello World!” 消息的响应,如下所示:// src/modules/public/home/_routes/index.js import Router from 'koa-router' const router = new Router() router.get('/', async (ctx, next) => { ctx.type = 'json' ctx.body = { message: 'Hello World!' } }) export default router
-
home
模块只有一个路由,但我们仍然需要在该模块的index.js
文件中组装这个路由,以便我们的代码与其他模块保持一致,如下所示:// src/modules/public/home/index.js import Router from 'koa-router' import index from './_routes' const router = new Router() // 没有前缀 const routes = [index] for (var route of routes) { router.use(route.routes(), route.allowedMethods()) } export default router
请注意,此
home
模块没有添加前缀,因此我们可以直接在localhost:4000/public
访问其唯一的路由。 -
在
/src/
目录下创建routes-public.js
文件,并从/modules/
目录下的公共模块导入所有公共路由,如下所示:// src/routes-public.js import Router from 'koa-router' import home from './modules/public/home' import user from './modules/public/user' import login from './modules/public/login' const router = new Router({ prefix: '/public' }) const modules = [home, user, login] for (var module of modules) { router.use(module.routes(), module.allowedMethods()) } export default router
正如你所看到的,我们导入了我们在前面步骤中刚刚创建的
home
模块。我们将在接下来的章节中创建user
和login
模块。导入这些模块后,我们应该将它们的路由注册到路由器,然后导出该路由器。请注意,这些路由添加了一个前缀/public
。另请注意,每个路由都通过普通的JavaScript for
循环函数进行循环并注册到路由器。 -
在
/src/
目录下创建routes-private.js
文件,并从/modules/
目录下的私有模块导入所有私有路由,如下所示:// src/routes-private.js import Router from 'koa-router' import home from './modules/private/home' import authenticate from './middlewares/authenticate' const router = new Router({ prefix: '/private' }) const modules = [home] for (var module of modules) { router.use(authenticate, module.routes(), module.allowedMethods()) } export default router
在这个文件中,你可以看到我们将在接下来的章节中仅创建一个私有的
home
模块。此外,authenticate
中间件被导入到这个文件中并添加到私有路由,以便保护私有模块。之后,我们应该使用路由器导出私有路由,并为其添加/private
前缀。我们将在接下来的章节中创建这个authenticate
中间件。现在,让我们使用Backpack
配置我们的模块文件路径,并安装我们的API
必需的基本Node.js
模块。 -
通过
Backpack
配置文件将以下额外的文件路径(./src
、./src/core
和./src/modules
)添加到webpack
配置中:// backpack.config.js module.exports = { webpack: (config, options, webpack) => { config.resolve.modules = ['./src', './src/core', './src/modules'] return config } }
有了这些额外的文件路径,我们可以简单地使用
import pool from 'core/database/mysql'
导入我们的模块,而不是使用以下代码:import pool from '../../../../core/database/mysql'
有关使用
webpack
中的modules
选项解析模块的更多信息,请访问 https://www.google.com/search?q=https://webpack.js.org/configuration/resolve/%23resolvemodules。 -
现在我们应该在我们的项目中安装
Backpack
,以及开发这个后端应用所需其他基本和必要的Node.js
模块:$ npm i backpack-core $ npm i cross-env $ npm i koa $ npm i koa-bodyparser $ npm i koa-favicon $ npm i koa-router $ npm i koa-static
你应该熟悉这些模块,因为你在第八章 “添加服务器端框架”(你可以在我们的
GitHub
仓库中的 /chapter-8/nuxt-universal/koa-nuxt/ 中回顾一下)、第十章 “添加 Vuex Store”(在/chapter-10/nuxt-universal/nuxtServerInit/
中)和第十一章 “编写路由中间件和服务器中间件”(在/chapter-11/nuxt-universal/route-middleware/per-route/
中)中学习并安装过它们。 -
在
/backend/
目录的package.json
文件中添加以下运行脚本:// package.json { "scripts": { "dev": "backpack", "build": "backpack build", "start": "cross-env NODE_ENV=production node build/main.js" } }
因此,“dev” 运行脚本用于开发我们的
API
,“build” 运行脚本用于在完成时构建我们的API
,“start” 脚本用于在构建后提供API
服务。 -
将以下服务器配置添加到
/config/
目录下的index.js
文件中:// src/config/index.js export default { server: { port: 4000 }, }
这个配置文件只有一个非常简单的配置,即服务器,配置为在
4000
端口运行。 -
导入你刚刚安装的以下模块,并将它们注册为
/src/
目录下的middlewares.js
文件中的中间件,如下所示:// src/middlewares.js import serve from 'koa-static' import favicon from 'koa-favicon' import bodyParser from 'koa-bodyparser' export default (app) => { app.use(serve('assets')) app.use(favicon('static/favicon.ico')) app.use(bodyParser()) }
-
在
/middlewares/
目录中创建一个处理HTTP
响应并返回200 HTTP
状态码的中间件:// src/middlewares/okOutput.js export default async (ctx, next) => { await next() if (ctx.status === 200) { ctx.body = { status: 200, data: ctx.body } } }
如果响应成功,我们将得到以下
JSON
输出:{"status":200,"data":{"message":"Hello World!"}}
-
创建一个处理
HTTP
错误状态(如400
、404
和500
)的中间件:// src/middlewares/errorHandler.js export default async (ctx, next) => { try { await next() } catch (err) { ctx.status = err.status || 500 ctx.type = 'json' ctx.body = { status: ctx.status, message: err.message } ctx.app.emit('error', err, ctx) } }
对于
400
错误响应,你将得到以下JSON
响应:{"status":400,"message":"username param is required."}
-
创建一个专门处理
HTTP
404
响应的中间件,通过抛出 “Not found” 消息:// src/middlewares/notFound.js export default async (ctx, next) => { await next() if (ctx.status === 404) { ctx.throw(404, 'Not found') } }
对于未知路由,我们将得到以下
JSON
输出:{"status":404,"message":"Not found"}
-
将这三个中间件导入
middlewares.js
,并将它们注册到Koa
实例,就像其他中间件一样:// src/middlewares.js import serve from 'koa-static' import favicon from 'koa-favicon' import bodyParser from 'koa-bodyparser' import errorHandler from './middlewares/errorHandler' import notFound from './middlewares/notFound' import okOutput from './middlewares/okOutput' export default (app) => { app.use(errorHandler) app.use(notFound) app.use(okOutput) app.use(serve('assets')) app.use(favicon('static/favicon.ico')) app.use(bodyParser()) }
请注意我们如何按顺序排列这些中间件——即使
errorHandler
中间件首先注册,如果HTTP
响应中存在错误,它也是在Koa
的上游级联中最后重新执行的中间件。如果HTTP
响应状态为200
,上游级联将在okOutput
中间件处停止。另请注意,这些中间件必须在static
、favicon
和bodyparser
中间件之后注册,这些中间件必须在下游级联中首先被调用和公开服务。 -
从
routes-public.js
和routes-private.js
导入公共和私有路由,并在前面的中间件之后注册它们,如下所示:// Import custom local middlewares. import routesPublic from './routes-public'; import routesPrivate from './routes-private'; export default (app) => { app.use(routesPublic.routes(), routesPublic.allowedMethods()); app.use(routesPrivate.routes(), routesPrivate.allowedMethods()); };
-
导入
Koa
,来自middlewares.js
文件的所有中间件,以及/config/
目录下的index.js
文件中的服务器配置,实例化一个Koa
实例并将其传递给middlewares.js
文件,然后使用此Koa
实例启动服务器:// index.js import Koa from 'koa'; import config from './config'; import middlewares from './middlewares'; const app = new Koa(); const host = process.env.HOST || '127.0.0.1'; const port = process.env.PORT || config.server.port; middlewares(app); app.listen(port, host);
-
使用
npm run dev
运行此API
,你应该在浏览器中看到应用运行在localhost:4000
上。当你访问localhost:4000
时,你应该在浏览器上看到以下输出:{"status":404,"message":"Not found"}
这是因为
/
上不再设置任何路由——我们已经为所有路由添加了/public
或/private
前缀。但是,如果你导航到localhost:4000/public
,你将得到以下JSON
输出:{"status":200,"data":{"message":"Hello World!"}}
这是我们前面步骤中刚刚创建的
home
模块的响应。此外,你应该看到你的favicon
和assets
在localhost:4000
上正确地提供服务——如果你已将它们放置在/static/
和/assets/
目录中,例如:localhost:4000/sample-asset.jpg localhost:4000/favicon.ico
你可以在 localhost:4000
上看到这两个目录中的文件。这是因为在 Koa
的下游级联发生时,static
和 favicon
中间件被安装并注册为首先执行在中间件栈中。
做得好!现在你已经准备好了新的工作目录,并且一个基本的 API
正在运行,就像第八章 “添加服务器端框架” 中一样。接下来,你需要在 /backend/
目录中安装其他服务器端依赖项,并开始向公共的 user
和 login
模块以及私有的 home
模块中的路由添加代码。让我们从下一节中的 bcryptjs
开始。
你可以在我们的 |
使用 bcryptjs 模块(Node.js)
正如我们之前提到的,bcryptjs
用于哈希和验证密码。请查看以下简化步骤,以获取有关如何在我们的应用中使用此模块的更多建议:
-
通过
npm
安装bcryptjs
模块:$ npm i bcryptjs
-
通过使用请求体(
request
)中从客户端发送的密码添加盐(salt
)来哈希密码,例如,在user
模块中创建新用户期间:// src/modules/public/user/_routes/create-user.js import bcrypt from 'bcryptjs' const saltRounds = 10 const salt = bcrypt.genSaltSync(saltRounds) const hashed = bcrypt.hashSync(request.password, salt)
请注意,为了加快本章的身份验证课程,我们跳过了创建新用户的过程。但是在更完整的
CRUD
中,你可以使用此步骤来哈希用户提供的密码。 -
通过比较从客户端发送的密码(
request
)与数据库中存储的密码来验证密码,例如,在login
模块中的登录身份验证过程中,如下所示:// src/modules/public/login/_routes/local.js import bcrypt from 'bcryptjs' const isMatched = bcrypt.compareSync(request.password, user.password) if (isMatched === false) { ... }
请注意,你可以在我们的
GitHub
仓库中的/chapter-12/nuxt-universal/cross-domain/jwt/axiosmodule/backend/src/modules/public/login/_routes/local.js
中找到此步骤在我们的后端应用中的应用方式。
我们将在下一节向你展示如何使用 bcryptjs
来验证来自客户端的传入密码。但是在哈希和验证来自客户端的密码之前,首先,我们需要连接到我们的 MySQL
数据库,以便确定是插入新用户还是查询现有用户。为此,我们的应用需要下一个 Node.js
模块:mysql
- 一个 MySQL
客户端。因此,让我们继续下一节,看看如何安装和使用它。
如果你想了解更多关于此模块的信息以及一些异步示例,请访问 https://www.google.com/search?q=https://github.com/dcodeIO/bcrypt.js。 |
使用 mysql 模块(Node.js)
我们在上一节中安装了 MySQL
服务器。现在我们需要一个 MySQL
客户端,我们可以连接到 MySQL
服务器并从我们的服务器端程序执行 SQL
查询。mysql
是标准的 MySQL
Node.js
模块,它实现了 MySQL
协议,因此无论你使用的是 MySQL
服务器还是 MariaDB
服务器,我们都可以使用这个模块来处理 MySQL
连接和 SQL
查询。那么,让我们按照以下步骤开始:
-
通过
npm
安装mysql
模块:$ npm i mysql
-
使用你的
MySQL
连接详细信息在/src/
目录的子目录中创建一个mysql.js
文件,并在其中创建MySQL
连接实例,如下所示:// src/core/database/mysql.js import util from 'util' import mysql from 'mysql' const pool = mysql.createPool({ connectionLimit: 10, host : 'localhost', user : '<用户名>', password : '<密码>', database : '<数据库>' }) pool.getConnection((err, connection) => { if (error) { // 处理错误 ... } // 如果没有错误,将连接释放回连接池。 if (connection) { connection.release() } return }) pool.query = util.promisify(pool.query) export default pool
让我们通过以下注释来理解我们刚刚创建的代码:
-
mysql
不支持async/await
,因此我们使用Node.js
的util.promisify
工具包装了MySQL
的pool.query
。pool.query
是mysql
模块中处理SQL
查询的函数,它通过回调函数返回结果,例如:connection.query('SELECT ...', function (error, results, fields) { if (error) { throw error } // 做一些事情 ... })
通过
util.promisify
工具,我们消除了回调函数,现在我们可以使用async/await
,如下所示:let result = null try { result = await pool.query('SELECT ...') } catch (error) { // 处理错误 ... }
-
pool.query
是这三个函数pool.getConnection、connection.query
和connection.release
的快捷方式函数,我们应该一起使用它们在mysql
模块的连接池中执行SQL
查询。通过使用pool.query
,当你完成操作后,连接会自动释放回连接池。这是pool.query
函数的基本底层结构:import mysql from 'mysql' const pool = mysql.createPool(...) pool.getConnection(function(error, connection) { if (error) { throw error } connection.query('SELECT ...', function (error, results, fields) { connection.release() if (error) { throw error } }) })
-
在这个
mysql
模块中,我们没有使用mysql.createConnection
逐一创建和管理MySQL
连接(这可能是一项开销很大的操作),而是使用mysql.createPool
进行连接池管理。连接池是可重用数据库连接的缓存,用于减少每次想要连接数据库时建立新连接的成本。有关连接池的更多信息,请访问 https://www.google.com/search?q=https://github.com/mysqljs/mysql%23pooling-connections。
-
-
因此,我们已将
MySQL
连接抽象到/core/
目录中的上述文件中。现在我们可以像下面这样在用户(user
)模块中使用它来获取用户列表:// backend/src/modules/public/user/_routes/index.js import Router from 'koa-router'; import pool from 'core/database/mysql'; const router = new Router(); router.get('/', async (ctx, next) => { try { var users = await pool.query('SELECT `id`, `name`, `created_on` FROM `users`'); } catch (err) { // Handle the error appropriately, e.g., log it and set an error response console.error('Error fetching users:', err); ctx.status = 500; ctx.body = { error: 'Failed to fetch users' }; return; } ctx.type = 'json'; ctx.body = users; }); export default router;
你可以看到,我们使用了在前一节中介绍的相同的代码结构,通过
MySQL
连接池将请求发送到MySQL
服务器。在我们发送的查询中,我们告诉MySQL
服务器在结果中只返回users
表的id
、name
和created_on
字段。 -
如果你访问 localhost:4000/public/users 这个用户路由,你应该在屏幕上看到以下输出:
{"status":200,"data":[{"id":1,"name":"Alexandre","created_on":"2019-06-16T22:00:00.000Z"}]}
现在我们有了用于连接 MySQL
服务器和数据库的 mysql
模块,以及用于哈希和验证客户端密码的 bcryptjs
模块,因此我们可以重构和改进我们在前一章中粗略创建的登录代码。让我们在下一节中了解如何操作。
如果你想了解更多关于 |
重构服务器端的登录代码
我们在前面的章节中收集了所有必要的要素,一旦创建了 MySQL
连接池,我们就可以按照以下步骤重构和改进我们在第十章 “添加 Vuex Store” 和第十一章 “编写路由中间件和服务器中间件” 中的登录代码:
-
导入所有依赖项,例如
koa-router
、jsonwebtoken
、bcryptjs
和登录路由的MySQL
连接池,如下所示:// src/modules/public/login/_routes/local.js import Router from 'koa-router' import jwt from 'jsonwebtoken' import bcrypt from 'bcryptjs' import pool from 'core/database/mysql' import config from 'config' const router = new Router() router.post('/login', async (ctx, next) => { let request = ctx.request.body || {} //... }) export default router
我们在这里导入了
config
文件,用于存储我们API
的配置选项,其中包含MySQL
数据库连接详细信息、服务器和静态目录的选项,以及我们稍后签署令牌所需的JWT
密钥。 -
验证登录路由的
post
方法中的用户输入,以确保它们已定义且不为空:if (request.username === undefined) { ctx.throw(400, 'username 参数是必需的。') } if (request.password === undefined) { ctx.throw(400, 'password 参数是必需的。') } if (request.username === '') { ctx.throw(400, 'username 是必需的。') } if (request.password === '') { ctx.throw(400, 'password 是必需的。') }
-
当用户名和密码通过验证后,将它们赋值给变量,以便查询数据库:
let username = request.username let password = request.password let users = [] try { users = await pool.query('SELECT * FROM users WHERE username = ?', [username]) } catch(err) { ctx.throw(400, err.sqlMessage) } if (users.length === 0) { ctx.throw(404, '未找到用户') }
-
如果
MySQL
查询有结果,则使用bcryptjs
比较存储的密码和用户输入的密码:let user = users[0] let match = false try { match = await bcrypt.compare(password, user.password) } catch(err) { ctx.throw(401, err) } if (match === false) { ctx.throw(401, '密码无效') }
-
如果用户通过了之前的所有步骤和验证,则签署一个
JWT
并将其发送给客户端:let payload = { name: user.name, email: user.email } let token = jwt.sign(payload, config.JWT_SECRET, { expiresIn: 1 * 60 }) ctx.body = { user: payload, message: '登录成功', token: token }
-
使用
npm run dev
运行 API,并在你的终端上使用curl
手动测试之前的路由,如下所示:$ curl -X POST -d "username=demo&password=123123" -H "Content-Type: application/x-www-form-urlencoded" http://localhost:4000/public/login/local
如果登录成功,你将得到以下结果:
{"status":200,"data":{"user":{"name":"Alexandre","email":"demo@gmail.com"},"message":"logged in ok","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQWxleGFuZHJlIiwiZW1haWwiOiJkZW1vQGdtYWlsLmNvbSIsImlhdCI6MTY4MjgwMzYwNSwiZXhwIjoxNjgyODAzNjY1fQ.someRandomToken"}}
当然,每次成功签署后,你在上述响应中都会得到不同的令牌。现在你已经成功地重构和改进了登录代码。接下来,我们将在下一节中了解如何验证客户端发送回请求标头的上述令牌。请继续阅读!
在服务器端验证传入的令牌
我们已经成功地签署了一个令牌,并在凭据与数据库中存储的凭据匹配时将其返回给客户端。但这只是一半的故事。每次客户端使用该令牌发出请求时,我们都应该验证该令牌,以访问所有受服务器端中间件保护的受保护路由。
因此,让我们按照以下步骤创建中间件和受保护的路由:
-
在
/src/
目录下的/middlewares/
目录中创建一个中间件文件,代码如下:// src/middlewares/authenticate.js import jwt from 'jsonwebtoken' import config from 'config' export default async (ctx, next) => { if (!ctx.headers.authorization) { ctx.throw(401, '受保护的资源,请使用 Authorization 标头获取访问权限') } const token = ctx.headers.authorization.split(' ')[1] try { ctx.state.jwtPayload = jwt.verify(token, config.JWT_SECRET) } catch (err) { // 处理错误。 ctx.throw(401, '无效的令牌') } await next() }
if (!ctx.headers.authorization)
条件用于确保客户端已在请求标头中包含令牌。由于授权信息以Bearer: [token]
的格式出现,其中包含一个空格,我们按空格拆分该值,并在try
和catch
块中仅获取[token]
进行验证。如果令牌有效,那么我们使用await next()
将请求传递到下一个路由。 -
导入此中间件并将其注入到我们想要使用
JWT
保护的路由组中:// src/routes-private.js import Router from 'koa-router' import home from './modules/private/home' import authenticate from './middlewares/authenticate' const router = new Router({ prefix: '/private' }) const modules = [home] for (var module of modules) { router.use(authenticate, module.routes(), module.allowedMethods()) } export default router
在这个 API
中,我们想要保护所有位于 /private
路由下的路由。因此,我们将在此文件中导入任何我们想要保护的路由,例如上面的 /home
路由。因此,当你使用 /private/home
请求此路由时,你必须在请求标头中包含令牌才能访问此路由。
就是这样。你已经成功地在服务器端创建并验证了 JWT
。接下来,我们将在下一节中了解如何在客户端使用 Nuxt
完成 JWT
身份验证。让我们开始吧!