telegram小程序(tma)数据验证方法 超详细!!!

最近接触到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校验通过,否则校验失败。

tg小程序数据验证流程.png

前端收到的数据:

小程序前端收到的数据.png

代码实现主要是三部分:

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);
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值