令牌和签名简介
1. 令牌(Token)
概念
令牌(Token)是一个用于身份验证的小段数据,通常在用户登录时由服务器生成,并返回给客户端。客户端在后续的请求中将令牌附加到请求头中,服务器通过验证令牌来确认用户身份,并决定是否授权访问请求的资源。
常见类型
- JWT(JSON Web Token):JWT 是一种非常常见的令牌形式,它以 JSON 格式存储信息,并使用签名来确保其内容的完整性。
- OAuth 令牌:在 OAuth 2.0 授权框架中,令牌(如访问令牌、刷新令牌)用于授权和认证。
令牌的结构
以 JWT 为例,它由三个部分组成,用点号(.
)分隔:
- Header(头部):定义了令牌的类型(通常是 “JWT”)和所使用的签名算法(如 HMAC SHA256)。
{ "alg": "HS256", "typ": "JWT" }
- Payload(负载):包含声明(Claims),可以包括用户信息、令牌的颁发时间和过期时间等数据。
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
- Signature(签名):对前两个部分(头部和负载)进行签名,确保其未被篡改。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
令牌的用途
- 身份验证:用户在成功登录后获取令牌,并在每次请求时将令牌附加到请求头中(通常是
Authorization: Bearer <token>
),服务器通过验证令牌来确认用户身份。 - 授权:服务器可以通过解析令牌中的信息决定用户是否有权访问特定资源。
令牌的优势
- 无状态:服务器不需要存储用户的会话信息,因为所有的身份验证信息都包含在令牌中。
- 安全性:令牌通过签名确保其内容未被篡改,并且可以设置过期时间,限制令牌的有效期。
2. 签名(Signature)
概念
签名(Signature)是通过加密算法生成的,用于确保数据的完整性和来源的真实性。签名通常是对消息的散列值(哈希值)进行加密得到的。签名可以防止数据在传输过程中被篡改。
签名的用途
- 数据完整性:签名可以验证数据在传输过程中是否被篡改。如果接收到的数据与签名验证结果不匹配,说明数据已被修改。
- 数据来源验证:签名可以用来确认数据的来源,因为只有拥有签名密钥的实体才能生成正确的签名。
- 请求保护:在一些系统中,每次请求都会生成一个新的签名,用于保护请求数据不被篡改。
签名的生成
签名的生成过程通常包括以下步骤:
- 选择密钥和算法:签名的生成需要一个密钥和算法(如 HMAC、RSA、ECDSA 等)。
- 生成散列值:对数据(如请求的参数、时间戳等)生成散列值。
- 加密散列值:使用选择的算法和密钥对散列值进行加密,从而生成签名。
签名的验证
签名的验证过程如下:
- 提取签名:从请求或数据包中提取签名。
- 生成散列值:使用与生成签名时相同的数据生成一个新的散列值。
- 加密并比较:将生成的散列值与提取的签名进行加密并比较,如果匹配,则签名有效,数据未被篡改。
签名的应用场景
- API 请求签名:为了确保 API 请求的完整性,服务器会对每个请求生成签名,客户端发送的请求中包含这个签名,服务器验证签名是否有效。
- 消息签名:在消息传递系统中,签名用于确保消息未被篡改,并且来自可信来源。
3. 结合令牌和签名的安全机制
在一个系统中,为了确保用户身份验证和数据传输的安全性,可以结合使用令牌和签名:
- 令牌认证:每次客户端发送请求时,附带上已生成的令牌,服务器通过验证令牌确认用户身份。
- 签名认证:在访问敏感资源或请求页面时,生成一个签名并发送给服务器,服务器通过验证签名确保请求未被篡改且合法。
流程总结
- 用户登录:服务器生成令牌并返回给客户端,客户端存储令牌。
- 访问资源:客户端每次访问资源时,发送存储的令牌,并生成一个签名附加到请求中。
- 服务器验证:服务器验证令牌和签名,如果验证成功,返回资源数据,否则拒绝请求。
这种机制能够同时确保用户身份的合法性和请求数据的完整性,大大提升了系统的安全性。
使用案例
在前端使用 HTML 或 JavaScript 实现结合令牌和签名的安全验证流程,以下是一个基本的实现方案。
1. 用户登录流程
前端:
-
用户输入并提交登录信息:
<form id="loginForm"> <input type="text" id="username" placeholder="Username"> <input type="password" id="password" placeholder="Password"> <button type="submit">Login</button> </form> <script> document.getElementById('loginForm').addEventListener('submit', async function(event) { event.preventDefault(); const username = document.getElementById('username').value; const password = document.getElementById('password').value; // 向服务器发送登录请求 const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const result = await response.json(); if (result.token) { // 将令牌存储在本地 localStorage.setItem('authToken', result.token); } else { alert('Login failed!'); } }); </script>
-
服务器返回令牌并存储:
- 令牌成功返回后,存储在
localStorage
中,方便后续请求使用。
- 令牌成功返回后,存储在
2. 访问页面资源的流程
前端:
-
生成签名并发出请求:
function generateSignature(data, secretKey) { // 这里用的是一个简单的加密算法,如有需要可使用更复杂的加密逻辑 return btoa(data + secretKey); } async function fetchProtectedResource(url) { const token = localStorage.getItem('authToken'); if (!token) { alert('Not authenticated!'); return; } // 数据可以包括请求URL、时间戳等 const requestData = url + new Date().toISOString(); const secretKey = 'your_secret_key'; // 注意:实际应用中,密钥不会存在前端 const signature = generateSignature(requestData, secretKey); const response = await fetch(url, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, 'X-Signature': signature } }); const data = await response.json(); console.log(data); } // 例子:访问受保护资源 fetchProtectedResource('/api/protected-resource');
-
服务器验证请求:
- 服务器收到请求后,从请求头中获取
Authorization
和X-Signature
,然后验证令牌和签名。
- 服务器收到请求后,从请求头中获取
3. 签名的生成与验证
签名在前端生成,并随请求发送到服务器。服务器使用相同的算法和密钥验证签名。
前端:
function generateSignature(data, secretKey) {
return btoa(data + secretKey); // Base64编码
}
服务器:
function verifySignature(receivedSignature, data, secretKey) {
const expectedSignature = generateSignature(data, secretKey);
return receivedSignature === expectedSignature;
}
4. 关键点
- 令牌存储安全: 在前端使用
localStorage
或sessionStorage
存储令牌,但要注意 XSS 攻击。 - 签名的动态生成: 每次请求都动态生成签名,避免重放攻击。
- 服务器端验证: 服务器端必须验证令牌和签名的有效性,确保安全。
5. 流程总结
- 用户登录: 用户提交用户名和密码,服务器验证后返回令牌,前端存储令牌。
- 访问页面资源: 前端在发出请求前,生成签名并携带令牌,服务器验证后返回数据。
通过这种方式,结合令牌和签名,前端和服务器可以共同确保用户的身份验证和请求的完整性,增强系统的安全性。
前端生成签名时,秘钥如何获取?
在前端生成签名时,使用的密钥(secret key)不能直接存储在前端代码中,因为前端代码是公开可见的,任何用户都可以查看源代码。因此,密钥的处理需要特别注意,以确保系统的安全性。
秘钥的获取和使用方法
-
使用短期有效的临时密钥:
- 方案: 当用户成功登录后,服务器生成一个临时密钥并随令牌一起返回给客户端。这个密钥的有效期较短(例如 15 分钟或 1 小时),客户端在生成签名时使用这个临时密钥。
- 优点: 临时密钥即使被泄露,由于有效期短,攻击者的利用时间也会被大大缩短。
- 实现: 服务器生成一个随机的、加密的密钥,发送给客户端。客户端存储在
localStorage
或sessionStorage
中,并在每次请求时使用。
示例:
// 登录时获取临时密钥和令牌 localStorage.setItem('authToken', result.token); localStorage.setItem('temporaryKey', result.temporaryKey);
-
通过 API 动态获取密钥:
- 方案: 前端在需要生成签名时,通过一个专门的 API 从服务器请求密钥。服务器根据请求内容、令牌等验证信息,生成并返回一个短期有效的密钥。
- 优点: 密钥只在生成签名时短暂存储,降低了被泄露的风险。
- 实现: 每次请求前,先通过 API 获取密钥,然后立即生成签名。
示例:
async function fetchTemporaryKey() { const token = localStorage.getItem('authToken'); const response = await fetch('/api/get-temp-key', { method: 'GET', headers: { 'Authorization': `Bearer ${token}` } }); const result = await response.json(); return result.temporaryKey; } async function fetchProtectedResource(url) { const temporaryKey = await fetchTemporaryKey(); const requestData = url + new Date().toISOString(); const signature = generateSignature(requestData, temporaryKey); // 继续请求过程... }
-
使用服务器端签名:
- 方案: 前端不生成签名,而是将请求数据发送到服务器,服务器生成签名并返回。前端在后续请求中使用这个签名。
- 优点: 密钥完全保存在服务器端,安全性最高。
- 实现: 请求中包含所有需要生成签名的数据,服务器返回签名。
示例:
async function fetchSignature(data) { const token = localStorage.getItem('authToken'); const response = await fetch('/api/generate-signature', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); return result.signature; }
关键点总结
- 短期有效性: 临时密钥的有效期应足够短,以防止长期存储导致的安全风险。
- 动态获取: 密钥的动态获取可以防止密钥在前端的长期暴露。
- 服务器端控制: 最安全的方式是将签名逻辑放在服务器端处理,前端只负责发送数据和接受签名结果。
通过这些措施,可以有效减少密钥暴露的风险,同时确保请求的完整性和安全性。