Press "Enter" to skip to content

告别裸奔的URL参数:使用数字签名打造防篡改的安全链接

在Web开发中,我们经常会遇到这样的场景:后端生成一个链接,并将一些关键信息作为GET参数拼接在URL上,然后由前端的静态页面来解析和展示这些信息。

一个典型的例子就是:
https://your-domain.com/show-info.html?phone=13800138000

这个页面会读取phone参数,并将其显示给用户。但这里存在一个严重的安全隐患:任何人都可以手动修改URL中的phone参数

https://your-domain.com/show-info.html?phone=我是骗子电话155...

如果这个链接被恶意传播,用户就会在你的域名下看到一个完全错误、甚至具有误导性的信息,这对你的产品信誉是巨大的打击。

那么,如何确保URL参数是由你的后端生成的,且在传递过程中没有被篡改呢?答案就是数字签名(Digital Signature)

方案:后端签名,前端验证

数字签名的工作流程完美地解决了“防篡改”和“身份验证”的需求。

  • 后端(签名方):使用私钥对数据(或其摘要)进行签名,就像给文件盖上一个独一无二的、无法伪造的公章。
  • 前端(验证方):使用公钥来验证这个签名。公钥是公开的,任何人都可以用它来检验公章的真伪。

如果数据被篡改,或者签名是伪造的(因为坏人没有你的私-钥),验证就会失败。

工作流程

  1. 后端
    • 获取原始数据(如手机号 13800138000)。
    • 使用只有后端知道的私钥,为这个手机号生成一个数字签名。
    • 原始数据签名一起作为参数,构建URL。
  2. 前端
    • 从URL中解析出原始数据签名
    • 使用预置在代码中的公钥,验证这个签名是否与原始数据匹配。
    • 如果验证通过,则渲染数据;如果失败,则显示错误提示。

实战演练:Java后端与JavaScript前端

第一步:后端(Java)生成带签名的URL

我们需要使用java.security包来处理密钥和签名。

关键点

  • 在生产环境中,密钥对应当持久化存储(例如,从密钥库文件.keystore或安全的配置中心加载),绝不能在代码中硬编码或每次运行时重新生成。
  • 生成的签名(Base64格式)包含+///=等特殊字符,放入URL前必须进行URL编码
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Base64;

public class UrlSigner {

    // --- 密钥管理 ---
    // 在真实项目中,私钥和公钥应从安全的地方加载。
    // 为了演示,我们在这里生成一次。
    private static PrivateKey privateKey;
    private static PublicKey publicKey;

    static {
        try {
            KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
            keyGen.initialize(2048);
            KeyPair keyPair = keyGen.generateKeyPair();
            privateKey = keyPair.getPrivate();
            publicKey = keyPair.getPublic();

            // !! 重要:将这个公钥字符串提供给前端JS代码
            String publicKeyBase64 = Base64.getEncoder().encodeToString(publicKey.getEncoded());
            System.out.println("请将此公钥配置到你的前端JS代码中:");
            System.out.println(publicKeyBase64);

        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("密钥对生成失败", e);
        }
    }

    /**
     * 生成带有签名的安全URL
     * @param baseUrl 静态页面的基础URL
     * @param data 要签名和传递的数据
     * @return 完整的、防篡改的URL
     */
    public static String generateSignedUrl(String baseUrl, String data) throws Exception {
        // 1. 创建Signature对象,指定签名算法 (SHA256withRSA是常用且安全的选择)
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(privateKey);

        // 2. 提供要签名的数据
        signature.update(data.getBytes(StandardCharsets.UTF_8));

        // 3. 生成数字签名
        byte[] digitalSignature = signature.sign();

        // 4. 将签名转换为对URL友好的Base64字符串
        String signatureBase64 = Base64.getEncoder().encodeToString(digitalSignature);

        // 5. 对URL参数进行编码,防止特殊字符干扰
        String encodedData = URLEncoder.encode(data, StandardCharsets.UTF_8);
        String encodedSignature = URLEncoder.encode(signatureBase64, StandardCharsets.UTF_8);

        // 6. 拼接最终的URL
        return String.format("%s?data=%s&signature=%s", baseUrl, encodedData, encodedSignature);
    }

    public static void main(String[] args) throws Exception {
        String baseUrl = "https://your-domain.com/static-page.html";
        String phoneNumber = "13800138000";

        String signedUrl = generateSignedUrl(baseUrl, phoneNumber);
        System.out.println("n生成的安全URL:");
        System.out.println(signedUrl);
    }
}

