十二、创建用户登录和 API 身份验证

在最后两章中,我们开始研究 Nuxt 应用中的会话和JSON Web 令牌JWT)身份验证。我们在第 10 章中使用了用于身份验证的会话,添加了 Vuex 存储来练习nuxtServerInit。然后在第 11 章编写路由中间件和服务器中间件中,我们将会话和令牌一起用于身份验证,以练习每路由中间件,例如:

// store/index.js
nuxtServerInit({ commit }, { req }) {
  if (req.ctx.session && req.ctx.session.authUser) {
    commit('setUser', req.ctx.session.authUser)
  }
}

// middleware/token.js
export default async ({ store, error }) => {
  if (!store.state.auth.token) {
    // handle error
  }
  axios.defaults.headers.common['Authorization'] = Bearer: ${store.state.auth.token}
}

如果您对 web 身份验证还不熟悉,那么它们可能会让您不知所措,但您不必担心。简而言之,身份验证是验证您是谁的过程。身份验证系统允许您在凭据与数据库或数据身份验证服务器中的凭据匹配时访问资源。有几种身份验证方法。基于会话和基于令牌的身份验证是最常见的,或者是这两种身份验证的组合。那么,让我们深入了解一下。

我们将在本章中介绍的主题如下:

  • 理解基于会话的身份验证
  • 理解基于令牌的身份验证
  • 创建后端身份验证
  • 创建前端身份验证
  • 使用 Google OAuth 登录

理解基于会话的身份验证

超文本传输协议HTTP是无状态的。因此,所有 HTTP 请求都是无状态的。这意味着它不记得我们已经验证过的任何东西或任何用户,并且我们的应用也不知道它是否是前一个请求中的同一个人。因此,我们必须在下一次请求时再次进行身份验证。这并不理想。

因此,引入了基于会话和基于 cookie 的身份验证(通常仅称为基于会话的身份验证)来存储 HTTP 请求之间的用户数据,以消除 HTTP 请求的无状态性。它们使认证过程“有状态”,这意味着认证记录或会话存储在服务器端和客户端。服务器可以将活动会话保存在数据库或服务器内存中,因此称为基于会话的身份验证。客户端可以创建一个 cookie 来保存会话标识符(会话 ID),因此它被称为基于 cookie 的身份验证。

但是会话和 cookies 到底是什么呢?让我们在下面几节中开始讨论它们。

什么是会话和 cookies?

会话是两个或多个通信设备之间或计算机与用户之间交换的临时信息。它在某个时间建立,然后在未来某个时间到期。当用户关闭浏览器或离开网站时,它也会过期。建立会话时,将在服务器上的临时目录(或数据库或服务器内存)中创建一个文件,以存储注册的会话值。然后,在访问期间,这些数据在整个网站上都可用,浏览器会收到一个会话 ID,该 ID 将通过 cookie 或GET变量发送回服务器进行验证。

简而言之,cookie 和会话只是数据。Cookie 仅存储在客户端计算机上,而会话存储在客户端和服务器上。会话被认为比 cookie 更安全,因为数据可以单独保存在服务器上。Cookie 通常在会话建立时创建,并保存在客户端计算机上。它们可以是经过身份验证的用户的名称、年龄或 ID,并由浏览器发送回服务器以识别用户。让我们来看看他们在下一节中如何使用一个示例流程。

会话身份验证流

在以下示例身份验证流中可以理解基于会话和基于 cookie 的身份验证:

  1. 用户将其凭据(例如用户名和密码)从浏览器上的客户端应用发送到服务器。
  2. 服务器检查凭据并向客户端发送唯一令牌(会话 ID)。此外,此令牌将保存在服务器端的数据库或内存中。
  3. 客户端应用将令牌存储在客户端的 cookies 中,并将在每个 HTTP 请求中使用它,然后将其发送回服务器。
  4. 然后,应用接收请求的令牌并将其返回给服务器进行身份验证。
  5. 当用户注销时,客户端应用将销毁令牌。在注销之前,客户端还可以向服务器发送删除会话的请求,或者会话将根据设置的过期时间自行结束。

在基于会话的身份验证中,服务器承担所有繁重的工作。它是有状态的。它将会话标识符与用户帐户关联(例如,在数据库中)。基于会话的身份验证的缺点是,当大量用户同时使用系统时,由于会话存储在服务器的内存中,因此它涉及大量内存使用,因此具有可扩展性。此外,Cookie 在单个域或子域上运行良好,但通常在跨域共享(跨源资源共享)时被浏览器禁用。因此,当发出从不同域提供服务的 API 请求时,这会给客户端带来问题。但是这个问题可以通过基于令牌的身份验证来解决,我们将在下一节中介绍。

理解基于令牌的身份验证

基于令牌的身份验证更简单。有一些令牌的实现,但是,JSON Web 令牌是最常见的一种。基于令牌的身份验证是无状态的。这意味着不会在服务器端保留任何会话,因为状态存储在客户端的令牌中。服务器的职责只是创建一个带有秘密的 JWT 并将其发送给客户端。客户端将 JWT 存储在本地存储器或客户端 cookie 中,并在发出请求时将其包含在头中。然后服务器验证 JWT 并发送响应。

但是什么是 JWT,它是如何工作的呢?让我们在下一节中找到答案。

什么是 JSON Web 标记?

要了解 JWT 是如何工作的,我们首先应该了解它是什么。简而言之,JWT 是由头、负载和签名组成的哈希 JSON 对象字符串。JWT 以以下格式生成:

header.payload.signature

标题通常由两部分组成:类型和算法。类型为 JWT,算法可以是 HMAC、SHA256 或 RSA,这是一种使用密钥对令牌签名的哈希算法,例如:

{
  "typ": "JWT",
  "alg": "HS256"
}

有效载荷是信息(或索赔)存储在 JWT 内的部分,例如:

{
  "userId": "b08f86af-35da-48f2-8fab-cef3904660bd",
  "name": "Jane Doe"
}

在本例中,我们在有效负载中只包含两个声明。你可以提出你喜欢的任何要求。您包含的声明越多,JWT 的大小就越大,这可能会影响性能。还有其他可选权利要求,例如iss(发行人)、sub(主题)和exp(到期时间)。

If you want to find out more details about the JWT standard fields, please visit https://tools.ietf.org/html/rfc7519.

