翻译 | 《JavaScript
Everywhere
》第7
章 用户帐户和身份验证(^_
^)
写在最后面
大家好呀,我是毛小悠,是一位前端开发工程师。正在翻译一本英文技术书籍。
为了进步大家的浏览体验,对语句的构造和内容略有调整。如果发现本文中有存在瑕疵的中央,或者你有任何意见或者倡议,能够在评论区留言,或者加我的微信:code\_maomao
,欢送互相沟通交流学习。
(゚∀゚)..
:\*☆哎哟不错哦
第7章 用户帐户和身份验证
想像一下本人走在光明的小巷里,你行将退出一个“超酷机密俱乐部”(如果你正在浏览此书,则是当之无愧的成员)。当你进入俱乐部的暗门时,接待员会向你打招呼,并递给你一张表格。在表格上,你必须填写你的姓名和明码,只有你和接待员能力晓得。填写完表格后,将其交给接待员,接待员将进入俱乐部的后厅。在后盾,接待员应用密钥对你的明码进行加密,而后将加密的明码存储在锁定的文件库中。而后,接待员会在通行币上盖章,而后盖下你的惟一会员ID
。回到前厅,接待员递给你通行币,你将通行币塞到口袋里。当初,每次你返回俱乐部时,只须要出示通行币即可进入。这种互动听起来像是一部高价特务电影中的货色,但它简直与每次咱们注册Web
应用程序时遵循的过程雷同。
在本章中,咱们将学习如何构建GraphQL
批改,该批改将容许用户创立一个帐户并登录到咱们的应用程序中。咱们还将学习如何加密用户的明码并向用户返回令牌,当他们与咱们的应用程序交互时,他们能够用来验证其身份。
应用程序认证流程
在开始之前,让咱们后退一步,确定用户注册帐户并登录到现有帐户时将遵循的流程。如果你还不理解这里介绍的所有概念,请不要放心:咱们将逐渐解决它们。
首先,让咱们回顾一下帐户创立流程:
- 用户将电子邮件地址、用户名和明码输出到用户界面(
UI
),例如GraphQL
Playground
,Web
应用程序或挪动应用程序。 - 用户界面会应用用户信息将
GraphQL
申请发送到咱们的服务器。 - 服务器对明码进行加密,并将用户信息存储在数据库中。
- 服务器向用户界面返回一个令牌,其中蕴含用户的
ID
。 UI
在指定的时间段内存储此令牌,并将其与每个申请一起发送到服务器以验证用户。
当初让咱们看一下用户登录流程:
- 用户在
UI
的字段中输出其电子邮件或用户名和明码。 UI
会应用此信息将GraphQL
申请发送到咱们的服务器。- 服务器解密存储在数据库中的明码,并将其与用户输出的明码进行比拟。
- 如果明码匹配,则服务器将向用户界面返回一个令牌,其中蕴含用户的
ID
。 UI
在指定的时间段内存储此令牌,并将其与每个申请一起发送到服务器。
如你所见,这些流程与咱们的“机密俱乐部”流程十分类似。在本章中,咱们将重点介绍实现这些交互的API
局部。
明码重设流程
你会留神到,咱们的应用程序不容许用户更改明码。咱们能够容许用户应用批改明码,然而首先通过电子邮件来验证重置申请更加平安。为简便起见,咱们不会在本书中实现明码重置性能,然而如果你对创立明码重置流程的示例和资源感兴趣,请拜访JavaScript
Everywhere
Spectrum
社区。
加密和令牌
在摸索用户身份验证流程时,我提到了加密和令牌。这些听起来像是神话中的光明艺术,所以让咱们花点工夫认真钻研一下每一项。
加密明码
为了无效地加密用户明码,咱们应该联合应用哈希和盐析。
哈希
是通过将文本字符串转换为看似随机的字符串来使其混同的行为。哈希函数是“一种形式”,这意味着一旦对文本进行哈希,就无奈将其还原为原始字符串。明码哈希后,明码的纯文本永远不会存储在咱们的数据库中。
盐析
是生成随机数据字符串的行为,该数据字符串将与哈希明码一起应用。这样能够确保即便两个用户明码雷同,哈希和盐析版本也将是惟一的。
bcrypt
基于河豚明码 ,通常在一系列网络框架中应用。在Node.js
开发中,咱们能够应用 bcrypt
模块对明码进行盐析和哈希解决。
在咱们的利用程序代码中,咱们将须要应用 bcrypt
模块并编写一个函数来解决盐析和哈希。
盐析和散列示例
以下示例仅用于阐明目标。 在本章的前面,咱们将明码盐析、哈希与bcrypt
集成在一起 。
// require the moduleconst bcrypt = require('bcrypt');// the cost of processing the salting data, 10 is the defaultconst saltRounds = 10;// function for hashing and saltingconst passwordEncrypt = async password => { return await bcrypt.hash(password, saltRounds)};
在此示例中,我能够传递明码 PizzaP
@rty99
,生成的盐析为 $2a
$10
$HF2rs.iYSvX1l5FPrX697O
和$2a
$10
$HF2rs.iYSvX1l5FPrX697O9dYF/O2kwHuKdQTdy.7oaMwVga54bWG
的哈希明码 。(盐析加上加密的明码字符串)。
当初,当依据哈希和盐析的明码检查用户明码时,咱们将应用 bcrypt
的 compare
办法:
// password is a value provided by the user// hash is retrieved from our DBconst checkPassword = async (plainTextPassword, hashedPassword) => { // res is either true or false return await bcrypt.compare(hashedPassword, plainTextPassword)};
通过对用户明码进行加密,咱们能够将其平安地存储在数据库中。
JSON
Web
令牌
作为用户,如果每次咱们想要拜访一个站点或应用程序的受爱护页面时都须要输出用户名和明码,那将十分令人讨厌。相同,咱们能够将用户带有JSON
Web
令牌ID
平安地存储在用户设施中。
用户从客户端收回的每个申请,他们都能够发送该令牌,服务器将应用该令牌来标识用户。
JSON
Web
令牌(JWT
)蕴含三个局部:
- 标头
Header
无关令牌和正在应用的签名算法类型的个别信息
- 有效载荷
Payload
咱们无意存储在令牌中的信息(例如用户名或
ID
) - 签名
Signature
Ameans
验证令牌
咱们看一下令牌,它仿佛由随机字符组成,每个局部之间用句点分隔:
xx
-header
-xx.yy
-payload
-yy.zz
-signature
-zz
。
在咱们的利用程序代码中,咱们能够应用jsonwebtoken
模块来生成和验证咱们的令牌。为此,咱们传递心愿存储的信息以及通常在咱们的.env
文件中存储的私密明码 。
const jwt = require('jsonwebtoken');// generate a JWT that stores a user idconst generateJWT = await user => { return await jwt.sign({ id: user._id }, process.env.JWT_SECRET);}// validate the JWTconst validateJWT = await token => { return await jwt.verify(token, process.env.JWT_SECRET);}
JWT与会议
如果你以前曾在Web
应用程序中应用过用户身份验证,则很可能会接触到用户会话。
会话信息通常存储在本地的cookie
中,并依据内存中的数据存储区(例如
Redis
,尽管也能够应用传统数据库。对于JWT
或会话,哪个有更好的争执?但我发现JWT
提供了最大的灵活性,尤其是在与非Web
环境(例如本机挪动应用程序)集成时。只管会话能够与GraphQL
很好地配合应用,但JWT
还是是GraphQL
Foundation
和 Apollo
Server
文档中的举荐办法 。
通过应用JWT
,咱们能够平安地返回用户ID
并将其存储在客户端应用程序中。
将身份验证集成到咱们的API中
当初,你曾经对用户身份验证的组件有了粗浅的理解,咱们将为用户实现注册和登录咱们的应用程序的能力。为此,咱们将更新GraphQL
和Mongoose
模式,编写 signUp
和 signIn
批改解析器以生成用户令牌,并在对服务器的每个申请中验证令牌。
用户构造
首先,咱们将增加User
类型,并更新 Note
类型的 author
字段,援用User
来更新GraphQL
模式 。为此,请更新 src/schema.js
文件:
type Note { id: ID! content: String! author: User! createdAt: DateTime! updatedAt: DateTime!}type User { id: ID! username: String! email: String! avatar: String notes: [Note!]!}
当用户注册咱们的应用程序时,他们将提交用户名、电子邮件地址和明码。当用户登录咱们的应用程序时,他们将发送一个蕴含用户名或电子邮件地址以及明码的批改。如果注册或登录批改胜利,则API
将以字符串模式返回令牌。为了在咱们的模式中实现此操作,咱们将须要在src/schema.js
文件中增加两个新的变量,每个变量将返回 String
,这将是咱们的JWT
:
type Mutation { ... signUp(username: String!, email: String!, password: String!): String! signIn(username: String, email: String, password: String!): String!}
当初,咱们的GraphQL
模式已更新,咱们还须要更新数据库模块。为此,咱们将在src/models/user.js
中创立一个Mongoose
模式文件 。该文件的设置将相似于咱们的笔记模块文件,其中蕴含用户名、电子邮件、明码和头像的字段。通过设置index
:{unique
:true
},咱们还将要求用户名和电子邮件字段在咱们的数据库中必须是惟一的 。
要创立用户数据库模块,请在src/models/user.js
文件中输出以下内容 :
const mongoose = require('mongoose');const UserSchema = new mongoose.Schema( { username: { type: String, required: true, index: { unique: true } }, email: { type: String, required: true, index: { unique: true } }, password: { type: String, required: true }, avatar: { type: String } }, { // Assigns createdAt and updatedAt fields with a Date type timestamps: true });const User = mongoose.model('User', UserSchema);module.exports = User;
搁置好咱们的用户模块文件后,咱们当初必须更新 src/models/index.js
来导出模块:
const Note = require('./note');const User = require('./user');const models = { Note, User};module.exports = models;
身份验证解析器
编写了GraphQL
和Mongoose
模式后,咱们能够实现解析器,使用户能够注册并登录到咱们的应用程序。
首先,咱们须要在 .env
环境文件增加一个JWT\_SECRET
变量。该值应该是没有空格的字符串。它将用于对咱们的JWT
进行签名,这使咱们能够在对它们进行解码时对其进行验证。
JWT_SECRET=YourPassphrase
创立此变量后,能够将所需的包导入咱们的 mutation.js
文件中。咱们将应用第三方bcrypt
、jsonwebtoken
、mongoose
和dotenv
包,并导入Apollo
服务器的AuthenticationError
和ForbiddenError
实用程序。
此外,咱们将导入gravatar
实用程序性能,该性能已蕴含在该我的项目中。这将产生一个用户电子邮件地址中的图片图像URL
。
在 src/resolvers/mutation.js
中,输出以下内容:
const bcrypt = require('bcrypt');const jwt = require('jsonwebtoken');const { AuthenticationError, ForbiddenError} = require('apollo-server-express');require('dotenv').config();const gravatar = require('../util/gravatar');
当初咱们能够编写咱们的signUp
申请。此申请将承受用户名、电子邮件地址和明码作为参数。咱们将通过批改所有空格并将其转换为所有小写字母来规范化电子邮件地址和用户名。接下来,咱们将应用bcrypt
模块对用户明码进行加密。咱们还将通过应用咱们的helper
程序库为用户头像生成Gravatar
图像URL
。执行完这些操作后,咱们会将用户数据存储在数据库中,并向用户返回令牌。
咱们能够在try/catch
块中进行所有的设置 ,如果在注册过程中呈现任何问题时,咱们的解析器能够将含糊的谬误返回给客户端。
要实现所有这些操作,请在src/resolvers/mutation.js
文件中编写 signUp
申请,如下所示 :
signUp: async (parent, { username, email, password }, { models }) => { // normalize email address email = email.trim().toLowerCase(); // hash the password const hashed = await bcrypt.hash(password, 10); // create the gravatar url const avatar = gravatar(email); try { const user = await models.User.create({ username, email, avatar, password: hashed }); // create and return the json web token return jwt.sign({ id: user._id }, process.env.JWT_SECRET); } catch (err) { console.log(err); // if there's a problem creating the account, throw an error throw new Error('Error creating account'); } },
当初,如果咱们在浏览器中切换到GraphQL
Playground
,咱们能够尝试一下 signUp
申请。为此,咱们将应用用户名,电子邮件和明码值编写一个GraphQL
批改:
mutation { signUp( username: "BeeBoop", email: "robot@example.com", password: "NotARobot10010!" )}
当咱们运行申请时,咱们的服务器将返回这样的令牌(图7
-1
):
"data": { "signUp": "eyJhbGciOiJIUzI1NiIsInR5cCI6..." }}
[外链图片转存失败,源站可能有防盗链机制,倡议将图片保留下来间接上传(img-HLrver5v-1606266940860)(http://vipkshttp0.wiz.cn/ks/s...]
图7
-1
。GraphQL
Playground
中的signUp
申请
下一步将是写咱们的登录申请。
此申请将承受用户的用户名、电子邮件和明码。而后,它将依据用户名或电子邮件地址在数据库中找到用户。找到用户后,它将解密存储在数据库中的明码,并将其与用户输出的明码进行比拟。如果用户名和明码匹配,咱们的应用程序将向用户返回令牌。如果它们不匹配,咱们将抛出一个谬误。
在src/resolvers/mutation.js
文件中,按如下所示编写此申请 :
signIn: async (parent, { username, email, password }, { models }) => { if (email) { // normalize email address email = email.trim().toLowerCase(); } const user = await models.User.findOne({ $or: [{ email }, { username }] }); // if no user is found, throw an authentication error if (!user) { throw new AuthenticationError('Error signing in'); } // if the passwords don't match, throw an authentication error const valid = await bcrypt.compare(password, user.password); if (!valid) { throw new AuthenticationError('Error signing in'); } // create and return the json web token return jwt.sign({ id: user._id }, process.env.JWT_SECRET); }
当初,咱们能够在浏览器中拜访GraphQL
Playground
,并 应用通过signUp
申请创立的帐户尝试 signIn
申请:
mutation { signIn( username: "BeeBoop", email: "robot@example.com", password: "NotARobot10010!" )}
同样,如果胜利,咱们的批改应通过JWT
解决(图7
-2
):
{ "data": { "signIn": "<TOKEN VALUE>" }}
图7
-2
。GraphQL
Playground
中的signIn
批改
有了这两个解析器,用户将可能应用JWT
注册和登录咱们的应用程序。为此,请尝试增加更多帐户,甚至成心输出不正确的信息(例如不匹配的明码),以查看GraphQL
API
返回的内容。
将用户增加到解析器上下文
当初,用户能够应用GraphQL
申请来接管惟一令牌,咱们将须要在每个申请上验证该令牌。
咱们冀望将是咱们的客户端(无论是web端、挪动端还是桌面版)都发送带有与申请中提及的HTTP
标头的受权的令牌。
而后,咱们可用于从HTTP
标头读取令牌,应用JWT\_SECRET
变量对其进行解码 ,并将用户信息以及上下文传递给每个GraphQL
解析器。通过这样做,咱们能够确定登录用户是否正在发出请求,如果他正在发出请求,那么就是那个用户。
首先,将 jsonwebtoken
模块导入 src/index.js
文件:
const jwt = require('jsonwebtoken');
导入模块后,咱们能够增加一个函数来验证令牌的有效性:
// get the user info from a JWTconst getUser = token => { if (token) { try { // return the user information from the token return jwt.verify(token, process.env.JWT_SECRET); } catch (err) { // if there's a problem with the token, throw an error throw new Error('Session invalid'); } }};
当初,在每个GraphQL
申请中,咱们将从申请的头部中获取令牌,尝试验证令牌的有效性,并将用户信息增加到上下文中。实现此操作后,每个GraphQL
解析器都能够拜访咱们存储在令牌中的用户ID
。
// Apollo Server setupconst server = new ApolloServer({ typeDefs, resolvers, context: ({ req }) => { // get the user token from the headers const token = req.headers.authorization; // try to retrieve a user with the token const user = getUser(token); // for now, let's log the user to the console: console.log(user); // add the db models and the user to the context return { models, user }; }});
只管咱们尚未执行用户交互,然而咱们能够在GraphQL
Playground
中测试用户上下文。在GraphQL
Playground
UI
的左下角,有一个标为HTTP
Header
的空间。在用户界面的那局部,咱们能够增加一个蕴含JWT
的标头,该标头是通过signUp
或 signIn
批改返回的, 如下所示(图7
-3
):
{ "Authorization": "<YOUR_JWT>"}
图7
-3
。GraphQL
Playground
中的受权标头
咱们能够通过将其与GraphQL
Playground
中的任何查问或批改一起传递来测试此受权标头。为此,咱们将编写一个简略的笔记查问,并蕴含 Authorization
标头(图7
-4
)。
query { notes { id }}
图7
-4
。GraphQL
Playground
中的受权标头和查问
如果咱们的身份验证胜利,则应该看到一个蕴含用户ID
的对象记录到终端应用程序的输入中,如图7
-5
所示 。
图7
-5
。终端的console.log
输入中的用户对象
实现所有这些步骤后,咱们当初能够在咱们的API
中对用户进行身份验证。
论断
用户帐户的创立和登录流程可能会让人感到困惑和手足无措,然而通过一一进行,咱们能够在咱们的API
中实现稳固且平安的身份验证流程。在本章中,咱们创立了注册和登录用户流程。这些只是帐户治理生态系统的一小部分,但将为咱们提供一个稳固的根底。在下一章中,咱们将在API
中实现特定于用户的交互,该交互将所有权调配给应用程序中的笔记和流动。
如果有了解不到位的中央,欢送大家纠错。如果感觉还能够,麻烦你点赞珍藏或者分享一下,心愿能够帮到更多人。