对于刚开始使用 GraphQL 的开发人员来说,身份验证是最具挑战性的任务之一。 其中涉及许多技术考量,包括什么 ORM 易于设置、如何生成安全令牌和散列密码,甚至使用什么 HTTP 库以及如何使用它。
在本文中,我们将重点关注 **本地身份验证**。 这是现代网站处理身份验证最流行的方式之一,它通过请求用户的 **电子邮件** 和 **密码** 来实现(而不是使用 Google 身份验证)。
此外,本文使用 Apollo Server 2、JSON Web Tokens (JWT) 和 Sequelize ORM 使用 Node 构建身份验证 API。
处理身份验证
例如,登录系统
- **身份验证**用于识别或验证用户。
- **授权**用于验证已认证用户可以访问的路由(或应用程序的部分)。
实现此功能的流程如下:
- 用户使用密码和电子邮件进行注册
- 用户的凭据存储在数据库中
- 注册完成后,用户将重定向到登录页面
- 用户在经过身份验证后,可以访问特定的资源
- 用户的状态存储在浏览器的任何一种存储介质中(例如
localStorage
、cookie、session)或 JWT 中。
先决条件
在深入研究实现之前,以下是一些您需要遵循的事项。
- Node 6 或更高版本
- Yarn(推荐)或 NPM
- GraphQL Playground
- GraphQL 和 Node 的基本知识
- …一颗好奇心!
依赖项
这是一个很大的列表,让我们深入了解一下
- Apollo Server: 一个开源的 GraphQL 服务器,与任何类型的 GraphQL 客户端兼容。 在此项目中,我们不会使用 Express 作为我们的服务器。 相反,我们将利用 Apollo Server 的强大功能来公开我们的 GraphQL API。
- bcryptjs: 我们希望在数据库中散列用户密码。 因此我们将使用 bcrypt。 它依赖于 Web Crypto API 的
getRandomValues
接口来获取安全的随机数。 - dotenv: 我们将使用 dotenv 从我们的
.env
文件加载环境变量。 - jsonwebtoken: 用户登录后,每个后续请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。
jsonwebtoken
将用于生成 JWT,用于对用户进行身份验证。 - nodemon: 一个工具,通过在检测到目录更改时自动重启 Node 应用程序来帮助开发基于 Node 的应用程序。 我们不希望每次代码发生更改时都关闭并启动服务器。 Nodemon 每次检查应用程序中的更改并自动重启服务器。
- mysql2: Node 的 SQL 客户端。 我们需要它连接到我们的 SQL 服务器,以便我们可以运行迁移。
- sequelize: Sequelize 是一个基于 Promise 的 Node ORM,适用于 Postgres、MySQL、MariaDB、SQLite 和 Microsoft SQL Server。 我们将使用 Sequelize 自动生成迁移和模型。
- sequelize cli: 我们将使用 Sequelize CLI 运行 Sequelize 命令。 在终端中使用
yarn add --global sequelize-cli
全局安装它。
设置目录结构和开发环境
让我们创建一个全新的项目。 创建一个新文件夹,并在其中执行以下操作
yarn init -y
-y
标志表示我们选择对所有 yarn init
问题都选择“是”并使用默认值。
我们还应该在文件夹中放置一个 package.json
文件,因此让我们安装项目依赖项
yarn add apollo-server bcrpytjs dotenv jsonwebtoken nodemon sequelize sqlite3
接下来,让我们添加 Babeto 到我们的开发环境中
yarn add babel-cli babel-preset-env babel-preset-stage-0 --dev
现在,让我们配置 Babel。 在终端中运行 touch .babelrc
。 这将创建一个 Babel 配置文件并打开它,在其中我们将添加以下内容
{
"presets": ["env", "stage-0"]
}
如果我们的服务器启动并迁移数据,那就更好了。 我们可以通过使用以下内容更新 package.json
来实现自动化
"scripts": {
"migrate": " sequelize db:migrate",
"dev": "nodemon src/server --exec babel-node -e js",
"start": "node src/server",
"test": "echo \"Error: no test specified\" && exit 1"
},
这是我们此时完整的 package.json
文件
{
"name": "graphql-auth",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"migrate": " sequelize db:migrate",
"dev": "nodemon src/server --exec babel-node -e js",
"start": "node src/server",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"apollo-server": "^2.17.0",
"bcryptjs": "^2.4.3",
"dotenv": "^8.2.0",
"jsonwebtoken": "^8.5.1",
"nodemon": "^2.0.4",
"sequelize": "^6.3.5",
"sqlite3": "^5.0.0"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-stage-0": "^6.24.1"
}
}
现在我们的开发环境已设置好,让我们转向数据库,我们将在此存储数据。
数据库设置
我们将使用 MySQL 作为我们的数据库,并使用 Sequelize ORM 来处理我们的关系。 运行 sequelize init(假设您之前已全局安装它)。 该命令应该创建三个文件夹:/config
/models
和 /migrations
。 此时,我们的项目目录结构正在形成。
让我们配置我们的数据库。 首先,在项目根目录中创建一个 .env
文件并粘贴以下内容
NODE_ENV=development
DB_HOST=localhost
DB_USERNAME=
DB_PASSWORD=
DB_NAME=
然后转到我们刚刚创建的 /config
文件夹,并将其中的 config.json
文件重命名为 config.js
。 然后,将以下代码放入其中
require('dotenv').config()
const dbDetails = {
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
host: process.env.DB_HOST,
dialect: 'mysql'
}
module.exports = {
development: dbDetails,
production: dbDetails
}
在这里,我们读取在 .env
文件中设置的数据库详细信息。 process.env
是 Node 注入的一个全局变量,它用于表示系统环境的当前状态。
让我们使用适当的数据更新我们的数据库详细信息。 打开 SQL 数据库并创建一个名为 graphql_auth
的表。 我使用 Laragon 作为我的本地服务器,并使用 phpmyadmin
来管理数据库表。
无论您使用什么,我们都需要使用最新信息更新 .env
文件
NODE_ENV=development
DB_HOST=localhost
DB_USERNAME=graphql_auth
DB_PASSWORD=
DB_NAME=<your_db_username_here>
让我们配置 Sequelize。 在项目的根目录中创建一个 .sequelizerc
文件并粘贴以下内容
const path = require('path')
module.exports = {
config: path.resolve('config', 'config.js')
}
现在让我们将我们的配置集成到模型中。 转到 /models
文件夹中的 index.js
并编辑 config
变量。
const config = require(__dirname + '/../../config/config.js')[env]
最后,让我们编写我们的模型。 对于此项目,我们需要一个 User
模型。 让我们使用 Sequelize 自动生成模型。 以下是我们需要在终端中运行以设置它的内容
sequelize model:generate --name User --attributes username:string,email:string,password:string
让我们编辑为我们创建的模型。 转到 /models
文件夹中的 user.js
并粘贴以下内容
'use strict';
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
username: {
type: DataTypes.STRING,
},
email: {
type: DataTypes.STRING,
},
password: {
type: DataTypes.STRING,
}
}, {});
return User;
};
在这里,我们为用户名、电子邮件和密码创建了属性和字段。 让我们运行迁移以跟踪架构中的更改
yarn migrate
现在让我们编写模式和解析器。
将模式和解析器与 GraphQL 服务器集成
在本节中,我们将定义我们的模式,编写解析器函数并在我们的服务器上公开它们。
模式
在 src 文件夹中,创建一个名为 /schema
的新文件夹,并在其中创建一个名为 schema.js
的文件。 粘贴以下代码
const { gql } = require('apollo-server')
const typeDefs = gql`
type User {
id: Int!
username: String
email: String!
}
type AuthPayload {
token: String!
user: User!
}
type Query {
user(id: Int!): User
allUsers: [User!]!
me: User
}
type Mutation {
registerUser(username: String, email: String!, password: String!): AuthPayload!
login (email: String!, password: String!): AuthPayload!
}
`
module.exports = typeDefs
在这里,我们从 apollo-server 导入了 graphql-tag。 Apollo Server 要求使用 gql
包装我们的模式。
解析器
在 src
文件夹中,创建一个名为 /resolvers
的新文件夹,并在其中创建一个名为 resolver.js
的文件。 粘贴以下代码
const bcrypt = require('bcryptjs')
const jsonwebtoken = require('jsonwebtoken')
const models = require('../models')
require('dotenv').config()
const resolvers = {
Query: {
async me(_, args, { user }) {
if(!user) throw new Error('You are not authenticated')
return await models.User.findByPk(user.id)
},
async user(root, { id }, { user }) {
try {
if(!user) throw new Error('You are not authenticated!')
return models.User.findByPk(id)
} catch (error) {
throw new Error(error.message)
}
},
async allUsers(root, args, { user }) {
try {
if (!user) throw new Error('You are not authenticated!')
return models.User.findAll()
} catch (error) {
throw new Error(error.message)
}
}
},
Mutation: {
async registerUser(root, { username, email, password }) {
try {
const user = await models.User.create({
username,
email,
password: await bcrypt.hash(password, 10)
})
const token = jsonwebtoken.sign(
{ id: user.id, email: user.email},
process.env.JWT_SECRET,
{ expiresIn: '1y' }
)
return {
token, id: user.id, username: user.username, email: user.email, message: "Authentication succesfull"
}
} catch (error) {
throw new Error(error.message)
}
},
async login(_, { email, password }) {
try {
const user = await models.User.findOne({ where: { email }})
if (!user) {
throw new Error('No user with that email')
}
const isValid = await bcrypt.compare(password, user.password)
if (!isValid) {
throw new Error('Incorrect password')
}
// return jwt
const token = jsonwebtoken.sign(
{ id: user.id, email: user.email},
process.env.JWT_SECRET,
{ expiresIn: '1d'}
)
return {
token, user
}
} catch (error) {
throw new Error(error.message)
}
}
},
}
module.exports = resolvers
有很多代码,让我们看看那里发生了什么。
首先,我们导入了模型 `bcrypt` 和 `jsonwebtoken`,然后初始化了环境变量。
接下来是解析器函数。在查询解析器中,我们有三个函数(`me`、`user` 和 `allUsers`)。
- `me` 查询获取当前已登录用户的详细信息。它接受一个 `user` 对象作为上下文参数。**上下文** 用于提供对我们数据库的访问权限,该数据库用于根据查询中提供的 **参数** 中的 ID 加载用户数据。
- `user` 查询根据用户的 ID 获取用户的详细信息。它接受 `id` 作为上下文参数和一个 `user` 对象。
- `alluser` 查询返回所有用户的详细信息。
如果用户状态为 `loggedIn`,则 `user` 将是一个对象;如果用户未登录,则为 `null`。我们将在我们的变异中创建此用户。
在变异解析器中,我们有两个函数(`registerUser` 和 `loginUser`)。
- `registerUser` 接受用户的 `username`、`email` 和 `password`,并在我们的数据库中使用这些字段创建一个新行。需要注意的是,我们使用 bcryptjs 包通过 `bcrypt.hash(password, 10)` 对用户的密码进行哈希处理。 `jsonwebtoken.sign` 同步地将给定的负载签名到 JSON Web Token 字符串中(在本例中为用户 `id` 和 `email`)。最后,如果成功,`registerUser` 将返回 JWT 字符串和用户个人资料;如果出现错误,则返回错误消息。
- `login` 接受 `email` 和 `password`,并检查这些详细信息是否与提供的详细信息匹配。首先,我们检查 `email` 值是否已存在于用户数据库中的某个位置。
models.User.findOne({ where: { email }})
if (!user) {
throw new Error('No user with that email')
}
然后,我们使用 bcrypt 的 `bcrypt.compare` 方法来检查密码是否匹配。
const isValid = await bcrypt.compare(password, user.password)
if (!isValid) {
throw new Error('Incorrect password')
}
然后,就像我们之前在 `registerUser` 中所做的那样,我们使用 `jsonwebtoken.sign` 生成一个 JWT 字符串。`login` 变异返回令牌和 `user` 对象。
现在让我们将 `JWT_SECRET` 添加到我们的 `.env` 文件中。
JWT_SECRET=somereallylongsecret
服务器
最后,是服务器!在项目的根文件夹中创建一个 `server.js` 文件并粘贴以下内容
const { ApolloServer } = require('apollo-server')
const jwt = require('jsonwebtoken')
const typeDefs = require('./schema/schema')
const resolvers = require('./resolvers/resolvers')
require('dotenv').config()
const { JWT_SECRET, PORT } = process.env
const getUser = token => {
try {
if (token) {
return jwt.verify(token, JWT_SECRET)
}
return null
} catch (error) {
return null
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const token = req.get('Authorization') || ''
return { user: getUser(token.replace('Bearer', ''))}
},
introspection: true,
playground: true
})
server.listen({ port: process.env.PORT || 4000 }).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
在这里,我们导入 schema、解析器和 jwt,并初始化我们的环境变量。首先,我们使用 `verify` 验证 JWT 令牌。 `jwt.verify` 接受令牌和 JWT 密钥作为参数。
接下来,我们使用接受 `typeDefs` 和解析器的 `ApolloServer` 实例创建我们的服务器。
我们有一个服务器了!让我们在终端中运行 `yarn dev` 来启动它。
测试 API
现在让我们使用 GraphQL Playground 测试 GraphQL API。我们应该能够注册、登录和查看所有用户——包括单个用户——通过 ID。
我们将首先打开 GraphQL Playground 应用程序,或者直接在浏览器中打开 `localhost://4000` 来访问它。
注册用户的变异
mutation {
registerUser(username: "Wizzy", email: "[email protected]", password: "wizzyekpot" ){
token
}
}
我们应该得到类似这样的结果
{
"data": {
"registerUser": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsImVtYWlsIjoiZWtwb3RAZ21haWwuY29tIiwiaWF0IjoxNTk5MjQwMzAwLCJleHAiOjE2MzA3OTc5MDB9.gmeynGR9Zwng8cIJR75Qrob9bovnRQT242n6vfBt5PY"
}
}
}
登录的变异
现在让我们使用刚刚创建的用户详细信息登录
mutation {
login(email:"[email protected]" password:"wizzyekpot"){
token
}
}
我们应该得到类似这样的结果
{
"data": {
"login": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsImVtYWlsIjoiZWtwb3RAZ21haWwuY29tIiwiaWF0IjoxNTk5MjQwMzcwLCJleHAiOjE1OTkzMjY3NzB9.PDiBKyq58nWxlgTOQYzbtKJ-HkzxemVppLA5nBdm4nc"
}
}
}
太棒了!
单个用户的查询
为了查询单个用户,我们需要将用户令牌作为授权标头传递。转到 HTTP 标头选项卡。