签名是使用编码的报头、编码的有效载荷、秘密和报头中指定的算法计算的。无论您在头部分中选择什么算法,都必须使用该算法加密 JWT:base64(header) + '.' + base64(payload)的前两部分,例如,在此伪代码中:

// signature algorithm
data = base64urlEncode(header) + '.' + base64urlEncode(payload)
hashedData = hash(data, secret)
signature = base64urlEncode(hashedData)

签名是 JWT 中唯一不公开可读的部分,因为它是用密钥加密的。除非有人拥有密钥,否则他们无法解密此信息。因此,前面伪代码的示例输出是三个由点分隔的 Base64 URL 字符串,可以在 HTTP 请求中轻松传递:

// JWT Token
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM

让我们来看看这个令牌验证在下一节中如何工作,以及一个示例流。

令牌身份验证流

基于令牌的身份验证可以通过以下示例身份验证流来理解:

  1. 用户将其凭据(例如用户名和密码)从浏览器上的客户端应用发送到服务器。
  2. 服务器检查用户名和密码,如果凭据正确,则返回签名令牌(JWT)。
  3. 此令牌存储在客户端。它可以存储在本地存储、会话存储或 cookie 中。
  4. 客户端应用通常将此令牌作为附加头包含在服务器的任何后续请求中。

  5. 服务器接收并解码 JWT,然后在令牌有效的情况下允许请求访问。

  6. 当用户注销时,令牌在客户端被销毁,不需要与服务器进一步交互。

通常情况下,您不应该在有效负载中包含敏感信息,也不应该在有效负载中保留很长时间。用于包含令牌的附加标头应采用以下格式:

Authorization: Bearer <token>

基于令牌的身份验证中的可伸缩性不是问题,因为令牌存储在客户端。跨域共享也不是问题,因为 JWT 是一个包含所有必要信息的字符串,包含在请求头中,在客户端向服务器发出的每个请求中都会检查这些信息。在 Node.js 应用中,我们可以使用 Node.js 模块之一,例如jsonwebtoken,为我们生成令牌。让我们来看看下一节中如何使用这个 No.js 模块。

为 JWT 使用 Node.js 模块

如前所述,jsonwebtoken可用于在服务器端生成 JWT。您可以在以下简化步骤中同步或异步使用此模块:

  1. 通过 npm 安装jsonwebtoken
$ npm i jsonwebtoken
  1. 在服务器端导入并签署令牌:
import jwt from 'jsonwebtoken'
var token = jwt.sign({ name: 'john' }, 'secret', { expiresIn: '1h' })
  1. 异步验证来自客户端的令牌:
try {
  var verified = jwt.verify(token, 'secret')
} catch(err) {
  // handle error
}

If you want to find out more information about this module, please visit https://github.com/brianloveswords/node-jws.

现在,您已经基本了解基于会话和基于令牌的身份验证,我们将指导您如何将它们应用于使用膝关节炎和 NUXT 的服务器端和客户端应用。在本章中,我们将使用基于令牌的身份验证在我们的应用中创建两个身份验证选项:本地身份验证和 Google OAuth 身份验证。本地身份验证是我们在应用内部和本地对用户进行身份验证的选项,而 Google OAuth 身份验证是我们使用 Google OAuth 对用户进行身份验证的选项。那么,让我们在接下来的部分中了解一下!

创建后端身份验证

第 10 章添加 Vuex 商店第 11 章编写路由中间件和服务器中间件的先前练习中,我们使用了一个虚拟用户进行后端身份验证,特别是在/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 })
    //...
  }
})

但在本章中,我们将使用一个数据库和一些用户数据进行身份验证。另外,在第 9 章添加了一个服务器端数据库,我们使用 MongoDB 作为我们的数据库服务器。但这一次,为了多样性,让我们尝试一种不同的数据库系统——MySQL。那么,让我们开始吧。

使用 MySQL 作为服务器数据库

确保在本地计算机上安装了 MySQL 服务器。在撰写本书时,最新的 MySQL 版本是 5.7。根据您使用的操作系统,您可以在中找到您系统的具体指南 https://dev.mysql.com/doc/mysql-installation-excerpt/5.7/en/installing.html 。如果您使用的是 Linux,您可以在上找到 Linux 发行版的安装指南 https://dev.mysql.com/doc/mysql-installation-excerpt/5.7/en/linux-installation.html 。如果您在 Linux Ubuntu 上并使用 APT 存储库,您可以按照上的指南进行操作 https://dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/apt-repo-fresh-install

或者,您可以安装 MariaDB 服务器而不是 MySQL 服务器,以便在项目中使用关系数据库管理系统DBMS。同样,根据您使用的操作系统,您可以在中找到您系统的具体指南 https://mariadb.com/downloads/ 。如果您正在使用 Linux,您可以在上找到特定 Linux 发行版的指南 https://downloads.mariadb.org/mariadb/repositories/ 。如果您使用的是 Linux Ubuntu19.10,您可以在上按照指南进行操作 https://downloads.mariadb.org/mariadb/repositories/#distro=Ubuntu &发行版=eoan——ubuntu\U eoan&镜像=bme&版本=10.4

