用AES-GCM算法加密链接内容,并生成可分享的加密链接和解密工具。
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>链接内容加密 · AES-GCM 演示</title>
<style>
* { box-sizing: border-box; margin: 0; }
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background: linear-gradient(145deg, #f5f7fc 0%, #e8ecf5 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
margin: 0;
}
.card {
max-width: 820px;
width: 100%;
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.6);
border-radius: 2rem;
padding: 2rem 2.2rem 2.4rem;
box-shadow: 0 30px 50px -20px rgba(0, 20, 40, 0.25), inset 0 1px 0 rgba(255,255,255,0.8);
}
h1 {
font-size: 1.9rem;
font-weight: 600;
color: #0f1a2b;
letter-spacing: -0.02em;
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 0.3rem;
}
.sub {
color: #4d627a;
font-size: 0.95rem;
margin-bottom: 1.8rem;
border-left: 4px solid #3b82f6;
padding-left: 1rem;
font-weight: 400;
}
label {
display: block;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.03em;
font-weight: 600;
color: #3f5570;
margin-bottom: 0.4rem;
}
.field {
margin-bottom: 1.5rem;
}
input, textarea {
width: 100%;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.9rem;
padding: 0.9rem 1.2rem;
border-radius: 1rem;
border: 1.5px solid #d7dfeb;
background: white;
outline: none;
transition: 0.2s;
resize: vertical;
color: #101c2c;
}
input:focus, textarea:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 4px rgba(59,130,246,0.15);
}
textarea {
min-height: 80px;
}
.row {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
margin-bottom: 1.5rem;
}
.btn {
border: none;
background: #1d2b3f;
color: white;
font-weight: 600;
font-size: 0.9rem;
padding: 0.8rem 1.8rem;
border-radius: 2rem;
cursor: pointer;
transition: 0.15s;
display: inline-flex;
align-items: center;
gap: 8px;
box-shadow: 0 6px 14px rgba(0,20,40,0.15);
flex: 0 0 auto;
}
.btn:hover {
background: #0f1a2b;
transform: translateY(-1px);
box-shadow: 0 10px 20px rgba(0,20,40,0.2);
}
.btn.secondary {
background: white;
color: #1d2b3f;
border: 1.5px solid #cbd5e1;
box-shadow: none;
}
.btn.secondary:hover {
background: #f1f5f9;
border-color: #94a3b8;
}
.btn:disabled {
opacity: 0.4;
pointer-events: none;
transform: none;
}
.output {
background: #0f1a2b;
color: #b4d0ff;
padding: 1rem 1.3rem;
border-radius: 1.2rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
word-break: break-all;
white-space: pre-wrap;
border: 1px solid #2b3b52;
margin-bottom: 1.2rem;
min-height: 3.2rem;
}
.badge {
display: inline-block;
background: #dbeafe;
color: #1e40af;
font-size: 0.7rem;
font-weight: 600;
padding: 0.2rem 0.7rem;
border-radius: 40px;
letter-spacing: 0.02em;
margin-left: 8px;
}
.status {
font-size: 0.8rem;
color: #2563eb;
background: #eff6ff;
padding: 0.6rem 1rem;
border-radius: 1rem;
margin-top: 0.5rem;
display: flex;
align-items: center;
gap: 8px;
border: 1px solid #bfdbfe;
font-weight: 500;
}
hr {
border: none;
border-top: 1.5px dashed #cbd5e1;
margin: 2rem 0 1.5rem;
}
.footer-note {
font-size: 0.8rem;
color: #5e728b;
display: flex;
gap: 1rem;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
}
.key-hint {
font-family: monospace;
background: #eef2f7;
padding: 0.2rem 0.6rem;
border-radius: 20px;
font-size: 0.75rem;
color: #1e293b;
}
.icon {
font-size: 1.3rem;
}
</style>
</head>
<body>
<div class="card">
<h1>
<span class="icon">🔗</span>
AES‑GCM 链接加密
<span class="badge">URL‑safe</span>
</h1>
<div class="sub">加密原始链接 → 得到安全 Token,可拼接到任意分享链接中</div>
<!-- 原始链接输入 -->
<div class="field">
<label for="plainUrl">📎 原始链接 (明文)</label>
<input id="plainUrl" type="text" value="https://example.com/pay?order=123\&amount=999\&user=alice" spellcheck="false" />
</div>
<!-- 加密 / 解密 按钮 -->
<div class="row">
<button class="btn" id="encryptBtn"><span>🔒</span> 加密链接</button>
<button class="btn secondary" id="decryptBtn"><span>🔓</span> 解密</button>
<button class="btn secondary" id="refreshKeyBtn" title="生成新的 AES-256 密钥">🔑 新密钥</button>
</div>
<!-- 加密结果(Token) -->
<div class="field">
<label>🔐 加密 Token (可直接拼到 URL 参数)</label>
<div class="output" id="tokenOutput">等待加密...</div>
</div>
<!-- 完整分享链接 -->
<div class="field">
<label>🌐 完整分享链接</label>
<div class="output" id="shareLinkOutput" style="color:#c9e0ff;">https://your.site/r?d=...</div>
</div>
<!-- 解密输入区 -->
<hr />
<div class="field">
<label>📥 粘贴加密 Token 解密</label>
<textarea id="tokenInput" placeholder="把加密后的 Token 粘贴到这里..." spellcheck="false"></textarea>
</div>
<button class="btn secondary" id="decryptFromInputBtn"><span>⚡</span> 解密 Token</button>
<!-- 解密结果展示 -->
<div class="field" style="margin-top:1.2rem;">
<label>🔎 解密结果</label>
<div class="output" id="decryptOutput" style="color:#bbf7d0; min-height:2.8rem;">---</div>
</div>
<!-- 状态提示 -->
<div class="status" id="statusMsg">
<span>✨</span> 就绪 · 使用 Web Crypto API (AES-GCM 256)
</div>
<!-- 底部密钥信息 -->
<div class="footer-note" style="margin-top:1.8rem;">
<span class="key-hint">密钥指纹: <span id="keyFingerprint">---</span></span>
<span style="display:flex; gap:10px;">
<span>⏳ 有效期 1 小时</span>
<span>🛡️ 认证加密</span>
</span>
</div>
</div>
<script>
(function() {
// ---------- 工具: ArrayBuffer / Base64url ----------
function bufToBase64Url(buf) {
const bytes = new Uint8Array(buf);
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytesi);
}
return btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
function base64UrlToBuf(base64url) {
let base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
while (base64.length % 4) base64 += '=';
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytesi = binary.charCodeAt(i);
}
return bytes.buffer;
}
// ---------- 密钥管理 ----------
let cryptoKey = null; // CryptoKey 对象
let keyFingerprint = '';
async function generateKey() {
// 生成 AES-GCM 256 位密钥(可导出以便保存到环境变量,这里仅用于演示)
const rawKey = crypto.getRandomValues(new Uint8Array(32)); // 256 bit
cryptoKey = await crypto.subtle.importKey(
'raw',
rawKey,
{ name: 'AES-GCM', length: 256 },
false, // 不可导出(安全)
'encrypt', 'decrypt'
);
// 计算密钥指纹(使用 SHA-256 的前 8 字节)
const hash = await crypto.subtle.digest('SHA-256', rawKey);
const hashBytes = new Uint8Array(hash);
keyFingerprint = Array.from(hashBytes.slice(0, 4))
.map(b => b.toString(16).padStart(2, '0'))
.join(':');
document.getElementById('keyFingerprint').textContent = keyFingerprint;
}
// ---------- 加密 ----------
async function encryptLink(url, ttlSeconds = 3600) {
if (!cryptoKey) throw new Error('密钥未初始化');
if (!url || url.trim() === '') throw new Error('链接不能为空');
const payload = {
u: url,
exp: Math.floor(Date.now() / 1000) + ttlSeconds,
};
const encoder = new TextEncoder();
const plaintext = encoder.encode(JSON.stringify(payload));
// 12 字节随机 nonce (IV)
const iv = crypto.getRandomValues(new Uint8Array(12));
// AES-GCM 加密 (自动附加 16 字节认证标签)
const ciphertextWithTag = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
cryptoKey,
plaintext
);
// 组合: nonce (12) + ciphertext+tag
const combined = new Uint8Array(iv.length + ciphertextWithTag.byteLength);
combined.set(iv, 0);
combined.set(new Uint8Array(ciphertextWithTag), iv.length);
return bufToBase64Url(combined.buffer);
}
// ---------- 解密 ----------
async function decryptLink(tokenBase64Url) {
if (!cryptoKey) throw new Error('密钥未初始化');
if (!tokenBase64Url || tokenBase64Url.trim() === '') throw new Error('Token 不能为空');
let combined;
try {
combined = new Uint8Array(base64UrlToBuf(tokenBase64Url.trim()));
} catch (e) {
throw new Error('Token 不是合法的 Base64URL 格式');
}
if (combined.length < 12 + 16) throw new Error('Token 太短,无法解密');
const iv = combined.slice(0, 12);
const ciphertextWithTag = combined.slice(12);
let decrypted;
try {
decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
cryptoKey,
ciphertextWithTag
);
} catch (err) {
// 认证失败 / 密钥不匹配 / 密文被篡改
throw new Error('解密失败:密钥不匹配或数据被篡改');
}
const decoder = new TextDecoder();
let payload;
try {
payload = JSON.parse(decoder.decode(decrypted));
} catch (e) {
throw new Error('解密内容不是有效的 JSON');
}
// 检查过期
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
throw new Error('链接已过期(超过有效期)');
}
return payload.u;
}
// ---------- DOM 元素 ----------
const plainUrlInput = document.getElementById('plainUrl');
const tokenOutput = document.getElementById('tokenOutput');
const shareLinkOutput = document.getElementById('shareLinkOutput');
const tokenInput = document.getElementById('tokenInput');
const decryptOutput = document.getElementById('decryptOutput');
const statusMsg = document.getElementById('statusMsg');
// 更新状态栏
function setStatus(text, isError = false) {
statusMsg.innerHTML = `<span>{isError ? '⚠️' : '✨'}\ {text}`;
statusMsg.style.background = isError ? '#fef2f2' : '#eff6ff';
statusMsg.style.color = isError ? '#b91c1c' : '#2563eb';
statusMsg.style.borderColor = isError ? '#fecaca' : '#bfdbfe';
}
// 刷新展示加密 token 和分享链接
async function doEncrypt() {
try {
const url = plainUrlInput.value.trim();
if (!url) throw new Error('请输入原始链接');
const token = await encryptLink(url, 3600); // 1 小时有效期
tokenOutput.textContent = token;
shareLinkOutput.textContent = `https://your.site/r?d=${token}\`;
// 自动填充到解密输入框,方便测试
tokenInput.value = token;
setStatus('加密成功 · Token 已生成(有效期 1 小时)');
} catch (err) {
setStatus(err.message, true);
}
}
// 从 tokenInput 解密
async function doDecryptFromInput() {
try {
const token = tokenInput.value.trim();
if (!token) throw new Error('请粘贴加密 Token');
const plain = await decryptLink(token);
decryptOutput.textContent = plain;
setStatus('解密成功 · 链接有效');
} catch (err) {
decryptOutput.textContent = '---';
setStatus(err.message, true);
}
}
// ---------- 事件绑定 ----------
document.getElementById('encryptBtn').addEventListener('click', doEncrypt);
document.getElementById('decryptBtn').addEventListener('click', () => {
// 使用当前 tokenOutput 中的 token 解密
const token = tokenOutput.textContent;
if (!token || token === '等待加密...') {
setStatus('请先点击「加密链接」生成 Token', true);
return;
}
tokenInput.value = token;
doDecryptFromInput();
});
document.getElementById('decryptFromInputBtn').addEventListener('click', doDecryptFromInput);
document.getElementById('refreshKeyBtn').addEventListener('click', async () => {
await generateKey();
setStatus('已生成新的 AES-256 密钥,之前的 Token 将无法解密');
// 清空输出
tokenOutput.textContent = '等待加密...';
shareLinkOutput.textContent = 'https://your.site/r?d=...';
tokenInput.value = '';
decryptOutput.textContent = '---';
});
// ---------- 初始化 ----------
(async function init() {
await generateKey();
setStatus('就绪 · 密钥已生成,可以加密链接了');
})();
// 方便控制台调试
window.__linkCrypto = { encryptLink, decryptLink, generateKey };
})();
</script>
</body>
</html>
```
加密与解密操作流程
· 加密链接:在"原始链接"输入框里填写要分享的网址,点击"加密链接"按钮,下方会立刻显示加密后的Token和完整的分享链接,方便复制使用。
· 解密链接:把加密Token粘贴到"粘贴加密Token解密"文本框,点击"解密Token"按钮,就能看到原始链接;也可以直接点击"解密"按钮,用当前生成的Token进行快速解密。
· 密钥管理:点击"新密钥"可以刷新AES-256密钥,之前的Token会立刻失效,保证只有掌握当前密钥的人才能解密。页面底部会显示密钥指纹,方便核对。
此仅为教学示例参考用。