…并粘贴以下内容
{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsImVtYWlsIjoiZWtwb3RAZ21haWwuY29tIiwiaWF0IjoxNTk5MjQwMzcwLCJleHAiOjE1OTkzMjY3NzB9.PDiBKyq58nWxlgTOQYzbtKJ-HkzxemVppLA5nBdm4nc"
}
这是查询
query myself{
me {
id
email
username
}
}
我们应该得到类似这样的结果
{
"data": {
"me": {
"id": 15,
"email": "[email protected]",
"username": "Wizzy"
}
}
}
太好了!现在让我们根据 ID 获取用户
query singleUser{
user(id:15){
id
email
username
}
}
这是获取所有用户的查询
{
allUsers{
id
username
email
}
}
总结
在构建需要身份验证的网站时,身份验证是最棘手的任务之一。GraphQL 使我们能够仅使用一个端点构建整个身份验证 API。Sequelize ORM 使得与我们的 SQL 数据库创建关系变得非常容易,我们几乎不必担心我们的模型。同样值得注意的是,我们不需要 HTTP 服务器库(如 Express)并使用 Apollo GraphQL 作为中间件。Apollo Server 2 现在使我们能够创建自己的库无关的 GraphQL 服务器!
在 GitHub 上查看本教程的源代码。
有很多代码异味
if(!user) throw new Error('You are not authenticated!')
,有没有更好的方法来做这个。