无论您选择哪一种,都可以使用管理工具从浏览器管理 MySQL 数据库。您可以使用 phpMyAdmin 或 Adminer(https://www.adminer.org/latest.php ;两者都需要在您的计算机上安装 PHP。如果您是 PHP 新手,可以使用第 16 章中的安装指南,为 Nuxt创建一个框架无关的 PHP API。在本书中,管理员是首选。您可以在下载该程序 https://www.phpmyadmin.net/downloads/ 。如果您想使用 phpMyAdmin,请访问https://www.phpmyadmin.net/ 了解更多。在整个管理过程中,我们将根据您的需要尽快采取以下步骤:

  1. 使用 Adminer 创建数据库,例如“nuxt auth”。

  2. 在数据库中插入下表和示例数据:

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且被 b 改写为$2a$10$pyMYtPfIvE.PAboF3cIx9.IsyW73voMIRxFINohzgeV0I2BxwnrEu。我们将安装并使用bcryptjsNode.js 模块在服务器端对该密码进行哈希和验证。但是在跳到 AutoT3 之前,让我们看看下一节将要创建的应用的结构。

You can find a copy of the database we have exported as nuxt-auth.sql in /chapter-12/ in our GitHub repository.

构造跨域应用目录

我们一直在为单个域制作 Nuxt 应用。我们的服务器端 API 已经与 Nuxt 紧密耦合,因为 Oracle T5 章第 8 章 AutoT6T,OutT7 增加了服务器端框架 To8 T8,其中我们使用 Koa 作为服务器端框架和 API 来处理和服务 NUXT 应用的数据。如果您回顾一下我们的 GitHub 存储库中的/chapter-8/nuxt-universal/koa-nuxt/,您应该记得我们将服务器端程序和文件保存在/server/目录中。我们还将包/模块依赖项保存在一个package.json文件中,并将它们安装在同一/node_modules/目录中。当我们的应用越来越大,将两个框架(Nuxt 和 Koa)的模块依赖项混合在同一个package.json文件中时,最终可能会令人困惑。这也会使调试过程更加困难。因此,将我们的 NUXT 和膝关节炎(或任何其他服务器端框架,如 Express)组成的单个应用分成两个单独的应用,可能对可扩展性和维护性更好。现在,是制作跨域 Nuxt 应用的时候了。我们将重用和重组我们的 Nuxt 应用,从第 8 章添加服务器端框架。让我们称我们的 Nuxt 应用前端应用和膝关节炎应用的后端应用。我们将在这两个应用中分别添加新模块。

后端应用将执行后端身份验证,而前端应用将单独执行前端身份验证,但它们最终将合二为一。为了使学习和重组过程更容易,我们将仅将 JWT 用于身份验证。因此,让我们通过以下步骤创建新的工作目录:

  1. 创建一个项目目录,并将其命名为任意名称,其中包含两个子目录。一个称为frontend,另一个称为backend,如下所示:
<project-name>
├── frontend
└── backend
  1. 使用脚手架工具create-nuxt-app/frontend/目录中安装 Nuxt 应用,以便获得您已经熟悉的 Nuxt 目录,如下所示:
frontend
├── package.json
├── nuxt.config.js
├── store
│ ├── index.js
│ └── ...
└── pages
    ├── index.vue
    └── ...
  1. /backend/目录中创建一个package.json文件、一个backpack.config.js文件、一个/static/文件夹和一个/src/文件夹,然后在/src/文件夹中创建其他文件和子文件夹(我们将在下一节详细介绍),如下所示:
backend
├── package.json
├── backpack.config.js
├── assets
│ └── ...
├── static
│ └── ...
└── src
    ├── index.js
    ├── ...
    ├── modules
    │ └── ...
    └── core
        └── ...

后端目录是我们的 API 所在,可以使用 Express 或膝关节炎来制作。我们仍然会使用你熟悉的膝关节炎。我们将在此目录中安装服务器端依赖项,例如mysqlbcryptjsjsonwebtoken,这样它们就不会与 Nuxt 应用的前端模块混淆。

如您所见,在这个新结构中,我们成功地将 API 与 Nuxt 应用完全分离和解耦。这对调试和开发有好处。从技术上讲,我们现在将一次开发和测试一个应用。在一个环境中开发两个应用可能会令人困惑,而且当应用变得更大时,很难进行协作——正如我们前面提到的。

在研究如何在服务器端使用 JWT 之前,让我们先在下一节中更深入地了解一下如何构造/src/目录中的 API 路由和模块。

创建 API 公用/专用路由及其模块

注意,不是必须遵循本书中建议的目录结构。对于我们应该如何使用 Koa 来构造我们的应用,没有任意的或官方的规则。膝关节炎社区有一些骨架、样板和框架,您可以通过访问 OutT3 来查看。https://github.com/koajs/koa/wiki 。现在让我们更仔细地看一下目录中的目录结构,我们将在下面的步骤中开发我们的 API 源代码:

  1. 创建以下文件夹并清空/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,我们想用 Kaoapp.use方法注册,/modules/目录是存放所有 API 端点组的地方,比如homeuserlogin

  1. 创建两个主目录privatepublic,每个目录中都有子目录,如下所示:
└── modules
    ├── private
    │ └── home
    └── public
        ├── home
        ├── user
        └── login

/public/目录用于无 JWT 的公共访问,如登录路由,/private/目录用于需要 JWT 保护模块的访问。如您所见,我们将 API 路由分为两个主要组,因此/private/组将在routes-private.js中处理,/public/组将在routes-public.js中处理。我们有/config/目录来保存所有配置文件,/core/目录来保存可在整个应用中共享和使用的抽象程序或模块,例如您将在本章后面发现的 mysql 连接池。因此,从前面的目录树中,我们将在 API 中使用这些公共模块:homeuserlogin和一个私有模块:home

  1. 在每个模块中,例如,在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())
}

设置为prefix键的/users值是该用户模块的模块路由。在每个导入的子路由中,我们开发代码,例如登录路由的代码。

  1. 在每个模块的每个.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) => {
  // code goes here....
})
export default router
  1. 让我们创建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
  1. home模块到home模块只有一条路由,但我们仍然需要将该路由组装到该模块的index.js文件中,以便我们的代码与其他模块保持一致,如下所示:
// src/modules/public/home/index.js
import Router from 'koa-router'
import index from './_routes'

const router = new Router() // no prefix
const routes = [index]

for (var route of routes) {
  router.use(route.routes(), route.allowedMethods())
}
export default router