运行后,你会得到一个公钥字符串和一个安全的URL,类似这样:
https://your-domain.com/static-page.html?data=13800138000&signature=...一长串经过URL编码的签名...

第二步:前端(JavaScript)验证并展示数据

前端页面需要使用现代浏览器的 Web Cryptography API (crypto.subtle) 来执行验证。

关键点

  • 将后端提供的公钥硬编码在JS文件中是安全的,因为公钥本身就是公开的。
  • 优雅地处理验证成功和失败两种情况,给用户明确的反馈。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>手机号信息查询 - Demo页面</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            margin: 0;
            padding: 20px;
            min-height: 100vh;
        }
        
        .container {
            max-width: 600px;
            margin: 50px auto;
            background: white;
            border-radius: 12px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.1);
            overflow: hidden;
        }
        
        .header {
            background: linear-gradient(45deg, #4CAF50, #45a049);
            color: white;
            padding: 30px;
            text-align: center;
        }
        
        .header h1 {
            margin: 0;
            font-size: 24px;
            font-weight: 300;
        }
        
        .content {
            padding: 30px;
        }
        
        .phone-info {
            background: #f8f9fa;
            padding: 25px;
            border-radius: 8px;
            margin: 20px 0;
            border-left: 4px solid #4CAF50;
        }
        
        .phone-number {
            font-size: 32px;
            font-weight: bold;
            color: #333;
            text-align: center;
            margin-bottom: 10px;
            letter-spacing: 2px;
        }
        
        .phone-label {
            text-align: center;
            color: #666;
            font-size: 14px;
            margin-top: 10px;
        }
        
        .status {
            padding: 15px;
            border-radius: 6px;
            margin: 15px 0;
            font-weight: 500;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        
        .status.success { 
            background: #d4edda; 
            color: #155724; 
            border: 1px solid #c3e6cb;
        }
        
        .status.error { 
            background: #f8d7da; 
            color: #721c24; 
            border: 1px solid #f5c6cb;
        }
        
        .status.warning { 
            background: #fff3cd; 
            color: #856404; 
            border: 1px solid #ffeaa7;
        }
        
        .loading {
            text-align: center;
            padding: 40px;
            color: #666;
        }
        
        .spinner {
            border: 3px solid #f3f3f3;
            border-top: 3px solid #4CAF50;
            border-radius: 50%;
            width: 30px;
            height: 30px;
            animation: spin 1s linear infinite;
            margin: 0 auto 15px;
        }
        
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        
        .test-urls {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 8px;
            margin-top: 30px;
            border: 1px solid #dee2e6;
        }
        
        .test-urls h3 {
            margin-top: 0;
            color: #333;
        }
        
        .url-example {
            background: white;
            padding: 10px;
            border-radius: 4px;
            margin: 8px 0;
            font-family: monospace;
            font-size: 12px;
            word-break: break-all;
            border: 1px solid #ddd;
        }
        
        .url-example a {
            color: #007bff;
            text-decoration: none;
        }
        
        .url-example a:hover {
            text-decoration: underline;
        }
        
        .footer {
            text-align: center;
            padding: 20px;
            background: #f8f9fa;
            color: #666;
            font-size: 12px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>📱 手机号信息查询</h1>
            <p style="margin: 10px 0 0 0; opacity: 0.9;">数字签名验证演示</p>
        </div>
        
        <div class="content">
            <div id="loading" class="loading">
                <div class="spinner"></div>
                正在验证参数...
            </div>
            <div id="result" style="display: none;"></div>
            
            <!-- 测试URL示例 -->
            <div class="test-urls">
                <h3>🧪 测试URL示例</h3>
                <p style="color: #666; font-size: 14px;">以下是一些测试URL,您可以复制并修改参数进行测试:</p>
                
                <div class="url-example">
                    <strong>简单模式:</strong><br>
                    <a href="?phone=4008286666">?phone=4008286666</a>
                </div>
                
                <div class="url-example">
                    <strong>安全模式(需要有效签名):</strong><br>
                    <a href="?data=4008286666&signature=testSignature">?data=4008286666&signature=testSignature</a>
                </div>
                
                <div class="url-example">
                    <strong>手机号示例:</strong><br>
                    <a href="?phone=13800138000">?phone=13800138000</a>
                </div>
                
                <div class="url-example">
                    <strong>无效参数测试:</strong><br>
                    <a href="?invalid=test">?invalid=test</a>
                </div>
            </div>
        </div>
        
        <div class="footer">
            Demo页面 | 统一消息推送服务 | v1.0
        </div>
    </div>

    <script>
        class PhoneQueryHandler {
            constructor() {
                // RSA公钥(Base64格式,与后端配置保持一致)
                this.publicKeyBase64 = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyVL2Ch4qBvICMBeS3XQDEK4A3R1l3qH9quqA+y+K7y1UC+G2wT5iC1ktyGVlPZKKOnhCuimwJgpuPMUoAO53HfuhIalXhO4+YFqdsmJfLpEdXr79Dl7idViWHuW5x3dEQYbgYteXc8HIWl0rK+kJGC3HRHvh7aBw08OS6tD5oOEncfg9jonvY3uUFlyLxil7CyYMj2GspBQonO7Kt5at9UBGqtqqaEaPnQzjkARxdC3u85+pMMr0lByIY896CEJKNYeDOToAYs1bJMhEU53xNvVPUERLHP5+sDt7bA7iwD7RT5SzWZ8zs9Oe9oIiVE7HIUKOHu86q2CZqm/lbzOC8QIDAQAB";
                
                this.init();
            }

            // 辅助函数:将Base64字符串转为ArrayBuffer
            base64ToArrayBuffer(base64) {
                const binaryString = window.atob(base64);
                const len = binaryString.length;
                const bytes = new Uint8Array(len);
                for (let i = 0; i < len; i++) {
                    bytes[i] = binaryString.charCodeAt(i);
                }
                return bytes.buffer;
            }

            async init() {
                try {
                    const urlParams = new URLSearchParams(window.location.search);
                    const result = await this.parseUrlParameters(urlParams);
                    
                    setTimeout(() => {
                        this.hideLoading();
                        this.displayResult(result.phoneNumber, result.isSecureMode, result.isValid);
                    }, 1000); // 模拟验证过程
                    
                } catch (error) {
                    console.error('页面初始化失败:', error);
                    setTimeout(() => {
                        this.hideLoading();
                        this.showError('页面初始化失败,请检查URL参数');
                    }, 1000);
                }
            }

            async parseUrlParameters(urlParams) {
                const data = urlParams.get('data');
                const signature = urlParams.get('signature'); 
                const phone = urlParams.get('phone');

                if (data && signature) {
                    console.log('🔐 检测到安全模式URL,开始验证签名...');
                    const isValid = await this.verifySignature(data, signature);
                    return {
                        phoneNumber: data,
                        isSecureMode: true,
                        isValid: isValid
                    };
                } else if (phone) {
                    console.log('📱 检测到简单模式URL');
                    return {
                        phoneNumber: phone,
                        isSecureMode: false,
                        isValid: true
                    };
                } else {
                    throw new Error('无效的URL参数格式');
                }
            }

            async verifySignature(data, signatureBase64) {
                try {
                    console.log('🔍 开始验证签名...');
                    console.log('数据:', data);
                    console.log('签名:', signatureBase64.substring(0, 20) + '...');

                    // 检查Web Crypto API支持
                    if (!crypto || !crypto.subtle) {
                        console.error('Web Crypto API不支持');
                        return false;
                    }

                    // URL解码签名(如果需要)
                    const decodedSignature = decodeURIComponent(signatureBase64);

                    // 导入公钥
                    const publicKeyBuffer = this.base64ToArrayBuffer(this.publicKeyBase64);
                    const publicKey = await crypto.subtle.importKey(
                        "spki", // SubjectPublicKeyInfo format, Java X.509 编码的公钥标准格式
                        publicKeyBuffer,
                        { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
                        false, // is extractable
                        ["verify"] // key usage
                    );

                    // 准备要验证的数据和签名
                    const signatureBuffer = this.base64ToArrayBuffer(decodedSignature);
                    const dataBuffer = new TextEncoder().encode(data);

                    // 核心:验证签名
                    const isValid = await crypto.subtle.verify(
                        "RSASSA-PKCS1-v1_5", // 必须与Java后端签名算法一致
                        publicKey,
                        signatureBuffer,
                        dataBuffer
                    );
                    
                    console.log('✅ 签名验证结果:', isValid);
                    return isValid;
                    
                } catch (error) {
                    console.error('❌ 签名验证异常:', error);
                    return false;
                }
            }

            displayResult(phoneNumber, isSecureMode, isValid) {
                const resultDiv = document.getElementById('result');
                
                let statusHtml = '';
                let phoneHtml = '';
                
                if (isSecureMode) {
                    if (isValid) {
                        statusHtml = '<div class="status success">✅ 签名验证成功,参数未被篡改</div>';
                        phoneHtml = `
                            <div class="phone-info">
                                <div class="phone-number">${this.formatPhoneNumber(phoneNumber)}</div>
                                <div class="phone-label">🔒 安全验证已通过</div>
                            </div>
                        `;
                    } else {
                        statusHtml = '<div class="status error">❌ 签名验证失败,参数可能被篡改</div>';
                        phoneHtml = '<div class="phone-info" style="text-align: center; color: #999;">⚠️ 无法显示手机号信息</div>';
                    }
                } else {
                    statusHtml = '<div class="status warning">⚠️ 简单模式,未启用签名验证</div>';
                    phoneHtml = `
                        <div class="phone-info">
                            <div class="phone-number">${this.formatPhoneNumber(phoneNumber)}</div>
                            <div class="phone-label">📱 手机号信息</div>
                        </div>
                    `;
                }
                
                resultDiv.innerHTML = statusHtml + phoneHtml;
                resultDiv.style.display = 'block';
            }

            formatPhoneNumber(phoneNumber) {
                if (!phoneNumber) return '无';
                
                // 手机号脱敏处理
                if (phoneNumber.length === 11 && phoneNumber.startsWith('1')) {
                    return phoneNumber.replace(/(d{3})d{4}(d{4})/, '$1****$2');
                }
                
                // 400号码等其他格式直接显示
                return phoneNumber;
            }

            hideLoading() {
                const loadingDiv = document.getElementById('loading');
                if (loadingDiv) {
                    loadingDiv.style.display = 'none';
                }
            }

            showError(message) {
                const resultDiv = document.getElementById('result');
                resultDiv.innerHTML = `<div class="status error">❌ ${message}</div>`;
                resultDiv.style.display = 'block';
            }
        }

        // 页面加载完成后直接初始化(使用Web Crypto API,无需等待第三方库)
        document.addEventListener('DOMContentLoaded', () => {
            new PhoneQueryHandler();
        });
    </script>
</body>
</html> 
    

方案二:备选方案 – 使用纯JS库 (兼容HTTP,但有安全隐患)

如果在无法部署HTTPS的特殊场景下,仍需实现验证功能,可以引入纯JavaScript实现的密码学库,如 jsrsasign

重要提示:此方案虽然能工作,但存在严重的安全风险,不推荐用于生产环境。

首先,在HTML中引入 jsrsasign 库:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>手机号信息查询 - jsrsasign版本</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            margin: 0;
            padding: 20px;
            min-height: 100vh;
        }
        
        .container {
            max-width: 600px;
            margin: 50px auto;
            background: white;
            border-radius: 12px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.1);
            overflow: hidden;
        }
        
        .header {
            background: linear-gradient(45deg, #ff6b6b, #ee5a52);
            color: white;
            padding: 30px;
            text-align: center;
        }
        
        .header h1 {
            margin: 0;
            font-size: 24px;
            font-weight: 300;
        }
        
        .content {
            padding: 30px;
        }
        
        .phone-info {
            background: #f8f9fa;
            padding: 25px;
            border-radius: 8px;
            margin: 20px 0;
            border-left: 4px solid #ff6b6b;
        }
        
        .phone-number {
            font-size: 32px;
            font-weight: bold;
            color: #333;
            text-align: center;
            margin-bottom: 10px;
            letter-spacing: 2px;
        }
        
        .phone-label {
            text-align: center;
            color: #666;
            font-size: 14px;
            margin-top: 10px;
        }
        
        .status {
            padding: 15px;
            border-radius: 6px;
            margin: 15px 0;
            font-weight: 500;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        
        .status.success { 
            background: #d4edda; 
            color: #155724; 
            border: 1px solid #c3e6cb;
        }
        
        .status.error { 
            background: #f8d7da; 
            color: #721c24; 
            border: 1px solid #f5c6cb;
        }
        
        .status.warning { 
            background: #fff3cd; 
            color: #856404; 
            border: 1px solid #ffeaa7;
        }
        
        .loading {
            text-align: center;
            padding: 40px;
            color: #666;
        }
        
        .spinner {
            border: 3px solid #f3f3f3;
            border-top: 3px solid #ff6b6b;
            border-radius: 50%;
            width: 30px;
            height: 30px;
            animation: spin 1s linear infinite;
            margin: 0 auto 15px;
        }
        
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        
        .test-urls {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 8px;
            margin-top: 30px;
            border: 1px solid #dee2e6;
        }
        
        .test-urls h3 {
            margin-top: 0;
            color: #333;
        }
        
        .url-example {
            background: white;
            padding: 10px;
            border-radius: 4px;
            margin: 8px 0;
            font-family: monospace;
            font-size: 12px;
            word-break: break-all;
            border: 1px solid #ddd;
        }
        
        .url-example a {
            color: #007bff;
            text-decoration: none;
        }
        
        .url-example a:hover {
            text-decoration: underline;
        }
        
        .footer {
            text-align: center;
            padding: 20px;
            background: #f8f9fa;
            color: #666;
            font-size: 12px;
        }
        
        .lib-info {
            background: #e3f2fd;
            padding: 15px;
            border-radius: 6px;
            margin: 15px 0;
            border-left: 4px solid #2196f3;
            font-size: 14px;
        }
        
        .lib-info strong {
            color: #1976d2;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>📱 手机号信息查询</h1>
            <p style="margin: 10px 0 0 0; opacity: 0.9;">jsrsasign 数字签名验证</p>
        </div>
        
        <div class="content">
            <div class="lib-info">
                <strong>🔧 验证方案:</strong> jsrsasign v11.1.0 外部密码学库<br>
                <strong>📚 算法:</strong> SHA256withRSA 数字签名<br>
                <strong>🌐 兼容性:</strong> 支持所有现代浏览器
            </div>
            
            <div id="loading" class="loading">
                <div class="spinner"></div>
                正在验证参数...
            </div>
            <div id="result" style="display: none;"></div>
            
            <!-- 测试URL示例 -->
            <div class="test-urls">
                <h3>🧪 测试URL示例</h3>
                <p style="color: #666; font-size: 14px;">以下是一些测试URL,您可以复制并修改参数进行测试:</p>
                
                <div class="url-example">
                    <strong>简单模式:</strong><br>
                    <a href="?phone=4008286666">?phone=4008286666</a>
                </div>
                
                <div class="url-example">
                    <strong>安全模式(需要有效签名):</strong><br>
                    <a href="?data=4008286666&signature=testSignature">?data=4008286666&signature=testSignature</a>
                </div>
                
                <div class="url-example">
                    <strong>手机号示例:</strong><br>
                    <a href="?phone=13800138000">?phone=13800138000</a>
                </div>
                
                <div class="url-example">
                    <strong>无效参数测试:</strong><br>
                    <a href="?invalid=test">?invalid=test</a>
                </div>
            </div>
        </div>
        
        <div class="footer">
            jsrsasign Demo页面 | 统一消息推送服务 | v1.0
        </div>
    </div>

    <!-- 引入jsrsasign库 -->
    <script src="https://cdn.jsdelivr.net/npm/jsrsasign@11.1.0/lib/jsrsasign-all-min.js"></script>
    
    <script>
        class PhoneQueryHandlerJsrsasign {
            constructor() {
                // RSA公钥(Base64格式,与后端配置保持一致)
                this.publicKeyBase64 = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyVL2Ch4qBvICMBeS3XQDEK4A3R1l3qH9quqA+y+K7y1UC+G2wT5iC1ktyGVlPZKKOnhCuimwJgpuPMUoAO53HfuhIalXhO4+YFqdsmJfLpEdXr79Dl7idViWHuW5x3dEQYbgYteXc8HIWl0rK+kJGC3HRHvh7aBw08OS6tD5oOEncfg9jonvY3uUFlyLxil7CyYMj2GspBQonO7Kt5at9UBGqtqqaEaPnQzjkARxdC3u85+pMMr0lByIY896CEJKNYeDOToAYs1bJMhEU53xNvVPUERLHP5+sDt7bA7iwD7RT5SzWZ8zs9Oe9oIiVE7HIUKOHu86q2CZqm/lbzOC8QIDAQAB";
                
                this.init();
            }

            async init() {
                try {
                    const urlParams = new URLSearchParams(window.location.search);
                    const result = await this.parseUrlParameters(urlParams);
                    
                    setTimeout(() => {
                        this.hideLoading();
                        this.displayResult(result.phoneNumber, result.isSecureMode, result.isValid);
                    }, 1500); // 稍长的验证时间展示jsrsasign工作过程
                    
                } catch (error) {
                    console.error('页面初始化失败:', error);
                    setTimeout(() => {
                        this.hideLoading();
                        this.showError('页面初始化失败,请检查URL参数: ' + error.message);
                    }, 1500);
                }
            }

            async parseUrlParameters(urlParams) {
                const data = urlParams.get('data');
                const signature = urlParams.get('signature'); 
                const phone = urlParams.get('phone');

                if (data && signature) {
                    console.log('🔐 检测到安全模式URL,开始jsrsasign验证签名...');
                    const isValid = await this.verifySignatureWithJsrsasign(data, signature);
                    return {
                        phoneNumber: data,
                        isSecureMode: true,
                        isValid: isValid
                    };
                } else if (phone) {
                    console.log('📱 检测到简单模式URL');
                    return {
                        phoneNumber: phone,
                        isSecureMode: false,
                        isValid: true
                    };
                } else {
                    throw new Error('无效的URL参数格式');
                }
            }

            async verifySignatureWithJsrsasign(data, signatureBase64) {
                return new Promise((resolve) => {
                    try {
                        console.log('🔍 开始jsrsasign签名验证...');
                        console.log('数据:', data);
                        console.log('签名:', signatureBase64.substring(0, 20) + '...');

                        // 检查jsrsasign是否已加载
                        if (typeof KJUR === 'undefined') {
                            console.error('jsrsasign库未加载');
                            resolve(false);
                            return;
                        }

                        console.log('📚 jsrsasign库版本:', KJUR.version || '未知');

                        // URL解码签名(如果需要)
                        const decodedSignature = decodeURIComponent(signatureBase64);
                        console.log('📋 URL解码后签名长度:', decodedSignature.length);

                        // 创建签名验证实例
                        const signature = new KJUR.crypto.Signature({"alg": "SHA256withRSA"});
                        console.log('🔧 创建签名验证实例成功');

                        // 构建PEM格式的公钥
                        const publicKeyPEM = `-----BEGIN PUBLIC KEY-----n${this.publicKeyBase64}n-----END PUBLIC KEY-----`;
                        console.log('🔑 构建PEM格式公钥');

                        // 初始化签名实例
                        signature.init(publicKeyPEM);
                        console.log('✅ 初始化签名实例成功');

                        // 提供需要验证的原始数据
                        signature.updateString(data);
                        console.log('📝 更新验证数据成功');

                        // 将Base64签名转为Hex格式(jsrsasign要求)
                        let signatureHex;
                        try {
                            // 使用jsrsasign的内置函数转换
                            signatureHex = KJUR.b64tohex ? KJUR.b64tohex(decodedSignature) : this.base64ToHex(decodedSignature);
                            console.log('🔄 签名格式转换成功,Hex长度:', signatureHex.length);
                        } catch (convertError) {
                            console.error('❌ 签名格式转换失败:', convertError);
                            resolve(false);
                            return;
                        }

                        // 执行验证
                        console.log('⚡ 执行jsrsasign签名验证...');
                        const isValid = signature.verify(signatureHex);
                        
                        console.log('✅ jsrsasign签名验证结果:', isValid);
                        resolve(isValid);
                        
                    } catch (error) {
                        console.error('❌ jsrsasign签名验证异常:', error);
                        resolve(false);
                    }
                });
            }

            // 备用的Base64到Hex转换函数
            base64ToHex(base64) {
                const binary = atob(base64);
                let hex = '';
                for (let i = 0; i < binary.length; i++) {
                    const byte = binary.charCodeAt(i);
                    hex += byte.toString(16).padStart(2, '0');
                }
                return hex;
            }

            displayResult(phoneNumber, isSecureMode, isValid) {
                const resultDiv = document.getElementById('result');
                
                let statusHtml = '';
                let phoneHtml = '';
                
                if (isSecureMode) {
                    if (isValid) {
                        statusHtml = '<div class="status success">✅ jsrsasign签名验证成功,参数未被篡改</div>';
                        phoneHtml = `
                            <div class="phone-info">
                                <div class="phone-number">${this.formatPhoneNumber(phoneNumber)}</div>
                                <div class="phone-label">🔒 jsrsasign安全验证已通过</div>
                            </div>
                        `;
                    } else {
                        statusHtml = '<div class="status error">❌ jsrsasign签名验证失败,参数可能被篡改</div>';
                        phoneHtml = '<div class="phone-info" style="text-align: center; color: #999;">⚠️ 无法显示手机号信息</div>';
                    }
                } else {
                    statusHtml = '<div class="status warning">⚠️ 简单模式,未启用jsrsasign签名验证</div>';
                    phoneHtml = `
                        <div class="phone-info">
                            <div class="phone-number">${this.formatPhoneNumber(phoneNumber)}</div>
                            <div class="phone-label">📱 手机号信息</div>
                        </div>
                    `;
                }
                
                resultDiv.innerHTML = statusHtml + phoneHtml;
                resultDiv.style.display = 'block';
            }

            formatPhoneNumber(phoneNumber) {
                if (!phoneNumber) return '无';
                
                // 手机号脱敏处理
                if (phoneNumber.length === 11 && phoneNumber.startsWith('1')) {
                    return phoneNumber.replace(/(d{3})d{4}(d{4})/, '$1****$2');
                }
                
                // 400号码等其他格式直接显示
                return phoneNumber;
            }

            hideLoading() {
                const loadingDiv = document.getElementById('loading');
                if (loadingDiv) {
                    loadingDiv.style.display = 'none';
                }
            }

            showError(message) {
                const resultDiv = document.getElementById('result');
                resultDiv.innerHTML = `<div class="status error">❌ ${message}</div>`;
                resultDiv.style.display = 'block';
            }
        }

        // 等待页面和jsrsasign库都加载完成
        function initializeWhenReady() {
            if (typeof KJUR !== 'undefined') {
                console.log('🚀 jsrsasign库已加载,开始初始化...');
                new PhoneQueryHandlerJsrsasign();
            } else {
                console.log('⏳ 等待jsrsasign库加载...');
                setTimeout(initializeWhenReady, 100);
            }
        }

        document.addEventListener('DOMContentLoaded', initializeWhenReady);
    </script>
</body>
</html> 

方案对比与安全考量

特性 方案一 (Web Crypto API) 方案二 (jsrsasign)
安全性 。运行在浏览器沙箱内,受安全上下文保护。 低 (在HTTP下)。JS代码本身在传输中可能被篡改。
依赖 浏览器原生支持,需要HTTPS 第三方JS库,可在HTTP下运行
性能 极高。原生C++实现,速度快。 较低。纯JS运算,比原生慢多个数量级。
代码体积 。浏览器内置。 。需额外加载几十KB的JS文件,影响页面加载速度。
维护成本 。由浏览器厂商(Google, Apple等)负责维护和更新。 。需关注第三方库的漏洞和版本升级。

核心安全风险警告:

方案二的最大问题在于,如果页面通过 HTTP 访问,攻击者可以发动中间人攻击(MitM),在服务器将JS代码发送给用户的途中对其进行篡改。例如,攻击者可以将 signature.verify(...) 直接替换为 true,从而绕过整个验证机制。

HTTPS 不仅加密了数据,更重要的是保证了您网站资源(包括JavaScript代码)在传输过程中的完整性,防止其被篡改。 这也是为什么浏览器强制要求在安全上下文中才能使用 Web Crypto API 的根本原因。

总结与建议

通过“后端签名、前端验证”的模式,可以有效构建一个防篡改的URL参数传递机制。

  1. 首选方案:强烈推荐使用方案一(Web Crypto API)。为网站部署 HTTPS 是现代Web开发的基石,它不仅能解决当前问题,还能全面提升网站的安全性、可信度和用户体验。

  2. 备选方案方案二(jsrsasign) 可以作为无法使用HTTPS环境下的临时备选或技术演示,但必须清醒地认识到其在HTTP协议下存在的巨大安全风险,不应作为生产环境的最终选择

通过以上“后端签名,前端验证”的模式,我们成功地构建了一个安全、防篡改的URL参数传递机制。

  • 完整性:任何对URL中data参数的修改都会导致签名验证失败。
  • 认证性:只有持有私钥的后端才能生成有效的签名,从而证明了数据的来源。

这个模式不仅限于手机号,它可以应用于任何不希望被随意篡改的URL参数场景,如用户ID、订单号、一次性令牌等,为你的Web应用增加一道坚实的安全防线。

演示页面:
浏览器Web Crypto API版本
jsrsasign解密版本

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注