微信截圖_1741240006974.png)
使用 Axios 在 React 中創(chuàng)建集中式 API 客戶端文件
我們將為每個(gè)API路由創(chuàng)建單獨(dú)的文件,因此這里我們使用glob
來(lái)查找所有這些文件,以便為每個(gè)路由創(chuàng)建一個(gè)新路由。在設(shè)置身份驗(yàn)證策略時(shí),我們需要提供一個(gè)密鑰以供使用,該密鑰將與JWT中提供的密鑰進(jìn)行驗(yàn)證。此密鑰設(shè)置在文件中,以便我們可以在其他位置共享它。我們還指定了應(yīng)使用的算法是HS256
,但當(dāng)然也可以使用其他算法。最后,服務(wù)器啟動(dòng)后,我們通過(guò)mongoose
連接到數(shù)據(jù)庫(kù),并在此過(guò)程中查找錯(cuò)誤。
我們需要在config.js
中設(shè)置一個(gè)密鑰。對(duì)于生產(chǎn)應(yīng)用程序,這應(yīng)該是一個(gè)長(zhǎng)且難以猜測(cè)的字符串,但現(xiàn)在我們將只使用一個(gè)簡(jiǎn)單的字符串。
// config.js
const key = 'secretkey';
module.exports = key;
我們此API的目標(biāo)是利用Hapi生態(tài)系統(tǒng)提供的一些工具,例如用于表單驗(yàn)證的Joi。我們還使用Mongoose,這意味著我們需要為我們的數(shù)據(jù)資源設(shè)置一個(gè)模式(模型)。為了保持整潔,我們將資源拆分為幾個(gè)不同的文件:
-- route
|-- model
|-- routes
|-- schemas
|-- util
我們將Mongoose模型保存在model
目錄中,并將任何驗(yàn)證模式保存在schemas
中。我們還有一個(gè)目錄用于任何特定于路由的實(shí)用程序函數(shù)。
我們應(yīng)該處理的第一個(gè)路由是用于創(chuàng)建新用戶的路由。此端點(diǎn)將接受用戶名、電子郵件和密碼,然后將用戶保存在數(shù)據(jù)庫(kù)中。當(dāng)然,我們希望對(duì)密碼進(jìn)行加鹽和哈希處理,以便安全存儲(chǔ),并且可以使用bcrypt來(lái)實(shí)現(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)行了一些驗(yàn)證。正如我們將在下面看到的,我們將使用 Joi 獲得更好的驗(yàn)證。
'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路由需要有一個(gè)路由和方法,以及一個(gè)處理程序(handler),如果要使其有用的話。在這里配置這些細(xì)節(jié)是不言而喻的,但有幾件事可能不熟悉。在底部,我們有一個(gè)用于驗(yàn)證輸入的位置,在這種情況下,我們要驗(yàn)證傳入的payload
。如果我們接受來(lái)自用戶的參數(shù),那么我們可以在鍵上指定驗(yàn)證。此驗(yàn)證來(lái)自子目錄中的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)易讀——我們希望確保每個(gè)項(xiàng)都是字符串,并且我們表示它們都是必需的。不過(guò),我們可以超越這一點(diǎn),就像我們對(duì)username
和email
所做的那樣。Joi模式有很多選項(xiàng),您可以在此處查看完整的API文檔。使用Joi設(shè)置驗(yàn)證非常棒,因?yàn)樗鼤?huì)自動(dòng)拒絕與模式內(nèi)容不匹配的任何輸入,并提供合理的錯(cuò)誤消息,而無(wú)需進(jìn)行任何配置。
路由中另一個(gè)可能不熟悉的項(xiàng)目是對(duì)象內(nèi)的pre
數(shù)組。在Hapi中,我們可以定義任意數(shù)量的先決條件函數(shù),這些函數(shù)將在到達(dá)路由處理程序之前運(yùn)行。如果我們需要對(duì)傳入的數(shù)據(jù)負(fù)載進(jìn)行一些處理,這非常有用,并且是驗(yàn)證提供給端點(diǎn)的username
和email
是否唯一以及是否已存在具有這些詳細(xì)信息的用戶的完美位置。我們將方法指向userFunctions.js
文件中的preverifyUniqueUser
。
我們可以使用pre
方法做很多事情,并且由于它們完全支持異步和并行化,因此我們可以使用許多很好的可能性來(lái)抽象路由邏輯的部分。這樣,我們的處理程序就變得非常小且更易于維護(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ù)庫(kù)中查找具有與負(fù)載中傳遞的相同用戶名或電子郵件地址的用戶,如果找到,則返回相應(yīng)的錯(cuò)誤消息。如果一切正常,則發(fā)送負(fù)載以供處理程序使用。
在上述路由中,當(dāng)用戶成功創(chuàng)建賬戶時(shí),他們的JWT會(huì)被發(fā)送回給他們。我們需要一個(gè)函數(shù)來(lái)實(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)注意到,我們?cè)谏厦娴穆酚商幚沓绦蛑心J(rèn)為 to。當(dāng)我們對(duì) JWT 進(jìn)行簽名時(shí),我們首先檢查用戶是否是管理員,如果是,我們會(huì)附加適當(dāng)?shù)姆秶?。我們還在此處指定要用作算法,并讓 JWT 在 1 小時(shí)后過(guò)期。
注意:在您自己的應(yīng)用程序中實(shí)現(xiàn)將 user 范圍附加到新創(chuàng)建的用戶的方式可能與我們?cè)诖颂巿?zhí)行的操作不同,但我們可以通過(guò)這種方法快速了解它。
現(xiàn)在,當(dāng)用戶成功注冊(cè)時(shí),將返回其 JWT。
如果我們?cè)俅螄L試保存同一個(gè)用戶,我們可以看到該函數(shù)正在工作。
稍后我們會(huì)看到如何保護(hù)不同的路由,但首先,讓我們添加一個(gè)路由,允許用戶在注冊(cè)后進(jìn)行自我認(rèn)證。我們需要一些邏輯來(lái)檢查用戶傳入的密碼是否與數(shù)據(jù)庫(kù)中存儲(chǔ)的哈希密碼匹配。如果兩者匹配,那么我們就可以向用戶頒發(fā)JWT。這是另一個(gè)可以使用某種方法的地方,我們將在此方法上附加一個(gè)新的函數(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
}
這個(gè)函數(shù)使用bcrypt
來(lái)檢查有效載荷中發(fā)送的密碼是否與數(shù)據(jù)庫(kù)中的用戶條目匹配,如果有效,則用戶對(duì)象會(huì)被發(fā)送到處理器。我們使用boom
來(lái)響應(yīng)錯(cuò)誤情況,如果遇到錯(cuò)誤,它們會(huì)冒泡到處理器。
現(xiàn)在我們的路由設(shè)置可以非常簡(jiǎn)單。
// 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
}
}
}
接下來(lái),我們需要設(shè)置驗(yàn)證規(guī)則,以便為此路由進(jìn)行Joi驗(yàn)證,但這次它的工作方式會(huì)略有不同。用戶注冊(cè)時(shí)需要提供用戶名和電子郵件,但當(dāng)他們進(jìn)行認(rèn)證時(shí),只需要提供其中之一即可。為此,我們可以使用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
方法接受我們希望嘗試的驗(yàn)證替代方案的參數(shù)。這些可以是像Joi.string()
這樣的類型檢查,或者我們可以傳遞單個(gè)對(duì)象。在這種情況下,我們傳遞了兩個(gè)對(duì)象——一個(gè)用于處理用戶名的情況,另一個(gè)用于處理電子郵件的情況。這將允許用戶使用他們的用戶名或電子郵件進(jìn)行認(rèn)證。
對(duì)于這個(gè)簡(jiǎn)單的API,我們假設(shè)只有管理員能夠獲取數(shù)據(jù)庫(kù)中所有用戶的列表。在Hapi應(yīng)用程序中使用帶作用域的JWT認(rèn)證可以輕松地實(shí)現(xiàn)細(xì)粒度的用戶訪問(wèn)控制,但目前我們僅設(shè)置兩個(gè)級(jí)別:管理員和其他用戶。請(qǐng)記住,我們?yōu)樵O(shè)置新用戶的作用域編寫了路由,默認(rèn)設(shè)置為非管理員。我們可以在處理器中臨時(shí)設(shè)置此值以獲取具有管理員訪問(wèn)權(quán)限的用戶,或者我們只需在數(shù)據(jù)庫(kù)中更改此值。請(qǐng)參閱createUseradminfalsetruerepo
,這是一個(gè)響應(yīng)請(qǐng)求的端點(diǎn),允許管理員更改其他用戶的作用域。
在為我們的一個(gè)用戶設(shè)置后,讓我們看看如何限制顯示所有用戶列表的終端節(jié)點(diǎn)的 API 訪問(wèn)。
// 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']
}
}
}
在為我們的一個(gè)用戶設(shè)置管理員權(quán)限后,讓我們看看如何限制顯示所有用戶列表的API訪問(wèn)。我們已指定此路由應(yīng)實(shí)現(xiàn)認(rèn)證策略(我們?cè)谥卸x了該策略),并且用戶必須具有管理員作用域才能訪問(wèn)該路由。如果我們檢查jwtserver.js
中的JWT,可以看到我們有一個(gè)作用域?yàn)?code>admin。
您可能想知道這是否安全。由于我們可以在調(diào)試器中檢查和更改JWT的內(nèi)容,惡意用戶是否可以更改現(xiàn)有的JWT或創(chuàng)建一個(gè)新的JWT來(lái)破壞API?請(qǐng)記住,JWT的優(yōu)點(diǎn)在于它們使用服務(wù)器上的密鑰進(jìn)行數(shù)字簽名。要修改JWT使其有效,攻擊者需要知道密鑰。只要我們有一個(gè)強(qiáng)大的私鑰,我們的JWT就是安全的。
現(xiàn)在我們已經(jīng)有了用于創(chuàng)建和驗(yàn)證用戶的終端節(jié)點(diǎn),我們可以簡(jiǎn)單地將身份驗(yàn)證策略應(yīng)用于我們喜歡的任何其他終端節(jié)點(diǎn)。
我們已經(jīng)看到了在Hapi應(yīng)用程序中將認(rèn)證應(yīng)用于單個(gè)端點(diǎn)是多么容易。我們只需將認(rèn)證策略附加到路由對(duì)象即可。但是,如果我們想為每個(gè)端點(diǎn)都應(yīng)用認(rèn)證,那么操作將變得更加簡(jiǎn)單。為此,我們只需在注冊(cè)策略時(shí)設(shè)置mode
為true
,并且可以通過(guò)將'required'
或'optional'
作為第三個(gè)參數(shù)傳遞來(lái)實(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'
將允許用戶無(wú)論是否經(jīng)過(guò)認(rèn)證都可以訪問(wèn)該路由。它們之間的區(qū)別在于,使用'optional'
時(shí),用戶的認(rèn)證數(shù)據(jù)必須有效,而使用'try'
時(shí),即使認(rèn)證數(shù)據(jù)無(wú)效,也會(huì)接受該數(shù)據(jù)。
我們已經(jīng)成功地在Hapi上實(shí)現(xiàn)了自己的認(rèn)證功能,但這只是冰山一角。為了構(gòu)建一個(gè)健壯的系統(tǒng),我們需要考慮認(rèn)證方面的許多更多細(xì)節(jié)。如果我們想支持現(xiàn)代認(rèn)證功能,如社交登錄、多因素認(rèn)證和單點(diǎn)登錄,那么自己實(shí)現(xiàn)端到端的認(rèn)證可能會(huì)非常棘手。幸運(yùn)的是,Auth0為我們提供了開(kāi)箱即用的所有這些功能(以及更多)!
使用Auth0,Hapi認(rèn)證變得非常簡(jiǎn)單。
如果您還沒(méi)有這樣做,請(qǐng)注冊(cè)您的免費(fèi)Auth0帳戶。免費(fèi)計(jì)劃為您提供7000個(gè)常規(guī)活躍用戶和兩個(gè)社交身份提供商,這對(duì)于許多實(shí)際應(yīng)用來(lái)說(shuō)已經(jīng)足夠了。
我們已經(jīng)為Hapi設(shè)置了一個(gè)認(rèn)證策略,使用上面提到的hapi-auth-jwt。現(xiàn)在,我們只需要使用我們的Auth0私鑰,而不是在.config.js中設(shè)置的簡(jiǎn)單密鑰。
// config.js
const key = 'your_auth0_secret';
module.exports = key;
現(xiàn)在,我們可以使用上面描述的任何方法來(lái)保護(hù)我們的端點(diǎn)。我們可以將認(rèn)證策略逐個(gè)應(yīng)用于每個(gè)路由,或者通過(guò)將模式設(shè)置為.required來(lái)全局設(shè)置它。
默認(rèn)情況下,Auth0 會(huì)為您存儲(chǔ)用戶數(shù)據(jù),這意味著當(dāng)用戶在您的應(yīng)用程序中進(jìn)行身份驗(yàn)證時(shí),調(diào)用不會(huì)轉(zhuǎn)到您的服務(wù)器。相反,Auth0 負(fù)責(zé)檢查用戶的憑證,并在成功登錄時(shí)向他們頒發(fā) JWT。
用戶可以通過(guò)幾種不同的方式進(jìn)行認(rèn)證并獲得JWT,但最簡(jiǎn)單的方法是使用Auth0在您的應(yīng)用程序前端提供的集中登錄頁(yè)面。我們可以輕松地將其添加到我們的項(xiàng)目中,并使用一些簡(jiǎn)單的JavaScript觸發(fā)它。
注意:Auth0為所有流行的框架提供了SDK和集成示例,您可以在文檔中查看適用于您特定項(xiàng)目的代碼示例。
首先,將庫(kù)添加到您的前端。這里指的是auth0-js
庫(kù)。
<!-- 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" />
...
接下來(lái),配置一個(gè)auth0-js
實(shí)例。
// app.js
var webAuth = new auth0.WebAuth({
domain: 'YOUR_DOMAIN',
clientID: 'YOUR_CLIENT_ID',
responseType: 'token',
redirectUri: 'YOUR_REDIRECT_URI'
});
您可以將事件監(jiān)聽(tīng)器附加到按鈕點(diǎn)擊事件上,并調(diào)用它來(lái)重定向到集中登錄頁(yè)面。一旦授權(quán),用戶將被重定向回我們的頁(yè)面,我們可以在那里獲取結(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)用戶成功登錄時(shí),他們的JWT和個(gè)人資料將保存在本地存儲(chǔ)中。
為了向您的API發(fā)出安全請(qǐng)求,只需將用戶的JWT作為標(biāo)頭附加即可。這里使用的是Authorization
標(biāo)頭。
我們上面構(gòu)建的API檢查了一個(gè)簡(jiǎn)單的作用域,這至少為我們提供了一定程度的訪問(wèn)控制。然而,我們可以通過(guò)使作用域特定于用戶應(yīng)該擁有的單個(gè)端點(diǎn)和操作(創(chuàng)建、更新等)來(lái)使作用域更加細(xì)化。使用Auth0,我們可以為用戶存儲(chǔ)任意元數(shù)據(jù),這就是我們可以存儲(chǔ)其作用域的地方。存儲(chǔ)元數(shù)據(jù)非常容易——我們可以手動(dòng)輸入,也可以創(chuàng)建管理員規(guī)則來(lái)自動(dòng)化該過(guò)程。
HapiJS是一個(gè)為Node打造的出色框架,它使得構(gòu)建API既簡(jiǎn)單又靈活。Hapi生態(tài)系統(tǒng)中的其他包,包括Joi和Boom,使得創(chuàng)建一個(gè)健壯的應(yīng)用程序變得輕而易舉,并且讓我們省去了很多繁重的工作。正如我們所見(jiàn),為Hapi實(shí)現(xiàn)JWT認(rèn)證也非常簡(jiǎn)單——我們只需要使用hapi-auth-jwt并注冊(cè)我們的認(rèn)證策略。
“HapiJS是一個(gè)為Node打造的出色框架,它使得構(gòu)建API既簡(jiǎn)單又靈活。”
你對(duì)HapiJS有什么看法?它是否是Express的一個(gè)好替代品?讓我們知道你的想法!
原文鏈接:https://auth0.com/blog/hapijs-authentication-secure-your-api-with-json-web-tokens/
使用 Axios 在 React 中創(chuàng)建集中式 API 客戶端文件
Cursor + Devbox 進(jìn)階開(kāi)發(fā)實(shí)踐:從 Hello World 到 One API
火山引擎如何接入API:從入門到實(shí)踐的技術(shù)指南
什么是聚類分析?
通過(guò)API監(jiān)控提高API穩(wěn)定性
使用 Whisper API 通過(guò)設(shè)備麥克風(fēng)把語(yǔ)音轉(zhuǎn)錄為文本
如何在 Apifox 中發(fā)布多語(yǔ)言的 API 文檔?
在 Golang 中實(shí)現(xiàn) JWT 令牌認(rèn)證
深入了解 Gateway API 的推理擴(kuò)展
對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力
一鍵對(duì)比試用API 限時(shí)免費(fèi)