Note that there is no prefix added to this home module, so we can access its only route directly at localhost:4000/public.

  1. /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模块。我们将在接下来的章节中创建userlogin模块。导入这些模块后,我们应该将它们的路由注册到路由,然后导出路由。请注意,这些路由中添加了前缀/public。另外,请注意,每个路由都是循环的,并使用普通 JavaScriptfor循环函数注册到路由。

  1. /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 模块。

  1. 通过背包配置文件将以下附加文件路径(./src./src/core./src/modules添加到网页包配置中:
// backpack.config.js
module.exports = {
  webpack: (config, options, webpack) => {
    config.resolve.modules = ['./src', './src/core',
      './src/modules']
    return config
  }
}

我们可以使用【T0 的附加路径】来导入以下路径:

import pool from '../../../../core/database/mysql'

For more information about resolving modules by using the modules option in webpack, please visit https://webpack.js.org/configuration/resolve/#resolvemodules.

  1. 现在,我们应该在我们的项目中安装 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

You should be familiar with these modules as you have learned about them and installed them in Chapter 8, Adding a Server-Side Framework, which you can revisit in /chapter-8/nuxt-universal/koa-nuxt/ in our GitHub repository, and also, in Chapter 10, Adding a Vuex Store, in /chapter-10/nuxt-universal/nuxtServerInit/, and Chapter 11, Writing Route Middlewares and Server Middlewares, in /chapter-11/nuxt-universal/route-middleware/per-route/.

  1. /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 提供服务。

  1. 将以下服务器配置添加到/config/目录下的index.js文件中:
// src/config/index.js
export default {
  server: {
    port: 4000
  },
}

此配置文件只有一个非常简单的配置,即服务器,配置为在端口4000上运行。

  1. 导入您刚刚安装的以下模块,并将其作为中间件注册到/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())
}
  1. /middlewares/目录中创建一个处理具有200HTTP 状态的 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!"}}
  1. 创建一个处理 HTTP 错误状态的中间件,例如400404500
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."}
  1. 创建一个中间件,专门通过抛出'Not found'消息来处理 HTTP 404 响应:
// 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"}
  1. 将这三个中间件导入到 OT0 中,并将它们登记到膝关节炎的实例中,就像其他中间件一样:
// src/middlewares.js
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)
}

