在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)。
方案:后端签名,前端验证
数字签名的工作流程完美地解决了“防篡改”和“身份验证”的需求。
- 后端(签名方):使用私钥对数据(或其摘要)进行签名,就像给文件盖上一个独一无二的、无法伪造的公章。
- 前端(验证方):使用公钥来验证这个签名。公钥是公开的,任何人都可以用它来检验公章的真伪。
如果数据被篡改,或者签名是伪造的(因为坏人没有你的私-钥),验证就会失败。
工作流程
- 后端:
- 获取原始数据(如手机号
13800138000)。 - 使用只有后端知道的私钥,为这个手机号生成一个数字签名。
- 将原始数据和签名一起作为参数,构建URL。
- 获取原始数据(如手机号
- 前端:
- 从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参数传递机制。
-
首选方案:强烈推荐使用方案一(Web Crypto API)。为网站部署 HTTPS 是现代Web开发的基石,它不仅能解决当前问题,还能全面提升网站的安全性、可信度和用户体验。
-
备选方案:方案二(jsrsasign) 可以作为无法使用HTTPS环境下的临时备选或技术演示,但必须清醒地认识到其在HTTP协议下存在的巨大安全风险,不应作为生产环境的最终选择。
通过以上“后端签名,前端验证”的模式,我们成功地构建了一个安全、防篡改的URL参数传递机制。
- 完整性:任何对URL中
data参数的修改都会导致签名验证失败。 - 认证性:只有持有私钥的后端才能生成有效的签名,从而证明了数据的来源。
这个模式不仅限于手机号,它可以应用于任何不希望被随意篡改的URL参数场景,如用户ID、订单号、一次性令牌等,为你的Web应用增加一道坚实的安全防线。