JWT使用HS512算法生成全局服务token原理

本文详细介绍了如何使用HS512算法在C++中生成和验证JWT令牌,包括token的构成(头部、Payload和Signature),以及加密和签名的具体步骤。

JWT使用HS512算法生成全局服务token原理


前言

最近需要使用C++服务中根据用户名生成全局服务的token,然后在java服务中印证token正确性。因为java服务中使用jwt库生成全局的token, 我就对应找C++中jwt的库, github中有开源库叫jwt-cpp然后我就下载了, 我使用没有找到java中生成token好使用的接口、没有办法我只有看一下jwt生成token的步骤实现一遍

一、jwt生成token原理

HMACSHA512(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
your-512-bit-secret
) secret base64 encoded

jwt由三部分构成

  • 头部(Header)
  • 负载(Payload)即数据
  • 签证(Signature)

1、 jwt的头部承载两部分信息

  • 声明类型, 即jwt
  • 声明加密的算法 (HMAC、SHA256, HS512)

头部json格式

{"typ“: "JWT", "alg":"HS512"}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

2、Payload数据

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

标准中注册的声明 (建议但不强制使用) :

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明 :

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

私有的声明 :

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:

{  "sub": "1234567890",  "name": "John Doe",  "admin": true}

然后将其进行base64加密,得到Jwt的第二部分

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

3、signature

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串(头部在前),然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

UQmqAUhUrpDVV2ST7mZKyLTomVfg7sYkEjmdDI5XF8Q

密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和验证,所以需要保护好。

4、 签名的目的

最后一步签名的过程,实际上是对头部以及载荷内容进行签名。一般而言,加密算法对于不同的输入产生的输出总是不一样的。对于两个不同的输入,产生同样的输出的概率极其地小(有可能比我成世界首富的概率还小)。所以,我们就把“不一样的输入产生不一样的输出”当做必然事件来看待吧。
所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。
服务器应用在接受到JWT后,会首先对头部和载荷的内容用同一算法再次签名。那么服务器应用是怎么知道我们用的是哪一种算法呢?别忘了,我们在JWT的头部中已经用alg字段指明了我们的加密算法了。
如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个Token,返回一个HTTP 401 Unauthorized响应。

二、 JWT的HS512算法生成token的C++语言实现

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/hmac.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <iostream>
namespace chen {


	namespace jwt {
	

		//java 从最后 删除'='个数  url-unfriendly base64 char u
		int remove_padding( char*  data, size_t len )
		{
			int paddingCount = 0;
			//int encoded_payload_len = strlen(encoded_payload);
			for (int i = len - 1; i > 0; --i)
			{
				if (data[i] == '=')
				{
					++paddingCount;
				}
				else
				{
					break;
				}
			}
			//replace URL-unfriendly Base64 chars to url-friendly ones:
			for (int i = 0; i < len; ++i)
			{
				if (data[i] == '+')
				{
					data[i] = '-';
				}
				else if (data[i] == '/')
				{
					data[i] = '_';
				}
			}

			return len - paddingCount;
		}



		std::string create_token( const std::string& secret_key,  const std::string& payload)
		{
			// 构建头部
			static const char* header = "{\"alg\":\"HS512\"}";
			char buffer[10240] = {0};

			// 计算 Base64 编码后头部的长度
			int header_len = EVP_ENCODE_LENGTH(strlen(header));

			// 计算 Base64 编码后负载的长度
			int payload_len =   payload.length();// = EVP_ENCODE_LENGTH(strlen(payload));

			// 分配内存存储 Base64 编码后的头部和负载
			 
			char  encoded_header[4096] = {0};
			char  encoded_payload[4096] = {0};
			 
			// Base64 编码头部和负载
			EVP_EncodeBlock((unsigned char*)encoded_header , (const unsigned char*)header, strlen(header));
			EVP_EncodeBlock((unsigned char*)encoded_payload , (const unsigned char*)payload.c_str(), payload.length());

 


			// 构建待签名字符串
			int unsigned_data_len = remove_padding( encoded_header , strlen(encoded_header) ) + remove_padding(encoded_payload, strlen(encoded_payload) ) + 2; // 加上两个点号
			 
			snprintf(buffer, unsigned_data_len, "%s.%s", std::string(encoded_header, remove_padding((char*)encoded_header , strlen(encoded_header) )).c_str(), std::string(encoded_payload, remove_padding((char*)encoded_payload ,  strlen(encoded_payload))).c_str());
			 
			// 使用 HMAC SHA-512 对待签名字符串进行签名
			signed char hmac_result[EVP_MAX_MD_SIZE];// = { 0 };
			unsigned int hmac_len;// = 0;

			HMAC(EVP_sha512(), secret_key.c_str(), secret_key.length(), (const unsigned char*)buffer, strlen(buffer), (unsigned char*)hmac_result, &hmac_len);

			// Base64 编码签名结果
			int signature_len = EVP_ENCODE_LENGTH(hmac_len);
			char encoded_signature[4096 * 2] = {0}; 
			EVP_EncodeBlock((unsigned char*)encoded_signature , (unsigned char*)hmac_result, hmac_len);

			// 构建 JWT token
			int token_len = unsigned_data_len + remove_padding((char *)encoded_signature , strlen(encoded_signature) ) + 1; // 加上两个点号和结尾的 null 字符
			 
			 snprintf(buffer, token_len, "%s.%s.%s", std::string(encoded_header, remove_padding((char*)encoded_header, strlen(encoded_header))).c_str(), std::string(encoded_payload, remove_padding((char*)encoded_payload, strlen(encoded_payload))).c_str(), std::string(encoded_signature, remove_padding((char*)encoded_signature, strlen(encoded_signature))).c_str());

			 
		
			return buffer;
		}
		void test_create_token()
		{
			std::string key = "chensong";
			std::string payload = "{\"chensong\":\"{\\\"userId\\\":18}\",\"sub\":\"chensong\"}";
			std::string token = create_token(key, payload);
			std::cout << "jwt token = " << token << std::endl;
		}
	}
}

总结

JWT的HS512算法实现源码地址:

<think>首先,用户的问题是:如何解决使用JwtService生成refreshToken时因时间精确到秒导致重复的问题。具体来说,问题是由于时间精确到秒,导致生成的refreshToken可能重复。 关键点: - 使用JwtService(可能是JSON Web Token服务生成refreshToken。 - 时间精确到秒,这可能导致多个token在同一秒内生成,从而产生重复。 - 用户想解决这个问题。 从系统指令: - 所有行内数学表达式用$...$格式。 - 独立公式用$$...$$$$格式。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 - 回答结构清晰,帮助用户逐步解决问题。 参考站内引用: - [^1]: 提到JWT自带用户信息,不需要持久化。 - [^2]: 解释access token和refresh token,access token有效期短,refresh token用于获取新的access token。 - [^3]: 描述了refresh token的工作流程。 这些引用是关于JWTtoken机制的,但用户的问题是关于时间精度导致重复的问题。所以,我需要基于这些上下文来回答,但焦点是解决重复问题。 核心问题:时间精确到秒导致refreshToken重复。refreshToken通常有较长的有效期,如果生成使用秒级时间戳,可能在同一秒内生成多个token,导致冲突。 常见解决方案: 1. **增加随机性**:在token生成中加入随机数或唯一标识符,确保每个token是唯一的。 2. **使用更高精度时间**:使用毫秒级或微秒级时间戳,减少冲突概率。 3. **序列号或计数器**:引入一个序列号,每次生成token时递增。 4. **数据库检查**:在保存token时检查是否已存在,如果存在则重新生成(但可能增加延迟)。 在JWT的上下文中,refreshToken通常是一个JWT,包含claims(声明)如exp(过期时间)、iat(签发时间)等。如果iat精确到秒,且在同一秒内签发多个token,它们可能有相同的iat,导致冲突。 如何避免: - 在JWT的payload中添加一个唯一标识符,如jti(JWT ID),这是一个唯一字符串。 - 使用更精细的时间戳,例如毫秒。 在实现中,JwtService可能允许自定义claims。所以,建议在生成refreshToken时添加一个唯一值。 步骤式回答: 1. **理解问题**:解释为什么时间精确到秒会导致重复。 2. **解决方案**:提供具体方法。 3. **代码示例**:如果可能,给出伪代码或代码片段。 4. **注意事项**:讨论潜在问题。 生成相关问题:在回答后,添加相关问题部分。 回答结构: - 开头确认问题。 - 逐步解释解决方案。 - 添加引用标识(如果适用)。 - 最后,生成相关问题。 引用标识:由于用户提供了引用[^1], [^2], [^3],我需要自然地融入它们。例如: - 引用[^2]提到refresh token用于获取access token,有效期较长。 - 在讨论refreshToken时,可以引用这些。 但用户的问题是关于技术实现,所以引用可能不是直接相关。我可以这样引用: - "正如引用[^2]所述,refresh token用于安全地获取新的access token,因此确保其唯一性至关重要。" - 然后讨论解决方案。 最终回答: - 用中文。 - 清晰结构。</think>### 解决JwtService生成refreshToken因时间精确到秒导致重复的问题 在使用JwtService生成refreshToken时,如果时间戳(如`iat`签发时间)精确到秒,可能在同一秒内生成多个token,导致重复问题。这是因为秒级时间戳无法区分毫秒级事件,尤其在并发请求或高负载场景下,多个token可能共享相同的时间戳值,从而引发冲突(例如,在token验证或存储中出现重复键错误)。refresh token通常具有较长的有效期(如引用[^2]所述,refresh token用于安全获取新的access token,有效期较长),因此确保其唯一性至关重要。下面我将逐步解释问题原因和解决方案,帮助您彻底解决这个问题。 #### 问题原因分析 - **时间戳精度不足**:JWT的payload中通常包含`iat`(签发时间)字段,默认使用Unix时间戳(精确到秒)。如果在同一秒内生成多个refreshToken,`iat`值相同,可能导致token内容重复。 - **缺少唯一标识**:如果token生成仅依赖时间戳和固定数据(如用户ID),而没有随机或唯一元素,重复概率增加。 - **潜在影响**:重复refreshToken可能导致: - 安全风险:攻击者可能利用重复token进行重放攻击。 - 系统错误:数据库存储时出现主键冲突(例如,如果token作为唯一键)。 - 用户体验问题:用户会话中断或重新认证失败。 正如引用[^1]提到的,JWT自带用户信息,不需要授权服务持久化,但这也意味着生成token时必须内置唯一性机制,以避免依赖外部存储。 #### 解决方案:确保refreshToken唯一性 以下是几种可靠的方法,按推荐顺序列出。核心思路是**增加随机性或更高精度时间戳**,确保每个refreshToken独一无二。您可以根据技术栈(如Spring Security、Node.js等)选择实现。 1. **添加唯一标识符(推荐方法)** 在JWT的payload中引入一个唯一字符串,如`jti`(JWT ID)。`jti`是一个标准JWT claim,用于防止重放攻击。生成时,使用随机UUID或纳秒级时间戳组合用户信息。 - **为什么有效**:`jti`作为唯一键,即使`iat`相同,也能保证token不同。 - **实现步骤**: - 在生成refreshToken时,使用UUID库(如Java的`java.util.UUID`)创建`jti`值。 - 将`jti`添加到JWT payload中。 - 在验证token时,检查`jti`是否已使用(可选,可通过缓存或数据库实现)。 - **代码示例(伪代码,基于Java Spring)**: ```java import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; import java.util.UUID; public String generateRefreshToken(String userId) { // 生成唯一jti String jti = UUID.randomUUID().toString(); // 构建JWT,添加jti和毫秒级时间戳(减少冲突) return Jwts.builder() .setSubject(userId) .setId(jti) // 设置jti claim .setIssuedAt(new Date()) // iat使用毫秒级精度 .setExpiration(new Date(System.currentTimeMillis() + refreshTokenValidity)) // 设置较长有效期 .signWith(SignatureAlgorithm.HS512, secretKey) // 使用密钥签名 .compact(); } ``` - **说明**:`setIssuedAt`使用`Date`对象,其内部是毫秒级时间戳(Java的`System.currentTimeMillis()`),这比秒级精度高1000倍,大大降低冲突概率。结合`jti`,重复概率几乎为零。 2. **提升时间戳精度到毫秒或微秒** 如果无法添加`jti`,直接提升时间戳精度是最简单的方法。JwtService通常支持毫秒级时间戳(如JavaScript的`Date.now()`或Python的`time.time() * 1000`)。 - **为什么有效**:毫秒级精度将冲突窗口从1秒缩小到1毫秒,显著减少重复。 - **实现步骤**: - 在生成token时,使用高精度时间函数替换秒级时间戳。 - 确保JWT库支持毫秒级`iat`(大多数库如jjwt或jsonwebtoken自动处理)。 - **注意事项**:在高并发系统(如每秒请求量 >1000)中,毫秒级仍可能有冲突,因此建议与随机数结合。例如: - 在payload中添加一个随机数:`"nonce": Math.random()`(JavaScript)或`ThreadLocalRandom.current().nextInt()`(Java)。 3. **引入序列号或计数器** 对于分布式系统,使用全局唯一序列号(如Snowflake算法)作为payload的一部分。 - **为什么有效**:序列号确保递增唯一性,不受时间限制。 - **实现步骤**: - 在token服务中维护一个计数器(例如,使用Redis或数据库自增键)。 - 将计数器值嵌入payload(如`"seq": atomicIncrement()`)。 - 验证时检查序列号有效性。 - **优点**:适合超高并发场景,但增加了系统复杂性(需要共享存储)。 4. **数据库去重检查(后备方案)** 在保存refreshToken前,检查是否已存在相同token(基于payload哈希值)。 - **为什么有效**:作为安全网,捕获潜在重复。 - **实现步骤**: - 生成token后,计算其SHA-256哈希值。 - 查询数据库或缓存,如果哈希值存在,则重新生成token。 - 示例SQL检查:`SELECT COUNT(*) FROM tokens WHERE hash = ?`。 - 如果使用引用[^3]的架构,refresh token由独立服务处理,可以在该服务中添加此检查。 - **缺点**:增加延迟和负载,只作为辅助手段。 #### 最佳实践建议 - **优先使用`jti` + 毫秒级时间戳**:这是最平衡的方案,无需额外存储,且符合JWT标准(RFC 7519)。确保在生成token时,始终包含唯一元素。 - **并发处理**:在高并发下,使用线程安全方法(如Java的`AtomicLong`或分布式锁)。 - **安全考虑**:refresh token有效期较长(引用[^2]强调其安全性),因此生成使用强随机源(避免`Math.random()`,改用加密安全库如`SecureRandom`)。 - **测试验证**:通过单元测试模拟高并发(如用1000个线程生成token),检查重复率。工具如JMeter可帮助压力测试。 - **性能影响**:以上方案CPU开销低(纳秒级),不影响服务响应。 通过以上方法,您可以彻底解决时间精度导致的重复问题。如果您的JwtService库有特定限制,请提供更多细节,我可以给出更针对性的代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值