使用 JWT 处理身份验证
JSON Web Token(JWT)是一种以 JSON 对象形式安全传输信息的方法。这些信息使用密钥进行数字签名,可以验证和信任。
许多应用程序在身份验证成功后使用 JWT 来识别客户端,以便发出后续请求。
令牌仅在服务器端(身份验证成功时)创建,通常包含与用户身份相关的信息。在服务器上,此信息使用密钥进行签名,并且可以在客户端上进行验证或检测是否更改。这可以防止攻击者篡改用户特征和身份。例如,如果服务器对有效载荷进行签名{ right: 'read' }
并发送给客户端,它期望收到相同的信息以验证相同的有效载荷。因此,如果您将有效载荷更改为其他内容{ right: 'write' }
并发送回服务器,服务器将检测到更改并拒绝该请求。
JWT 的结构
JWT 由三部分编码信息组成,用点分隔:header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. // header
eyJzdWIiOiIxMjM0NSIsInJvbGUiOiJhZG1pbiJ9. // payload
bi_wAbm4vOKxM8zjDYEeiseRPfKtum_7S2H-DmpDDwg // signature
标头信息包含令牌的类型(即 JWT)以及用于编码的算法,例如 HMAC、SHA 256 或 RSA。
因此,如果对标头进行编码,{ alg: 'HSA256', typ: 'JWT' }
我们将获得eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
。
有效载荷包含以 Base64Url 编码的关于用户身份或特征的附加信息,例如识别号、角色、发行或到期日期。但请勿将敏感信息包含在有效载荷中,因为使用窗口atob()
方法解码 Base64 编码的字符串可以轻松检索这些信息。尝试通过atob('eyJzdWIiOiIxMjM0NSIsInJvbGUiOiJhZG1pbiJ9')
在浏览器控制台中写入来解码上述示例中的有效载荷,您将获得包含附加数据的已解析对象。
签名由编码后的标头、有效载荷和密钥串联而成。然后,我们使用标头中指定的算法对串联信息进行编码,并获取签名。签名用于验证消息在传输过程中是否未被篡改。
密钥
JWT 的安全性取决于用于签名令牌的密钥的强度。理想情况下,密钥应该是唯一的、强健的,至少 64 个字符,由加密安全函数生成,尽可能随机。
如果攻击者能够获取有效的 JWT,他们可以尝试通过离线攻击破解密钥。如果成功,他们将能够修改令牌并使用获取的密钥再次签名。
此外,如果所有令牌都使用相同的密钥签名,并且被攻击者破解,这将危及其他用户帐户。
为了充分利用密钥,一个想法是为每次身份验证创建唯一的密钥。这可以通过将一段经过哈希处理的用户密码与一个随机生成的常量密钥连接起来来实现。
客户端上的存储
通常,JWT 存储在浏览器的 Cookies 或 localStorage 容器中。这两种方式都非常方便,因为浏览器每次向服务器发送请求时都会自动发送 Cookies,而 localStorage 容器中的令牌没有有效期限制,除非您手动操作。
然而,Cookies 或 localStorage 中的令牌可能会被 XSS 攻击获取。
为了充分利用这一点,建议将 JWT 存储在 sessionStorage 容器中。它与 localStorage 类似,不同之处在于它会为每个浏览器和选项卡单独创建会话,并且在关闭浏览器后会话也会被清除。
会话存储也容易受到 XSS 攻击,但它具有时间限制,并且与浏览器的单个选项卡隔离,这使得访问令牌更加困难。
此外,还需要考虑其他安全措施:
- 将令牌作为 Bearer HTTP
Authentication
标头添加到对服务器的所有请求中 - 添加
fingerprint
到令牌(随机生成的字符串;作为原始文本添加到 Cookies 中,并将散列版本添加到令牌中)
实施示例
我将使用axios
库作为浏览器和 Node.js HTTP 客户端,并利用库来处理 JWT。始终使用受信任的 JWT 库。您可以在www.jwt.iojasonwebtoken
上找到 JWT 库列表。
认证成功后存储令牌的代码:
function handleAuthentication() {
axios
.post('/authenticate', {
email: 'test@example.com',
password: 'test'
})
.then(function(res) {
if (res.status === 200) {
sessionStorage.setItem('token', res.data.token);
} else {
sessionStorage.removeItem('token');
}
})
.catch(function(error) {
sessionStorage.removeItem('token');
});
}
服务端认证并生成JWT的代码:
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const SECRET_KEY = 'strongUniqueAndRandom';
function authenticate(req, res) {
const { password } = req.data;
const isValid = bcrypt.compareSync(password, hashedUserPasswordFromDb);
if (isValid) {
const payload = {
sub: '1234', // user identifying information, such as an Id from database
iat: new Date().getTime()
};
const token = jwt.sign(payload, SECRET_KEY);
res.status(200).json({ token });
}
}
将 JWT 包含为标头的代码Authentication
:
function handleTokenValidation() {
const token = sessionStorage.getItem('token');
const config = {
headers: {
Authorization: `Bearer ${token}`
}
};
axios
.post('/validate', {}, config)
.then(function(response) {
// do something if response is valid
})
.catch(function(error) {
// handle failed token validation
// navigate user to login page
});
}
服务端验证JWT的代码:
const jwt = require('jsonwebtoken');
const SECRET_KEY = 'strongUniqueAndRandom';
function validate(req, res, next) {
const bearer = req.headers.authorization;
const [, token] = bearer.split(' ');
const payload = jwt.verify(token, SECRET_KEY);
// If payload was decoded, that means the token was valid
// Further payload validation can be done to identify user
if (!!payload) {
res.json(true);
}
// ...
}