最近接触到telegram小程序后台服务,其中涉及到小程序数据验证流程。找了一圈发现网上还没有关于这个功能的java实现,所以自己写了一个验证工具,作为技术分享。
tg官网的文档 验证通过小程序收到的数据。
通过文档能知道数据验证主要是用到了HmacSha256摘要算法验证前端获取到的initData是否有效。该算法通过传入加密内容content及密钥secretKey生成摘要值,只要content及secretKey相同每次生成的hash也是一样的。
- content生成规则: 将收到的initData参数按参数名字母顺序排序后,使用k=v及换行符**“\n”**进行拼接。
- secret_key生成规则:使用HmacSha256算法以小程序机器人的botToken作为content,字符串”WebAppData“为key生成的摘要做为密钥。
- 校验结果:对比HmacSha256(content,secret_key)计算出来的calcHash值及小程序前端获取到的hash参数, 相同则表示数据来源于telegram校验通过,否则校验失败。
前端收到的数据:
代码实现主要是三部分:
1. 构建数据校验字符串 data-check-string
- buildDataMap()
前端拿到的初始化数据initData为urlEncode过的格式,需要先将原文decode,再转成map便于后续处理按参数字母顺序排序。
注意点:这里有一个坑,前端拿到的initData 包含了hash参数,需要将这个参数hash排除,否则验签不过,这里卡了我一段时间。囧( ╯□╰ )- buildDataCheckString()
构建data_check_string,将buildDataMap()得到的参数map排序后,使用k=v的方式使用换行符**”\n“**拼接
private static String buildDataCheckString(Map<String, String> dataMap) {
List<String> keys = new ArrayList<>(dataMap.keySet());
Collections.sort(keys); // 字典序排序
StringBuilder sb = new StringBuilder();
for (String key : keys) {
sb.append(key).append("=").append(dataMap.get(key)).append("\n");
}
// 移除最后一个换行符
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
}
return sb.toString();
}
private static Map<String, String> buildDataMap(String data) throws Exception {
Map<String, String> params = new HashMap<>();
String[] pairs = data.split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
if (idx > 0) {
String key = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8.name());
if (Arrays.asList("hash", "").contains(key)) {
continue;
}
String value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.name());
params.put(key, value);
}
}
return params;
}
2. HmacSha256算法实现
private static String encryptHex(String content, byte[] key) {
return Hex.encodeHexString(encrypt(content, key));
}
public static byte[] encrypt(String content, byte[] key) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "HmacSHA256");
mac.init(secretKeySpec);
return mac.doFinal(content.getBytes());
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException(e);
}
}
3. 验证方法
public static boolean validData(String data,String hash) {
try {
// 1. 构建 data-check-string
String dataCheckString = buildDataCheckString(buildDataMap(data));
// 2. 计算 secret_key = HMAC_SHA256(<bot_token>, "WebAppData")
byte[] secretKey = encrypt(botToken, "WebAppData".getBytes(StandardCharsets.UTF_8));
// 3. 计算 HMAC-SHA256(data-check-string, secret_key)
String calcHash = encryptHex(dataCheckString, secretKey);
// 4. 比较 hash 和 calcHash
return hash.equalsIgnoreCase(calcHash);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
完整代码如下:
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Hex;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
/**
* @author wds
* @DateTime: 2024/11/27 11:00
*/
@Slf4j
public class ValidUtils {
//替换为你的机器人token
public static String botToken = "xxx";
/**
* 验证tg小程序数据
* 1. 构建 data-check-string
* 2. 计算【secret_key】
* = HMAC_SHA256(<bot_token>, "WebAppData")
* 3. 计算参数【calcHash】
* = HMAC-SHA256(data-check-string, secret_key)
* 4. 比较 hash和calcHash是否一致
*
* @param data tg文档中的initData
* @param hash 收到的Hash
* @return 校验结果
*/
public static boolean validData(String data,String hash) {
try {
// 1. 构建 data-check-string
String dataCheckString = buildDataCheckString(buildDataMap(data));
// 2. 计算 secret_key = HMAC_SHA256(<bot_token>, "WebAppData")
byte[] secretKey = encrypt(botToken, "WebAppData".getBytes(StandardCharsets.UTF_8));
// 3. 计算 HMAC-SHA256(data-check-string, secret_key)
String calcHash = encryptHex(dataCheckString, secretKey);
// 4. 比较 hash 和 calcHash
return hash.equalsIgnoreCase(calcHash);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 构建数据检查字符串
* 1.按照字段名的字母顺序排序
* 2.使用换行符"\n" 连接
*
* @param dataMap 参数map
* @return 数据检查字符串 即tg文档中的data_check_string
*/
private static String buildDataCheckString(Map<String, String> dataMap) {
List<String> keys = new ArrayList<>(dataMap.keySet());
Collections.sort(keys); // 字典序排序
StringBuilder sb = new StringBuilder();
for (String key : keys) {
sb.append(key).append("=").append(dataMap.get(key)).append("\n");
}
// 移除最后一个换行符
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
}
return sb.toString();
}
/**
* 构建参数map
* 需要去除hash参数
*
* @param data tg文档中的initData
* @return 返回参数map
*/
private static Map<String, String> buildDataMap(String data) throws Exception {
Map<String, String> params = new HashMap<>();
String[] pairs = data.split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
if (idx > 0) {
String key = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8.name());
if (Arrays.asList("hash", "").contains(key)) {
continue;
}
String value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.name());
params.put(key, value);
}
}
return params;
}
/**
* 计算HMAC-SHA256 16进制值
* @param content 加密内容
* @param key 密钥 [bytes]
* @return
*/
private static String encryptHex(String content, byte[] key) {
return Hex.encodeHexString(encrypt(content, key));
}
/**
* 计算 HMAC-SHA256
* @param content 加密内容
* @param key 密钥 [bytes]
* @return
*/
public static byte[] encrypt(String content, byte[] key) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "HmacSHA256");
mac.init(secretKeySpec);
return mac.doFinal(content.getBytes());
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws Exception {
// 从 Telegram 接收到的哈希
String hash = "c6978f397xxx4f58afb770cc94475a36d2cce6b65a635a6464ba5a655545fade";
// 从 Telegram 接收到的数据
String data = "user=%7B%22id%22%3Axxxx%2C%22first_name%22%3A%22xx%22%2C%22last_name%22%3A%22xxx%22%2C%22language_code%22%3A%22zh-hans%22%2C%22allows_write_to_pm%22%3Atrue%2C%22photo_url%22%3A%22https%3A%5C%2F%5C%2Ft.me%5C%2Fi%5C%2Fuserpic%5C%2F320%5C%2FK3s4gI28di2N4c377dNAvSm55rCk-_e-0p3XtbRhopepeiUDqCBg2CmR0PeMzKDK.svg%22%7D&chat_instance=-7404315562752763869&chat_type=private&auth_date=1732677313&signature=nhoK6WbQfrinb4Adxk-vzV9ffc7Hw2BjRulbOpqEC-HeYMkRbdEBdkaFSQrVeOj3FNScMmVbYswos2zP8am0Bg&hash=c6978f397b2f4f58afb770cc94475a36d2cce6b65a635a6464ba5a655545fade";
// 验证数据
boolean isValid = validData(data, hash);
System.out.println("小程序参数校验结果: " + isValid);
}
}