注意我们如何按顺序排列这些中间件——即使在 To.T0-中间件首先登记时,如果 HTTP 响应中存在错误,它将在膝关节炎的上游级联中重新执行。如果 HTTP 响应状态为200,上游级联将在okOutput中间件停止。另外,请注意,这些中间件必须在staticfaviconbodyparser中间件之后注册,这些中间件必须首先在下游级联中公开调用和服务。

  1. routes-public.jsroutes-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())
}
  1. 导入膝关节炎膝关节炎,从 MyTo0tALE 文件中的所有中间件,以及在 ORYT2 目录中的 ORDT1 文件中的服务器配置,实例化膝关节炎实例并将其传递给 AUTYT3AY 文件,然后用这个 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)
  1. 使用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 和资产在localhost:4000上得到了正确的服务–如果您将它们中的任何一个放在/static/img/`目录中,例如:

localhost:4000/sample-asset.jpg
localhost:4000/favicon.ico

您可以在localhost:4000的这两个目录中看到您的文件。这是因为当 Koa 下游级联发生时,中间件 T1 和 T2 T2 中间件安装并登记在中间件栈中执行。

做得好!现在您已经准备好了新的工作目录,并且运行了一个基本的 API,就像第 8 章中添加的服务器端框架一样。接下来,您需要在/backend/目录中安装其他服务器端依赖项,并开始向公共userlogin模块以及私有home模块中的路由添加代码。让我们从下一节的bcryptjs开始。

You can find the example app with the preceding structure in /chapter-12/nuxt-universal/cross-domain/jwt/axios-module/backend/ in our GitHub repository.

为 Node.js 使用 bcryptjs 模块

如前所述,bcryptjs用于散列和验证密码。有关如何在我们的应用中使用此模块的进一步建议,请查看简化步骤:

  1. 通过 npm 安装 bcryptjs 模块:
$ npm i bcryptjs
  1. 通过在请求正文(请求)中使用客户端发送的密码添加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)

Note that to speed up our authentication lesson in the chapter, we skip the process of creating a new user. But in a more complete CRUD, you can use this step to hash the password provided by the user.

  1. 例如,在login模块的登录验证过程中,通过比较从客户端(请求)发送的密码与数据库中存储的密码来验证密码,如下所示:
// src/modules/public/login/_routes/local.js
import bcrypt from 'bcryptjs'

const isMatched = bcrypt.compareSync(request.password,
  user.password)
if (isMatched === false) { ... }

Note that you can find out how this step is applied in our backend app in /chapter-12/nuxt-universal/cross-domain/jwt/axios-module/backend/src/modules/public/login/_routes/local.js in our GitHub repository.

在接下来的部分中,我们将向您展示如何使用bcryptjs验证来自客户端的传入密码。但是在散列和验证来自客户端的密码之前,首先,我们需要连接到 MySQL 数据库,以确定是注入新用户还是查询现有用户。为此,我们需要应用中的下一个 Node.js 模块:mysql——一个 mysql 客户端。因此,让我们转到下一节,看看如何安装和使用它。

If you want to find more information about this module and some asynchronous examples, please visit https://github.com/dcodeIO/bcrypt.js.

使用 Node.js 的 mysql 模块

我们在上一节中安装了 MySQL 服务器。现在我们需要一个 MySQL 客户端,它可以连接到 MySQL 服务器并从服务器端程序执行 SQL 查询。mysql 是实现 mysql 协议的标准 mysql Node.js 模块,因此我们可以使用此模块处理 mysql 连接和 SQL 查询,无论您是在 mysql 服务器上还是在 MariaDB 服务器上。那么,让我们从以下步骤开始:

  1. 通过 npm 安装mysql模块:
$ npm i mysql
  1. mysql.js文件中创建 MySQL 连接实例,在/src/目录的子目录中创建 MySQL 连接详细信息,如下所示:
// src/core/database/mysql.js
import util from 'util'
import mysql from 'mysql'

const pool = mysql.createPool({
  connectionLimit: 10,
  host : 'localhost',
  user : '<username>',
  password : '<password>',
  database : '<database>'
})

pool.getConnection((err, connection) => {
  if (error) {
    // Handle errors ...
  }
  // Release the connection to the pool if no error.
  if (connection) {
    connection.release()
  }
  return
})
pool.query = util.promisify(pool.query)
export default pool

让我们看一下我们刚刚在以下注释中创建的代码:

  • mysql 不支持async/await,所以我们使用 Node.js 中的promisify实用程序包装了 mysql 的pool.querypool.query是 mysql 中处理我们的 SQL 查询的函数,它在回调中返回结果,例如:
connection.query('SELECT ...', function (error, results, fields) {
  if (error) {
    throw error
  }
  // Do something ...
})

通过 promisify 实用程序,我们消除了回调,现在我们可以使用async/await如下:

let result = null
try {
  result = await pool.query('SELECT ...')
} catch (error) {
  // Handle errors ...
}
  • pool.querypool.getConnectionconnection.queryconnection.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://github.com/mysqljs/mysqlpooling-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) { ... }

  ctx.type = 'json'
  ctx.body = users
})

export default router

您可以看到,我们使用与上一节相同的代码结构,通过 MySQL 连接池将请求发送到 MySQL 服务器。在我们发送的查询中,我们告诉 MySQL 服务器只返回结果中users表中的idnamecreated_on字段。

**4. 如果您在localhost:4000/public/users访问用户路线,您应该在屏幕上获得以下输出:

{"status":200,"data":[{"id":1,"name":"Alexandre","created_on":"2019-06-16T22:00:00.000Z"}]}

现在我们有了用于连接 mysql 服务器和数据库的 mysql 模块,以及用于散列和验证客户端密码的 bcryptjs 模块,因此我们可以重构和改进我们在前一章中大致创建的登录代码。让我们在下一节中了解如何进行。

If you want to find out more information about the mysql module, please visit https://github.com/mysqljs/mysql.

重构服务器端的登录代码

我们已经收集了前面章节中的所有基本要素,一旦我们创建了 MySQL 连接池,我们就可以重构和改进我们的登录代码,从第 10 章添加 Vuex 商店第 11 章编写路由中间件和服务器中间件,在以下步骤中:

  1. 导入登录路由的所有依赖项,如koa-routerjsonwebtokenbcryptjs和 MySQL 连接池,如下所示:
// src/modules/public/login/_routes/local.js
import Router from 'koa-router'
import jwt from 'jsonwebtoken'
import bcrypt from 'bcrypt'
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

我们在这里为 API 的配置选项导入了配置文件,其中包含 MySQL 数据库连接详细信息、服务器和静态目录的选项,以及稍后签名令牌所需的 JWT 密码。

  1. 验证登录路径的post方法内的用户输入,以确保它们已定义且不为空:
if (request.username === undefined) {
  ctx.throw(400, 'username param is required.')
}
if (request.password === undefined) {
  ctx.throw(400, 'password param is required.')
}
if (request.username === '') {
  ctx.throw(400, 'username is required.')
}
if (request.password === '') {
  ctx.throw(400, 'password is required.')
}
  1. 为通过验证时查询数据库的变量分配用户名和密码:
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, 'no user found')
}
  1. 如果 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, 'invalid password')
}
  1. 如果用户通过了前面的所有步骤和验证,请签署 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: 'logged in ok',
  token: token
}
  1. 使用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":"thiamkok.lau@gmail.com"},"message":"logged in ok","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQWxleGFuZHJlIiwiZW1haWwiOiJ0aGlhbWtvay5sYXVAZ21haWwuY29tIiwiaWF0IjoxNTgwMDExNzAwLCJleHAiOjE1ODAwMTE3NjB9.Lhd78jokSGALup6DUYAqWAjl7C-8dLhXjEba-KAxy4k"}}

当然,只要成功签名,您就会在前面的响应中获得不同的令牌。现在您已经成功地重构并改进了登录代码。接下来,我们将在下一节中查看如何验证前面的令牌,该令牌将在请求头中从客户端发回。所以,继续阅读!

在服务器端验证传入令牌

我们已成功签署令牌,并在凭据与数据库中存储的内容匹配时将其返回给客户端。但这只是故事的一半。我们应该在客户端每次使用该令牌发出请求时验证该令牌,以访问服务器端中间件保护的所有受保护路由。

因此,让我们按照以下步骤创建中间件和受保护路由:

  1. /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, 'Protected resource, use Authorization header 
    to get access')
  }
  const token = ctx.headers.authorization.split(' ')[1]

  try {
    ctx.state.jwtPayload = jwt.verify(token, config.JWT_SECRET)
  } catch (err) {
    // handle error.
  }
  await next()
}

if条件!ctx.headers.authorization用于确保客户端已将令牌包含在请求头中。由于authorization的值格式为Bearer: [token],其中只有一个空格,因此我们将该值除以该空格,只取[token]trycatch块中进行验证。如果令牌有效,那么我们使用await next()让请求通过下一个路由。

  1. 导入此中间件并将其注入到我们希望使用 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())
}

在这个 API 中,我们希望保护属于/private路由的所有路由。因此,我们将在该文件中导入我们想要保护的任何路由,例如,前面的/home路由。因此,当您使用/private/home请求此路由时,您必须在请求到标头中包含令牌才能访问此路由。

就这样。您已经在服务器端创建并验证了 JWT。接下来,我们将在下一节中了解如何在客户端使用 Nuxt 完成 JWT 身份验证。我们走吧!

创建前端身份验证

您会发现这一部分很简单,也很熟悉,因为在前两章中,您已经使用虚拟后端身份验证构建了一些身份验证 Nuxt 应用。本章的不同之处在于,我们正在制作跨域应用,而不是像前两章那样制作单域应用。您可以在/chapter-10/nuxt-universal/nuxtServerInit//chapter-11/nuxt-universal/route-middleware/per-route/中重新访问这些单域 Nuxt 应用。

此外,我们将再次使用我们在第 6 章编写插件和模块@nuxtjs/axios@nuxtjs/proxy中已经介绍过的 Nuxt 模块。您可以在/chapter-6/nuxt-universal/module-snippets/top-level/中重新访问采用这两个模块的 Nuxt 应用。但是现在,让我们为这个 Nuxt 应用安装和配置它们,我们将从前面的章节中重构它,以便在以下步骤中创建客户端身份验证:

  1. 通过 npm 安装@nuxtjs/axios@nuxtjs/proxy
$ npm i @nuxtjs/axios
$ npm i @nuxtjs/proxy
  1. 在 Nuxt 配置文件中配置这两个模块,如下所示:
// nuxt.config.js
module.exports = {
  modules: [
    '@nuxtjs/axios',
  ],

  axios: {
    proxy: true
  },

  proxy: {
    '/api/': { target: 'http://localhost:4000/', pathRewrite:
     {'^/api/': ''} },
  }
}

由于我们知道在前面几节中创建的远程 API 服务器在localhost:4000上运行,因此在此配置中,我们将此 API 地址分配给proxy选项中的/api/键。

  1. 删除之前导入 axios Node.js 模块时使用的任何import语句;例如,在安全页面上:
// pages/secured.vue
import axios from '~/plugins/axios'

这是因为我们现在使用的是@nuxtjs/axios(Nuxt Axios 模块),我们不再需要在代码中直接导入 vanilla Axios Node.js 模块。

  1. 使用$axios调用 Nuxt Axios 模块,并替换axios(来自 vanilla Axios Node.js 模块),这是我们之前在 HTTP 请求代码中使用的;例如,在安全页面上:
// pages/secured.vue
async asyncData ({ $axios, redirect }) {
  const { data } = await $axios.$get('/api/private')
}

Nuxt Axios 模块通过步骤 2中的 Nuxt 配置文件加载到我们的 Nuxt 应用中,因此我们可以使用$axios从 Nuxt 上下文或this访问它。

我们还应该使用这两个 Nuxt 模块–@nuxtjs/axios@nuxtjs/proxy以及 cookies、Node.js 模块(客户端和服务器端)重构应用中商店和中间件中的其余代码。那么让我们在下面几节中讨论它。

在(Nuxt)客户端使用 cookie

在这个应用中,我们不再使用会话来“记住”经过身份验证的数据。相反,我们将使用js-cookieNode.js 模块创建 cookie 来存储来自远程服务器的数据。

使用 Node.js 模块创建一个 cookie 非常容易,它可以在整个站点上显示;例如:

  1. 使用以下格式设置 cookie:
Cookies.set(<name>, <value>)

如果要创建从现在起 30 天过期的 cookie,请使用以下代码:

Cookies.set(<name>, <value>, { expires: 30 })
  1. 使用以下格式读取 cookie:
Cookies.get(<name>)

您可以看到使用 Node.js 模块是多么容易–您只需要使用setget方法在客户端设置和检索 cookie。因此,让我们按照以下步骤重构存储中的代码:

  1. 仅在客户端处理我们的 Nuxt 应用时,使用if三元条件导入 js cookie Node.js 模块:
// store/actions.js
const cookies = process.client ? require('js-cookie') : undefined
  1. 使用 js cookie 中的set函数,将来自服务器的数据存储为login动作中的auth,如下所示:
// store/actions.js
export default {
  async login(context, { username, password }) {
    const { data } = await 
     this.$axios.$post('/api/public/login/local', 
     { username, password })
    cookies.set('auth', data)
    context.commit('setAuth', data)
  }
}
  1. 使用 js cookie 中的remove函数删除logout动作中的authcookie,如下所示:
// store/actions.js
export default {
  logout({ commit }) {
    cookies.remove('auth')
    commit('setAuth', null)
  }
}

这很简单,不是吗?但是,你可能会问:我们用这种饼干干什么?怎么用?让我们在下一节中了解如何在 Nuxt 服务器端使用 cookie。

For more information and code examples of the Node.js module, please visit https://github.com/js-cookie/js-cookie.

在(Nuxt)服务器端使用 cookie

由于我们使用 JWT 验证的数据已被js-cookie作为auth散列并存储在 cookie 中,因此我们需要随时读取和解析此 cookie。这就是 Node.js 模块cookie的用武之地。同样,我们在过去的章节中使用了 Node.js 模块,但我们没有讨论它。

cookie Node.js 模块是 HTTP 服务器的 HTTP cookie 解析器和序列化程序。它用于解析服务器端的 cookie 头。让我们来看看如何在下面的步骤中使用它。

  1. 仅在服务器端处理我们的 Nuxt app 时,使用if三元条件导入 cookie Node.js 模块:
// store/index.js
const cookie = process.server ? require('cookie') : undefined
  1. 使用 cookie Node.js 模块中的parse函数,在nuxtServerInit动作中解析 HTTP 请求头中的authcookie,如下所示:
// store/index.js
export const actions = {
  nuxtServerInit({ commit }, { req }) {
    if (req.headers.cookie && req.headers.cookie.indexOf('auth') >
      -1) {
      let auth = cookie.parse(req.headers.cookie)['auth']
      commit('setAuth', JSON.parse(auth))
    }
  }
}
  1. 通过$axios使用 Nuxt Axios 模块的setHeader功能,将令牌(JWT)包含在令牌中间件的 HTTP 头中,用于访问远程服务器上的私有 API 路由,如下所示:
// middleware/token.js
export default async ({ store, error, $axios }) => {
  if (!store.state.auth.token) {
    // handle error
  }
  $axios.setHeader('Authorization', Bearer: ${store.state.auth.token})
}
  1. 使用npm run dev运行 Nuxt 应用。您应该在localhost:3000的浏览器上运行应用。您可以在登录页面上使用凭据登录,然后访问受 JWT 保护的受限安全页面。

做得好!您已完成基于令牌的本地身份验证。您已经重构了应用商店和中间件中的代码,使js-cookiecookieNode.js 模块能够在 Nuxt 应用中的客户端和服务器端协同工作,实现完美的互补,以进行前端身份验证。此外,您还通过跨域方法将 Nuxt 应用与 API 解耦。

如您所见,使用js-cookiecookieNode.js 模块进行前端身份验证非常简单。但它也可以通过 GoogleOAuth 实现,我们将在下一节中研究。将 Google OAuth 添加到前端身份验证可以为用户提供一个额外的选项来登录您的应用。那么,让我们开始吧。

You can find the source code of this Nuxt app in /chapter-12/nuxt-universal/cross-domain/jwt/axios-module/frontend/ in our GitHub repository.

For more information and code examples of the cookie Node.js module, please visit https://github.com/jshttp/cookie.

For more information about helpers, such as the setHeader helper from the Nuxt Axios module, please visit https://axios.nuxtjs.org/helpers.

使用 Google OAuth 登录

OAuth 是一个开放的委托授权协议,它在网站或应用之间授予访问权限,而不向已被授予访问权限的各方公开用户密码。这是许多公司和网站使用的一种非常常见的访问授权,用于识别提供 OAuth 授权的 Google 和 Facebook 等方的用户。让我们让用户使用 Google OAuth 登录我们的应用。此选项需要来自 Google 开发者控制台的客户端 ID 和客户端密码。可通过以下步骤获得:

  1. 的 Google 开发者控制台中创建一个新项目 https://console.developers.google.com/

  2. 在 OAuth 同意屏幕选项卡上选择外部。

  3. 从凭据选项卡上的创建凭据下拉选项中选择 OAuth 客户端 ID,然后为应用类型选择 Web 应用。

  4. 在名称字段中提供 OAuth 客户端 ID 的名称,并在授权重定向 URI 字段中提供重定向 URI,以便 Google 在 Google 同意页面上进行身份验证后重定向用户。

  5. 启用 Google People API,该 API 可从“库”选项卡访问 API 库中的个人资料和联系人信息。

一旦您设置了一个开发者帐户,并获得了按照前面步骤创建的客户端 ID客户端机密,您就可以在下一节将 Google OAuth 添加到后端身份验证中了。让我们开始吧。

将 Google OAuth 添加到后端身份验证

要让我们将某人登录到谷歌,我们需要将他们发送到谷歌登录页面。从那里,他们将登录到他们的帐户,并将被重定向到我们的应用和他们的谷歌登录详细信息,我们将从中提取谷歌代码,并将其发送回谷歌,以获取我们可以在我们的应用中使用的用户数据。这个过程需要googleapisNode.js 模块,这是一个使用 Google API 的客户端库。

让我们通过以下步骤在代码中安装并采用它:

  1. 通过 npm 安装googleapisNode.js 模块:
$ npm i googleapis
  1. 使用您的凭据创建一个文件,以便 Google 知道是谁提出请求:
// backend/src/config/google.js
export default {
  clientId: '<client ID>',
  clientSecret: '<client secret>',
  redirect: 'http://localhost:3000/login'
}

注意:您必须用从 Google 开发者控制台获得的 ID 和密码替换前面的<client ID><client secret>值。另外,请注意,redirect选项中的 URL 必须与您的 Google 应用 API 设置中授权重定向 URI 中的重定向 URI 相匹配。

  1. 使用 Google OAuth 生成 Google 身份验证 URL,用于将用户发送到 Google 同意页面,以获得用户检索访问令牌的权限,如下所示:
// backend/src/modules/public/login/_routes/google/url.js
import Router from 'koa-router'
import { google } from 'googleapis'
import googleConfig from 'config/google'

const router = new Router()

router.get('/google/url', async (ctx, next) => {

  const oauth = new google.auth.OAuth2(
    googleConfig.clientId,
    googleConfig.clientSecret,
    googleConfig.redirect
  )

  const scopes = [
    'https://www.googleapis.com/auth/userinfo.email',
    'https://www.googleapis.com/auth/userinfo.profile',
  ]

  const url = oauth.generateAuthUrl({
    access_type: 'offline',
    prompt: 'consent',
    scope: scopes
  })

  ctx.body = url
})

作用域决定了当用户登录并生成 URL 时,我们希望用户提供哪些信息和权限。在本例中,我们希望获得检索用户电子邮件和个人资料信息的权限:userinfo.emailuserinfo.profile。在用户在谷歌同意页面上进行身份验证后,谷歌会将用户重定向回我们的应用,并提供一组经过身份验证的数据和访问用户数据的授权码。

  1. 从上一步返回的 URL 中 Google 附加的已验证数据中提取code参数中的值。我们将回到 Node.js 模块,在下一节中,它可以帮助我们从 URL 查询中提取code参数。现在,假设我们提取了code值,并将其发送到服务器端,以使用以下基本代码结构的 Google OAuth2 实例请求令牌:
// backend/src/modules/public/login/_routes/google/me.js
import Router from 'koa-router'
import { google } from 'googleapis'
import jwt from 'jsonwebtoken'
import pool from 'core/database/mysql'
import config from 'config'
import googleConfig from 'config/google'

const router = new Router()

router.get('/google/me', async (ctx, next) => {

  // Get the code from url query.
  const code = ctx.query.code

  // Create a new google oauth2 client instance.
  const oauth2 = new google.auth.OAuth2(
    googleConfig.clientId,
    googleConfig.clientSecret,
    googleConfig.redirect
  )
  //...
})
  1. 使用我们刚才提取的代码从 Google 获取代币,并传递给 Google People,google.people,使用get方法获取用户数据,并在personFields查询参数中指定需要返回哪些与该人相关的字段:
// backend/src/modules/public/login/_routes/google/me.js
...
const {tokens} = await oauth2.getToken(code)
oauth.setCredentials(tokens)

const people = google.people({
  version: 'v1',
  auth: oauth2,
})

const me = await people.people.get({
  resourceName: 'people/me',
  personFields: 'names,emailAddresses'
})

您可以看到,在前面的代码中,我们只需要两个与来自谷歌的人相关的字段,即namesemailAddresses。您可以通过从谷歌找到与您想要的人相关的其他字段 https://developers.google.com/people/api/rest/v1/people/get 。如果访问成功,我们应该从 Google 获得 JSON 格式的用户数据,然后我们可以从该数据中提取电子邮件,以确保下一步它将匹配数据库中的用户。

  1. 仅从 Google person 数据检索第一封电子邮件,并查询我们的数据库,以查看是否有任何用户已使用该电子邮件:
// backend/src/modules/public/login/_routes/google/me.js
...
let email = me.data.emailAddresses[0].value
let users = []

try {
  users = await pool.query('SELECT  FROM users WHERE email = ?',
   [email])
} catch(err) {
  ctx.throw(400, err.sqlMessage)
}
  1. 如果没有用户使用该电子邮件,则向客户端发送一条包含谷歌用户数据的'signup required'消息,并要求用户在我们的应用中注册一个帐户:
// backend/src/modules/public/login/_routes/google/me.js
...
if (users.length === 0) {
  ctx.body = {
    user: me.data,
    message: 'signup required'
  }
  return
}
let user = users[0]
  1. 如果存在匹配项,则使用有效负载和 JWT 机密对 JWT 进行签名,然后将令牌(JWT)发送到客户端:
// backend/src/modules/public/login/_routes/google/me.js
...
let payload = { name: user.name, email: user.email }
let token = jwt.sign(payload, config.JWT_SECRET, { expiresIn: 1 * 60 })

ctx.body = {
  user: payload,
  message: 'logged in ok',
  token: token
}

就这样。在前面的几个步骤中,您已经成功地在服务器端添加了 GoogleOAuth。接下来,在下一节中,我们应该看看如何在客户端为 GoogleOAuth 使用 Nuxt 完成身份验证。我们走吧。

For more information about the googleapis Node.js module, please visit https://github.com/googleapis/google-api-nodejs-client.

为 Google OAuth 创建前端身份验证

当 Google 将用户重定向回我们的应用时,我们将在重定向 URL 上获得一组数据,例如:

http://localhost:3000/login?code=4%2F1QGpS37E21TcgQhhIvJZlK1cG4M1jpPJ0I_XPQgrFjvKUFUJQ3aYuO1zYsqPmKgNb4Wfd8ito88yDjUTD6CKD3E&scope=email%20profile%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile%20openid&authuser=1&prompt=consent

当您第一次看到它时,很难读取和破译它,但它只是一个带有附加到重定向 URL 的参数的查询字符串:

<redirect URL>?
code=4/1QFvWYDSrW...
&scope=email profile...
&authuser=1
&prompt=consent

我们可以使用 Node.js 模块query-string解析 URL 中的查询字符串,例如:

const queryString = require('query-string')
const parsed = queryString.parse(location.search)
console.log(parsed)

然后,您将在浏览器控制台中获得以下 JavaScript 对象:

{authuser: "1", code: "4/1QFvWYDSrWLklhIgRfVR0LJy6Pk0gn5TkjTKWKlRr9pdZveGAHV_pMrxBhicy7Zd6d9nfz0IQrcLl-VGS-Gu9Xk", prompt: "consent", scope: "email profile https://www.googleapis.com/auth/user…//www.googleapis.com/auth/userinfo.profile openid"}

在前面的重定向 URL 中,code参数是我们最感兴趣的,因为我们需要将其发送到服务器端,正如您在上一节中所了解的那样,以便通过 googleapis Node.js 模块获取 Google 用户数据。那么,让我们通过以下步骤安装query-string并在我们的 Nuxt 应用中创建前端身份验证:

  1. 通过 npm 安装query-stringNode.js 模块:
$ npm i query-string
  1. 在登录页面上创建一个按钮,绑定一个名为loginWithGoogle的方法,在商店中调度getGoogleUrl方法,如下所示:
// frontend/pages/login.vue
<button v-on:click="loginWithGoogle">Google Login</button>

export default {
  methods: {
    async loginWithGoogle() {
      try {
        await this.$store.dispatch('getGoogleUrl')
      } catch (error) {
        let errorData = error.response.data
        this.formError = errorData.message
      }
    }
  }
}
  1. getGoogleUrl方法中调用 API 中的/api/public/login/google/url路由,如下所示:
// frontend/store/actions.js
export default {
  async getGoogleUrl(context) {
    const { data } = await this.$axios.$get('/api/public/login/
     google/url')
    window.location.replace(data)
  }
}

/api/public/login/google/url路由将返回一个谷歌 URL,然后我们可以使用它将用户重定向到谷歌登录页面。从那里,用户将决定哪个谷歌帐户登录,如果他们有一个以上。

  1. 从返回的 URL 中提取查询部分,当 Google 将用户重定向回登录页面时,将其发送到商店中的loginWithGoogle方法,如下所示:
// frontend/pages/login.vue
export default {
  async mounted () {
    let query = window.location.search

    if (query) {
      try {
        await this.$store.dispatch('loginWithGoogle', query)
      } catch (error) {
        // handle error
      }
    }
  }
}
  1. 使用query-string从前面查询部分的code参数中提取代码,并使用$axios将其发送给我们的 API/api/public/login/google/me,如下所示:
// frontend/store/actions.js
import queryString from 'query-string'

export default {
  async loginWithGoogle (context, query) {
    const parsed = queryString.parse(query)
    const { data } = await this.$axios.$get('/api/public/login/
     google/me', {
      params: {
        code: parsed.code
      }
    })

    if (data.message === 'signup required') {
      localStorage.setItem('user', JSON.stringify(data.user))
      this.$router.push({ name: 'signup'})
    } else {
      cookies.set('auth', data)
      context.commit('setAuth', data)
    }
  }
}

当我们从服务器收到'signup required'消息时,我们会将用户重定向到注册页面。但是如果我们使用 JWT 获取消息,那么我们可以将 cookie 和经过身份验证的数据设置为存储状态。我们将把注册页面留给您的想象和自己的努力,因为它是一种从用户那里收集数据并存储在数据库中的表单。

  1. 最后,使用npm run dev运行 Nuxt 应用。您应该在localhost:3000的浏览器上运行应用。您可以使用 Google 登录,然后访问受 JWT 保护的受限页面,就像本地身份验证一样。

好了,这就是使用 GoogleOAuthAPI 登录用户所采取的基本步骤。一点也不难,是吗?我们还可以使用 Nuxt Auth 模块实现与我们在这里实现的几乎相同的功能。使用此模块,您可以使用 Auth0、Facebook、GitHub、Laravel Passport 和 Google 登录用户。如果您正在寻找对 Nuxt 的快速、简单和零模板身份验证支持,那么它可能是您的项目的一个很好的选择。有关此 Nuxt 模块的更多信息,请访问https://auth.nuxtjs.org/ 。现在,让我们在下一节总结一下您在本章学到的知识。

You can find the preceding login option with Google OAuth in /chapter-12/nuxt-universal/cross-domain/jwt/axios-module/ in our GitHub repository.

For more information about the usage of the query-string Node.js module, visit https://www.npmjs.com/package/query-string.

总结

做得好!你已经走了这么远。毕竟,进行 web 身份验证并不困难。在本章中,您已经了解了什么是基于会话的身份验证和基于令牌的身份验证,特别是关于 JSON Web 令牌(JWT)。您现在应该知道它们与 JWT 的组成部分之间的区别,以及如何使用jsonwebtokenNode.js 模块生成 JWT。我们还介绍了 MySQL Node.js 模块,并将其用作身份验证系统的一部分。您还集成了 GoogleOAuth,用于登录用户,然后使用 Nuxt 创建前端身份验证。

在下一章中,您将学习如何在 Nuxt 应用中编写端到端测试。您将了解可以安装并用于编写端到端测试的工具,特别是 AVA 和 Nightwatch。除此之外,您还将学习如何使用 Node.js 模块,即jsdom,使您的端到端测试在服务器端成为可能。这是因为 Nuxt 在技术上是一种服务器端技术,在服务器端呈现 HTML 页面,但在服务器端没有 DOM,因此我们可以利用jsdom实现这一点。但是请放心,我们将指导您完成设置所有这些工具并编写测试的步骤。所以,请继续关注!****