微信截圖_1741240006974.png)
使用 Axios 在 React 中創(chuàng)建集中式 API 客戶端文件
我們將為每個API路由創(chuàng)建單獨(dú)的文件,因此這里我們使用glob
來查找所有這些文件,以便為每個路由創(chuàng)建一個新路由。在設(shè)置身份驗證策略時,我們需要提供一個密鑰以供使用,該密鑰將與JWT中提供的密鑰進(jìn)行驗證。此密鑰設(shè)置在文件中,以便我們可以在其他位置共享它。我們還指定了應(yīng)使用的算法是HS256
,但當(dāng)然也可以使用其他算法。最后,服務(wù)器啟動后,我們通過mongoose
連接到數(shù)據(jù)庫,并在此過程中查找錯誤。
我們需要在config.js
中設(shè)置一個密鑰。對于生產(chǎn)應(yīng)用程序,這應(yīng)該是一個長且難以猜測的字符串,但現(xiàn)在我們將只使用一個簡單的字符串。
// config.js
const key = 'secretkey';
module.exports = key;
我們此API的目標(biāo)是利用Hapi生態(tài)系統(tǒng)提供的一些工具,例如用于表單驗證的Joi。我們還使用Mongoose,這意味著我們需要為我們的數(shù)據(jù)資源設(shè)置一個模式(模型)。為了保持整潔,我們將資源拆分為幾個不同的文件:
-- route
|-- model
|-- routes
|-- schemas
|-- util
我們將Mongoose模型保存在model
目錄中,并將任何驗證模式保存在schemas
中。我們還有一個目錄用于任何特定于路由的實用程序函數(shù)。
我們應(yīng)該處理的第一個路由是用于創(chuàng)建新用戶的路由。此端點(diǎn)將接受用戶名、電子郵件和密碼,然后將用戶保存在數(shù)據(jù)庫中。當(dāng)然,我們希望對密碼進(jìn)行加鹽和哈希處理,以便安全存儲,并且可以使用bcrypt來實現(xiàn)這一點(diǎn)。
首先,讓我們?yōu)橘Y源設(shè)置Mongoose模型。
// api/users/model/User.js
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const userModel = new Schema({
email: { type: String, required: true, index: { unique: true } },
username: { type: String, required: true, index: { unique: true } },
password: { type: String, required: true },
admin: { type: Boolean, required: true }
});
module.exports = mongoose.model('User', userModel);
此模型描述了應(yīng)如何塑造資源,并為我們進(jìn)行了一些驗證。正如我們將在下面看到的,我們將使用 Joi 獲得更好的驗證。
'use strict';
const bcrypt = require('bcrypt');
const Boom = require('boom');
const User = require('../model/User');
const createUserSchema = require('../schemas/createUser');
const verifyUniqueUser = require('../util/userFunctions').verifyUniqueUser;
const createToken = require('../util/token');
function hashPassword(password, cb) {
// Generate a salt at level 10 strength
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(password, salt, (err, hash) => {
return cb(err, hash);
});
});
}
module.exports = {
method: 'POST',
path: '/api/users',
config: {
// Before the route handler runs, verify that the user is unique
pre: [
{ method: verifyUniqueUser }
],
handler: (req, res) => {
let user = new User();
user.email = req.payload.email;
user.username = req.payload.username;
user.admin = false;
hashPassword(req.payload.password, (err, hash) => {
if (err) {
throw Boom.badRequest(err);
}
user.password = hash;
user.save((err, user) => {
if (err) {
throw Boom.badRequest(err);
}
// If the user is saved successfully, issue a JWT
res({ id_token: createToken(user) }).code(201);
});
});
},
// Validate the payload against the Joi schema
validate: {
payload: createUserSchema
}
}
}
Hapi路由需要有一個路由和方法,以及一個處理程序(handler),如果要使其有用的話。在這里配置這些細(xì)節(jié)是不言而喻的,但有幾件事可能不熟悉。在底部,我們有一個用于驗證輸入的位置,在這種情況下,我們要驗證傳入的payload
。如果我們接受來自用戶的參數(shù),那么我們可以在鍵上指定驗證。此驗證來自子目錄中的schemas
。
// api/users/schemas/createUser.js
'use strict';
const Joi = require('joi');
const createUserSchema = Joi.object({
username: Joi.string().alphanum().min(2).max(30).required(),
email: Joi.string().email().required(),
password: Joi.string().required()
});
module.exports = createUserSchema;
該模式相當(dāng)易讀——我們希望確保每個項都是字符串,并且我們表示它們都是必需的。不過,我們可以超越這一點(diǎn),就像我們對username
和email
所做的那樣。Joi模式有很多選項,您可以在此處查看完整的API文檔。使用Joi設(shè)置驗證非常棒,因為它會自動拒絕與模式內(nèi)容不匹配的任何輸入,并提供合理的錯誤消息,而無需進(jìn)行任何配置。
路由中另一個可能不熟悉的項目是對象內(nèi)的pre
數(shù)組。在Hapi中,我們可以定義任意數(shù)量的先決條件函數(shù),這些函數(shù)將在到達(dá)路由處理程序之前運(yùn)行。如果我們需要對傳入的數(shù)據(jù)負(fù)載進(jìn)行一些處理,這非常有用,并且是驗證提供給端點(diǎn)的username
和email
是否唯一以及是否已存在具有這些詳細(xì)信息的用戶的完美位置。我們將方法指向userFunctions.js
文件中的preverifyUniqueUser
。
我們可以使用pre
方法做很多事情,并且由于它們完全支持異步和并行化,因此我們可以使用許多很好的可能性來抽象路由邏輯的部分。這樣,我們的處理程序就變得非常小且更易于維護(hù)。
// api/users/util/userFunctions.js
'use strict';
const Boom = require('boom');
const User = require('../model/User');
function verifyUniqueUser(req, res) {
// Find an entry from the database that
// matches either the email or username
User.findOne({
$or: [
{ email: req.payload.email },
{ username: req.payload.username }
]
}, (err, user) => {
// Check whether the username or email
// is already taken and error out if so
if (user) {
if (user.username === req.payload.username) {
res(Boom.badRequest('Username taken'));
}
if (user.email === req.payload.email) {
res(Boom.badRequest('Email taken'));
}
}
// If everything checks out, send the payload through
// to the route handler
res(req.payload);
});
}
module.exports = {
verifyUniqueUser: verifyUniqueUser
}
此函數(shù)在數(shù)據(jù)庫中查找具有與負(fù)載中傳遞的相同用戶名或電子郵件地址的用戶,如果找到,則返回相應(yīng)的錯誤消息。如果一切正常,則發(fā)送負(fù)載以供處理程序使用。
在上述路由中,當(dāng)用戶成功創(chuàng)建賬戶時,他們的JWT會被發(fā)送回給他們。我們需要一個函數(shù)來實際簽發(fā)JWT。
// api/users/util/token.js
'use strict';
const jwt = require('jsonwebtoken');
const secret = require('../../../config');
function createToken(user) {
let scopes;
// Check if the user object passed in
// has admin set to true, and if so, set
// scopes to admin
if (user.admin) {
scopes = 'admin';
}
// Sign the JWT
return jwt.sign({ id: user._id, username: user.username, scope: scopes }, secret, { algorithm: 'HS256', expiresIn: "1h" } );
}
module.exports = createToken;
你可能已經(jīng)注意到,我們在上面的路由處理程序中默認(rèn)為 to。當(dāng)我們對 JWT 進(jìn)行簽名時,我們首先檢查用戶是否是管理員,如果是,我們會附加適當(dāng)?shù)姆秶N覀冞€在此處指定要用作算法,并讓 JWT 在 1 小時后過期。
注意:在您自己的應(yīng)用程序中實現(xiàn)將 user 范圍附加到新創(chuàng)建的用戶的方式可能與我們在此處執(zhí)行的操作不同,但我們可以通過這種方法快速了解它。
現(xiàn)在,當(dāng)用戶成功注冊時,將返回其 JWT。
如果我們再次嘗試保存同一個用戶,我們可以看到該函數(shù)正在工作。
稍后我們會看到如何保護(hù)不同的路由,但首先,讓我們添加一個路由,允許用戶在注冊后進(jìn)行自我認(rèn)證。我們需要一些邏輯來檢查用戶傳入的密碼是否與數(shù)據(jù)庫中存儲的哈希密碼匹配。如果兩者匹配,那么我們就可以向用戶頒發(fā)JWT。這是另一個可以使用某種方法的地方,我們將在此方法上附加一個新的函數(shù),我們稱之為preverifyCredentials
// api/users/util/userFunctions.js
...
function verifyCredentials(req, res) {
const password = req.payload.password;
// Find an entry from the database that
// matches either the email or username
User.findOne({
$or: [
{ email: req.payload.email },
{ username: req.payload.username }
]
}, (err, user) => {
if (user) {
bcrypt.compare(password, user.password, (err, isValid) => {
if (isValid) {
res(user);
}
else {
res(Boom.badRequest('Incorrect password!'));
}
});
} else {
res(Boom.badRequest('Incorrect username or email!'));
}
});
}
module.exports = {
verifyUniqueUser: verifyUniqueUser,
verifyCredentials: verifyCredentials
}
這個函數(shù)使用bcrypt
來檢查有效載荷中發(fā)送的密碼是否與數(shù)據(jù)庫中的用戶條目匹配,如果有效,則用戶對象會被發(fā)送到處理器。我們使用boom
來響應(yīng)錯誤情況,如果遇到錯誤,它們會冒泡到處理器。
現(xiàn)在我們的路由設(shè)置可以非常簡單。
// api/users/routes/authenticateUser.js
'use strict';
const Boom = require('boom');
const User = require('../model/User');
const authenticateUserSchema = require('../schemas/authenticateUser');
const verifyCredentials = require('../util/userFunctions').verifyCredentials;
const createToken = require('../util/token');
module.exports = {
method: 'POST',
path: '/api/users/authenticate',
config: {
// Check the user's password against the DB
pre: [
{ method: verifyCredentials, assign: 'user' }
],
handler: (req, res) => {
// If the user's password is correct, we can issue a token.
// If it was incorrect, the error will bubble up from the pre method
res({ id_token: createToken(req.pre.user) }).code(201);
},
validate: {
payload: authenticateUserSchema
}
}
}
接下來,我們需要設(shè)置驗證規(guī)則,以便為此路由進(jìn)行Joi驗證,但這次它的工作方式會略有不同。用戶注冊時需要提供用戶名和電子郵件,但當(dāng)他們進(jìn)行認(rèn)證時,只需要提供其中之一即可。為此,我們可以使用Joi的.alternatives
方法。
// api/users/schema/authenticateUser.js
'use strict';
const Joi = require('joi');
const authenticateUserSchema = Joi.alternatives().try(
Joi.object({
username: Joi.string().alphanum().min(2).max(30).required(),
password: Joi.string().required()
}),
Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required()
})
);
module.exports = authenticateUserSchema;
.alternatives
方法接受我們希望嘗試的驗證替代方案的參數(shù)。這些可以是像Joi.string()
這樣的類型檢查,或者我們可以傳遞單個對象。在這種情況下,我們傳遞了兩個對象——一個用于處理用戶名的情況,另一個用于處理電子郵件的情況。這將允許用戶使用他們的用戶名或電子郵件進(jìn)行認(rèn)證。
對于這個簡單的API,我們假設(shè)只有管理員能夠獲取數(shù)據(jù)庫中所有用戶的列表。在Hapi應(yīng)用程序中使用帶作用域的JWT認(rèn)證可以輕松地實現(xiàn)細(xì)粒度的用戶訪問控制,但目前我們僅設(shè)置兩個級別:管理員和其他用戶。請記住,我們?yōu)樵O(shè)置新用戶的作用域編寫了路由,默認(rèn)設(shè)置為非管理員。我們可以在處理器中臨時設(shè)置此值以獲取具有管理員訪問權(quán)限的用戶,或者我們只需在數(shù)據(jù)庫中更改此值。請參閱createUseradminfalsetruerepo
,這是一個響應(yīng)請求的端點(diǎn),允許管理員更改其他用戶的作用域。
在為我們的一個用戶設(shè)置后,讓我們看看如何限制顯示所有用戶列表的終端節(jié)點(diǎn)的 API 訪問。
// api/users/routes/getUsers.js
'use strict';
const User = require('../model/User');
const Boom = require('boom');
module.exports = {
method: 'GET',
path: '/api/users',
config: {
handler: (req, res) => {
User
.find()
// Deselect the password and version fields
.select('-password -__v')
.exec((err, users) => {
if (err) {
throw Boom.badRequest(err);
}
if (!users.length) {
throw Boom.notFound('No users found!');
}
res(users);
})
},
// Add authentication to this route
// The user must have a scope of admin
auth: {
strategy: 'jwt',
scope: ['admin']
}
}
}
在為我們的一個用戶設(shè)置管理員權(quán)限后,讓我們看看如何限制顯示所有用戶列表的API訪問。我們已指定此路由應(yīng)實現(xiàn)認(rèn)證策略(我們在中定義了該策略),并且用戶必須具有管理員作用域才能訪問該路由。如果我們檢查jwtserver.js
中的JWT,可以看到我們有一個作用域為admin
。
您可能想知道這是否安全。由于我們可以在調(diào)試器中檢查和更改JWT的內(nèi)容,惡意用戶是否可以更改現(xiàn)有的JWT或創(chuàng)建一個新的JWT來破壞API?請記住,JWT的優(yōu)點(diǎn)在于它們使用服務(wù)器上的密鑰進(jìn)行數(shù)字簽名。要修改JWT使其有效,攻擊者需要知道密鑰。只要我們有一個強(qiáng)大的私鑰,我們的JWT就是安全的。
現(xiàn)在我們已經(jīng)有了用于創(chuàng)建和驗證用戶的終端節(jié)點(diǎn),我們可以簡單地將身份驗證策略應(yīng)用于我們喜歡的任何其他終端節(jié)點(diǎn)。
我們已經(jīng)看到了在Hapi應(yīng)用程序中將認(rèn)證應(yīng)用于單個端點(diǎn)是多么容易。我們只需將認(rèn)證策略附加到路由對象即可。但是,如果我們想為每個端點(diǎn)都應(yīng)用認(rèn)證,那么操作將變得更加簡單。為此,我們只需在注冊策略時設(shè)置mode
為true
,并且可以通過將'required'
或'optional'
作為第三個參數(shù)傳遞來實現(xiàn)。如果我們希望所有端點(diǎn)都需要認(rèn)證,可以將mode
設(shè)置為'required'
。
// server.js
...
server.auth.strategy('jwt', 'jwt', 'required', {
key: secret,
verifyOptions: { algorithms: ['HS256'] }
});
...
Hapi還提供了一些其他有趣的認(rèn)證功能,其中之一是使認(rèn)證成為可選的。將mode
設(shè)置為'optional'
或'try'
將允許用戶無論是否經(jīng)過認(rèn)證都可以訪問該路由。它們之間的區(qū)別在于,使用'optional'
時,用戶的認(rèn)證數(shù)據(jù)必須有效,而使用'try'
時,即使認(rèn)證數(shù)據(jù)無效,也會接受該數(shù)據(jù)。
我們已經(jīng)成功地在Hapi上實現(xiàn)了自己的認(rèn)證功能,但這只是冰山一角。為了構(gòu)建一個健壯的系統(tǒng),我們需要考慮認(rèn)證方面的許多更多細(xì)節(jié)。如果我們想支持現(xiàn)代認(rèn)證功能,如社交登錄、多因素認(rèn)證和單點(diǎn)登錄,那么自己實現(xiàn)端到端的認(rèn)證可能會非常棘手。幸運(yùn)的是,Auth0為我們提供了開箱即用的所有這些功能(以及更多)!
使用Auth0,Hapi認(rèn)證變得非常簡單。
如果您還沒有這樣做,請注冊您的免費(fèi)Auth0帳戶。免費(fèi)計劃為您提供7000個常規(guī)活躍用戶和兩個社交身份提供商,這對于許多實際應(yīng)用來說已經(jīng)足夠了。
我們已經(jīng)為Hapi設(shè)置了一個認(rèn)證策略,使用上面提到的hapi-auth-jwt。現(xiàn)在,我們只需要使用我們的Auth0私鑰,而不是在.config.js中設(shè)置的簡單密鑰。
// config.js
const key = 'your_auth0_secret';
module.exports = key;
現(xiàn)在,我們可以使用上面描述的任何方法來保護(hù)我們的端點(diǎn)。我們可以將認(rèn)證策略逐個應(yīng)用于每個路由,或者通過將模式設(shè)置為.required來全局設(shè)置它。
默認(rèn)情況下,Auth0 會為您存儲用戶數(shù)據(jù),這意味著當(dāng)用戶在您的應(yīng)用程序中進(jìn)行身份驗證時,調(diào)用不會轉(zhuǎn)到您的服務(wù)器。相反,Auth0 負(fù)責(zé)檢查用戶的憑證,并在成功登錄時向他們頒發(fā) JWT。
用戶可以通過幾種不同的方式進(jìn)行認(rèn)證并獲得JWT,但最簡單的方法是使用Auth0在您的應(yīng)用程序前端提供的集中登錄頁面。我們可以輕松地將其添加到我們的項目中,并使用一些簡單的JavaScript觸發(fā)它。
注意:Auth0為所有流行的框架提供了SDK和集成示例,您可以在文檔中查看適用于您特定項目的代碼示例。
首先,將庫添加到您的前端。這里指的是auth0-js
庫。
<!-- index.html -->
...
<!-- Auth0.js script -->
<script src="https://cdn.auth0.com/js/auth0/9.0.0/auth0.min.js"></script>
<!-- Setting the right viewport -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
...
接下來,配置一個auth0-js
實例。
// app.js
var webAuth = new auth0.WebAuth({
domain: 'YOUR_DOMAIN',
clientID: 'YOUR_CLIENT_ID',
responseType: 'token',
redirectUri: 'YOUR_REDIRECT_URI'
});
您可以將事件監(jiān)聽器附加到按鈕點(diǎn)擊事件上,并調(diào)用它來重定向到集中登錄頁面。一旦授權(quán),用戶將被重定向回我們的頁面,我們可以在那里獲取結(jié)果。這里使用的是webAuth.authorize
方法。
// app.js
document.getElementById('btn-login').addEventListener('click', function() {
webAuth.authorize();
});
if (window.location.hash) {
webAuth.parseHash({ hash: window.location.hash }, function(err, authResult) {
if (err) {
return console.log(err);
}
if (authResult) {
webAuth.client.userInfo(authResult.accessToken, function(err, user) {
localStorage.setItem('userProfile', JSON.stringify(user))
localStorage.setItem('id_token', authResult.idToken)
});
}
});
}
當(dāng)用戶成功登錄時,他們的JWT和個人資料將保存在本地存儲中。
為了向您的API發(fā)出安全請求,只需將用戶的JWT作為標(biāo)頭附加即可。這里使用的是Authorization
標(biāo)頭。
我們上面構(gòu)建的API檢查了一個簡單的作用域,這至少為我們提供了一定程度的訪問控制。然而,我們可以通過使作用域特定于用戶應(yīng)該擁有的單個端點(diǎn)和操作(創(chuàng)建、更新等)來使作用域更加細(xì)化。使用Auth0,我們可以為用戶存儲任意元數(shù)據(jù),這就是我們可以存儲其作用域的地方。存儲元數(shù)據(jù)非常容易——我們可以手動輸入,也可以創(chuàng)建管理員規(guī)則來自動化該過程。
HapiJS是一個為Node打造的出色框架,它使得構(gòu)建API既簡單又靈活。Hapi生態(tài)系統(tǒng)中的其他包,包括Joi和Boom,使得創(chuàng)建一個健壯的應(yīng)用程序變得輕而易舉,并且讓我們省去了很多繁重的工作。正如我們所見,為Hapi實現(xiàn)JWT認(rèn)證也非常簡單——我們只需要使用hapi-auth-jwt并注冊我們的認(rèn)證策略。
“HapiJS是一個為Node打造的出色框架,它使得構(gòu)建API既簡單又靈活。”
你對HapiJS有什么看法?它是否是Express的一個好替代品?讓我們知道你的想法!
原文鏈接:https://auth0.com/blog/hapijs-authentication-secure-your-api-with-json-web-tokens/
使用 Axios 在 React 中創(chuàng)建集中式 API 客戶端文件
Cursor + Devbox 進(jìn)階開發(fā)實踐:從 Hello World 到 One API
火山引擎如何接入API:從入門到實踐的技術(shù)指南
什么是聚類分析?
通過API監(jiān)控提高API穩(wěn)定性
使用 Whisper API 通過設(shè)備麥克風(fēng)把語音轉(zhuǎn)錄為文本
如何在 Apifox 中發(fā)布多語言的 API 文檔?
在 Golang 中實現(xiàn) JWT 令牌認(rèn)證
深入了解 Gateway API 的推理擴(kuò)展