介绍
许多 Web 应用程序和 API 使用一种身份验证形式来保护资源并将其访问权限限制为仅允许经过验证的用户。
JSON Web Token (JWT) 是一种开放标准,它定义了一种紧凑且自包含的方式,用于在各方之间作为 JSON 对象安全传输信息。
本指南将引导您了解如何使用 JWT 和Passport(Node的身份验证中间件)为API 实现身份验证。
以下是您将要构建的应用程序的简要概述:
- 用户注册,并创建用户帐户。
- 用户登录后,会为用户分配一个 JSON Web 令牌。
- 此令牌由用户在尝试访问某些安全路由时发送。
- 一旦令牌被验证,用户就可以访问该路由。
先决条件
要完成本教程,您需要:
- Node.js 安装在本地,您可以按照如何安装 Node.js 和创建本地开发环境来完成。
- MongoDB在本地安装并运行,您可以按照官方文档进行操作。
- 测试 API 端点需要下载并安装Postman 之类的工具。
本教程已通过 Node v14.2.0、npm
v6.14.5 和mongodb-community
v4.2.6 验证。
步骤 1 — 设置项目
让我们从设置项目开始。在终端窗口中,为项目创建一个目录:
- mkdir jwt-and-passport-auth
并导航到该新目录:
- cd jwt-and-passport-auth
接下来,初始化一个新的package.json
:
- npm init -y
安装项目依赖:
- npm install --save bcrypt@4.0.1 body-parser@1.19.0 express@4.17.1 jsonwebtoken@8.5.1 mongoose@5.9.15 passport@0.4.1 passport-jwt@4.0.0 passport-local@1.0.0
您将需要bcrypt
散列用户密码、jsonwebtoken
签署令牌、passport-local
实施本地策略以及passport-jwt
检索和验证 JWT。
警告:运行安装时,您可能会遇到问题,bcrypt
具体取决于您运行的 Node 版本。
请参阅自述文件以确定与您的环境的兼容性。
至此,你的项目已经初始化完毕,所有的依赖都已经安装完毕。接下来,您将添加一个数据库来存储用户信息。
步骤 2 — 设置数据库
一个数据库架构建立的各类数据库的数据和结构。您的数据库将需要用户架构。
创建model
目录:
- mkdir model
model.js
在这个新目录中创建一个文件:
- nano model/model.js
该mongoose
库用于定义映射到 MongoDB 集合的模式。在架构中,用户需要电子邮件和密码。该mongoose
库采用模式并将其转换为模型:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
}
});
const UserModel = mongoose.model('user', UserSchema);
module.exports = UserModel;
您应该避免以纯文本形式存储密码,因为如果攻击者设法访问数据库,则可以读取密码。
为了避免这种情况,您将使用一个称为bcrypt
散列用户密码的包并安全地存储它们。添加库和以下代码行:
// ...
const bcrypt = require('bcrypt');
// ...
const UserSchema = new Schema({
// ...
});
UserSchema.pre(
'save',
async function(next) {
const user = this;
const hash = await bcrypt.hash(this.password, 10);
this.password = hash;
next();
}
);
// ...
module.exports = UserModel;
UserScheme.pre()
函数中的代码称为预挂钩。在将用户信息存入数据库之前,会调用此函数,您将获取明文密码,将其散列并存储。
this
指的是要保存的当前文档。
await bcrypt.hash(this.password, 10)
将密码和salt round(或cost)的值传递给10
. 更高的成本将运行散列进行更多的迭代并且更安全。它需要在计算量更大的情况下进行权衡,以至于可能会影响应用程序的性能。
接下来,用散列替换纯文本密码,然后存储它。
最后,您表示您已完成并应该使用next()
.
您还需要确保尝试登录的用户具有正确的凭据。添加以下新方法:
// ...
const UserSchema = new Schema({
// ...
});
UserSchema.pre(
// ...
});
UserSchema.methods.isValidPassword = async function(password) {
const user = this;
const compare = await bcrypt.compare(password, user.password);
return compare;
}
// ...
module.exports = UserModel;
bcrypt
散列用户发送的登录密码,并检查存储在数据库中的散列密码是否与发送的密码匹配。true
如果匹配,它将返回。否则,false
如果不匹配,它将返回。
此时,您已经为 MongoDB 集合定义了架构和模型。
步骤 3 — 设置注册和登录中间件
Passport 是用于验证请求的验证中间件。
它允许开发人员使用不同的策略来验证用户,例如使用本地数据库或通过 API 连接到社交网络。
在此步骤中,您将使用本地(电子邮件和密码)策略。
您将使用该passport-local
策略创建处理用户注册和登录的中间件。然后将其插入某些路由并用于身份验证。
创建auth
目录:
- mkdir auth
auth.js
在这个新目录中创建一个文件:
- nano auth/auth.js
首先要求passport
,passport-local
和UserModel
在上一步中创建的 :
const passport = require('passport');
const localStrategy = require('passport-local').Strategy;
const UserModel = require('../model/model');
首先,添加一个 Passport 中间件来处理用户注册:
// ...
passport.use(
'signup',
new localStrategy(
{
usernameField: 'email',
passwordField: 'password'
},
async (email, password, done) => {
try {
const user = await UserModel.create({ email, password });
return done(null, user);
} catch (error) {
done(error);
}
}
)
);
此代码将用户提供的信息保存到数据库中,如果成功则将用户信息发送到下一个中间件。
否则,它会报告错误。
接下来,添加一个 Passport 中间件来处理用户登录:
// ...
passport.use(
'login',
new localStrategy(
{
usernameField: 'email',
passwordField: 'password'
},
async (email, password, done) => {
try {
const user = await UserModel.findOne({ email });
if (!user) {
return done(null, false, { message: 'User not found' });
}
const validate = await user.isValidPassword(password);
if (!validate) {
return done(null, false, { message: 'Wrong Password' });
}
return done(null, user, { message: 'Logged in Successfully' });
} catch (error) {
return done(error);
}
}
)
);
此代码查找与提供的电子邮件关联的一个用户。
- 如果用户与数据库中的任何用户都不匹配,则返回
"User not found"
错误。 - 如果密码与数据库中与用户关联的密码不匹配,则返回
"Wrong Password"
错误。 - 如果用户和密码匹配,则返回一条
"Logged in Successfully"
消息,并将用户信息发送到下一个中间件。
否则,它会报告错误。
此时,您有一个用于处理注册和登录的中间件。
第 4 步 – 创建注册端点
Express是一个提供路由的 Web 框架。在此步骤中,您将为signup
端点创建路由。
创建routes
目录:
- mkdir routes
routes.js
在这个新目录中创建一个文件:
- nano routes/routes.js
从要求express
和开始passport
:
const express = require('express');
const passport = require('passport');
const router = express.Router();
module.exports = router;
接下来,添加对 POST 请求的处理signup
:
// ...
const router = express.Router();
router.post(
'/signup',
passport.authenticate('signup', { session: false }),
async (req, res, next) => {
res.json({
message: 'Signup successful',
user: req.user
});
}
);
module.exports = router;
当用户向该路由发送 POST 请求时,Passport 会根据之前创建的中间件对用户进行身份验证。
您现在有了一个signup
端点。接下来,您将需要一个login
端点。
第 5 步 – 创建登录端点并签署 JWT
当用户登录时,用户信息将传递给您的自定义回调,后者又会使用该信息创建一个安全令牌。
在此步骤中,您将为login
端点创建路由。
首先,要求jsonwebtoken
:
const express = require('express');
const passport = require('passport');
const jwt = require('jsonwebtoken');
// ...
接下来,添加对 POST 请求的处理login
:
// ...
const router = express.Router();
// ...
router.post(
'/login',
async (req, res, next) => {
passport.authenticate(
'login',
async (err, user, info) => {
try {
if (err || !user) {
const error = new Error('An error occurred.');
return next(error);
}
req.login(
user,
{ session: false },
async (error) => {
if (error) return next(error);
const body = { _id: user._id, email: user.email };
const token = jwt.sign({ user: body }, 'TOP_SECRET');
return res.json({ token });
}
);
} catch (error) {
return next(error);
}
}
)(req, res, next);
}
);
module.exports = router;
您不应在令牌中存储敏感信息,例如用户密码。
您将id
和存储email
在 JWT 的负载中。然后,您使用秘密或密钥 ( TOP_SECRET
)对令牌进行签名。最后,您将令牌发回给用户。
注意:您设置{ session: false }
是因为您不想在会话中存储用户详细信息。您希望用户将每个请求的令牌发送到安全路由。
这对于 API 尤其有用,但出于性能原因,它不是 Web 应用程序的推荐方法。
您现在有了一个login
端点。成功登录的用户将生成一个令牌。但是,您的应用程序尚未对令牌执行任何操作。
第 6 步 – 验证 JWT
所以现在您已经处理了用户注册和登录,下一步是允许具有令牌的用户访问某些安全路由。
在此步骤中,您将验证令牌未被操纵且有效。
重温auth.js
文件:
- nano auth/auth.js
添加以下代码行:
// ...
const JWTstrategy = require('passport-jwt').Strategy;
const ExtractJWT = require('passport-jwt').ExtractJwt;
passport.use(
new JWTstrategy(
{
secretOrKey: 'TOP_SECRET',
jwtFromRequest: ExtractJWT.fromUrlQueryParameter('secret_token')
},
async (token, done) => {
try {
return done(null, token.user);
} catch (error) {
done(error);
}
}
)
);
此代码用于passport-jwt
从查询参数中提取 JWT。然后验证此令牌是否已使用登录期间设置的秘密或密钥进行签名 ( TOP_SECRET
)。如果令牌有效,则将用户详细信息传递给下一个中间件。
注意:如果您需要令牌中不可用的有关用户的额外或敏感详细信息,您可以使用_id
令牌上的可用信息从数据库中检索它们。
您的应用程序现在能够签署令牌并验证它们。
第 7 步 – 创建安全路由
现在,让我们创建一些只有经过验证的令牌的用户才能访问的安全路由。
创建一个新secure-routes.js
文件:
- nano routes/secure-routes.js
接下来,添加以下代码行:
const express = require('express');
const router = express.Router();
router.get(
'/profile',
(req, res, next) => {
res.json({
message: 'You made it to the secure route',
user: req.user,
token: req.query.secret_token
})
}
);
module.exports = router;
此代码处理 GET 请求profile
。它返回一条"You made it to the secure route"
消息。它还返回有关user
和 的信息token
。
目标是只有拥有经过验证的令牌的用户才会收到此响应。
第 8 步 — 将所有内容放在一起
所以现在您已经完成了路由和身份验证中间件的创建,您可以将所有内容放在一起。
创建一个新app.js
文件:
- nano app.js
接下来,添加以下代码:
const express = require('express');
const mongoose = require('mongoose');
const passport = require('passport');
const bodyParser = require('body-parser');
const UserModel = require('./model/model');
mongoose.connect('mongodb://127.0.0.1:27017/passport-jwt', { useMongoClient: true });
mongoose.connection.on('error', error => console.log(error) );
mongoose.Promise = global.Promise;
require('./auth/auth');
const routes = require('./routes/routes');
const secureRoute = require('./routes/secure-routes');
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use('/', routes);
// Plug in the JWT strategy as a middleware so only verified users can access this route.
app.use('/user', passport.authenticate('jwt', { session: false }), secureRoute);
// Handle errors.
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.json({ error: err });
});
app.listen(3000, () => {
console.log('Server started.')
});
注意:根据您的 版本mongoose
,您可能会遇到以下消息:WARNING: The 'useMongoClient' option is no longer necessary in mongoose 5.x, please remove it.
.
你也可能遇到弃用通知的useNewUrlParser
,useUnifiedTopology
和ensureIndex
(createIndexes
)。
在故障排除期间,我们能够通过修改mongoose.connect
方法调用并添加mongoose.set
方法调用来解决这些问题:
mongoose.connect("mongodb://127.0.0.1:27017/passport-jwt", {
useNewUrlParser: true,
useUnifiedTopology: true,
});
mongoose.set("useCreateIndex", true);
使用以下命令运行您的应用程序:
- node app.js
您将看到一条"Server started."
消息。让应用程序运行以测试它。
步骤 9 — 使用 Postman 进行测试
现在您已将所有内容放在一起,您可以使用 Postman 来测试您的 API 身份验证。
注意:如果您需要帮助导航 Postman 界面以获取请求,请参阅官方文档。
首先,您必须使用电子邮件和密码在您的应用程序中注册一个新用户。
在 Postman 中,设置对signup
您在 中创建的端点的请求routes.js
:
POST localhost:3000/signup
Body
x-www-form-urlencoded
并通过Body
您的请求发送这些详细信息:
钥匙 | 价值 |
---|---|
电子邮件 | [email protected] |
密码 | password |
完成后,单击“发送”按钮以发起POST
请求:
Output{
"message": "Signup successful",
"user": {
"_id": "[a long string of characters representing a unique id]",
"email": "[email protected]",
"password": "[a long string of characters representing an encrypted password]",
"__v": 0
}
}
您的密码显示为加密字符串,因为这是它在数据库中的存储方式。这是你在写的前钩的结果model.js
使用bcrypt
到哈希密码。
现在,使用凭据登录并获取您的令牌。
在 Postman 中,设置对login
您在 中创建的端点的请求routes.js
:
POST localhost:3000/login
Body
x-www-form-urlencoded
并通过Body
您的请求发送这些详细信息:
钥匙 | 价值 |
---|---|
电子邮件 | [email protected] |
密码 | password |
完成后,单击“发送”按钮以发起POST
请求:
Output{
"token": "[a long string of characters representing a token]"
}
现在您有了令牌,只要您想访问安全路由,就可以发送此令牌。复制并粘贴以备后用。
您可以通过访问 来测试您的应用程序如何处理验证令牌/user/profile
。
在 Postman 中,设置对profile
您在 中创建的端点的请求secure-routes.js
:
GET localhost:3000/user/profile
Params
并在名为的查询参数中传递您的令牌secret_token
:
钥匙 | 价值 |
---|---|
secret_token | [a long string of characters representing a token] |
完成后,单击“发送”按钮以发起GET
请求:
Output{
"message": "You made it to the secure route",
"user": {
"_id": "[a long string of characters representing a unique id]",
"email": "[email protected]"
},
"token": "[a long string of characters representing a token]"
}
令牌将被收集和验证。如果令牌有效,您将获得访问安全路由的权限。这是您在 中创建的响应的结果secure-routes.js
。
您也可以尝试访问此路由,但如果令牌无效,请求将返回Unauthorized
错误。
结论
在本教程中,您使用 JWT 设置 API 身份验证并使用 Postman 对其进行测试。
JSON Web 令牌提供了一种为 API 创建身份验证的安全方式。通过加密令牌中的所有信息,可以添加额外的安全层,从而使其更加安全。
如果您想更深入地了解 JWT,可以使用以下额外资源:
- 通过 Auth0开始使用 JSON Web 令牌
- Auth0使用 JSON Web 令牌作为 API 密钥