一、技术实现方案
1.1 前端加密架构
前端加密体系
├── 接口签名
│ ├── 签名算法(HMAC-SHA256)
│ ├── 时间戳验证
│ ├── 随机数(Nonce)
│ └── 请求参数排序
│
├── 代码混淆
│ ├── JavaScript混淆
│ ├── 变量名混淆
│ ├── 控制流扁平化
│ └── 字符串加密
│
├── 数据加密
│ ├── AES对称加密
│ ├── RSA非对称加密
│ ├── 前端存储加密
│ └── 传输数据加密
│
└── 防护机制
├── 数字水印
├── 防调试
├── 防截图
└── 反爬虫
1.2 技术栈
- 加密: CryptoJS, JSEncrypt
- 混淆: javascript-obfuscator, UglifyJS
- 水印: Canvas API, SVG
- 签名: HMAC-SHA256, MD5
二、接口签名算法
2.1 签名工具类
api-signature.js
import CryptoJS from 'crypto-js'
export class APISignature {
constructor(options = {}) {
this.appKey = options.appKey || 'default-app-key'
this.appSecret = options.appSecret || 'default-app-secret'
this.signatureExpiry = options.signatureExpiry || 300000 // 5分钟
}
// 生成签名
generateSignature(params = {}, timestamp, nonce) {
// 1. 添加公共参数
const allParams = {
...params,
app_key: this.appKey,
timestamp: timestamp,
nonce: nonce,
}
// 2. 参数排序
const sortedKeys = Object.keys(allParams).sort()
// 3. 拼接参数字符串
const paramString = sortedKeys
.map((key) => `${key}=${allParams[key]}`)
.join('&')
// 4. 添加密钥
const signString = `${paramString}&key=${this.appSecret}`
// 5. 计算签名
const signature = CryptoJS.HmacSHA256(
signString,
this.appSecret
).toString()
return signature
}
// 生成完整的请求参数
signRequest(params = {}) {
const timestamp = Date.now()
const nonce = this.generateNonce()
const signature = this.generateSignature(params, timestamp, nonce)
return {
...params,
app_key: this.appKey,
timestamp: timestamp,
nonce: nonce,
sign: signature,
}
}
// 验证签名(客户端模拟,实际在服务端验证)
verifySignature(params = {}) {
const { sign, timestamp, nonce, ...restParams } = params
// 1. 检查时间戳是否过期
if (Date.now() - timestamp > this.signatureExpiry) {
return {
valid: false,
reason: 'Signature expired',
}
}
// 2. 重新计算签名
const calculatedSign = this.generateSignature(
restParams,
timestamp,
nonce
)
// 3. 对比签名
if (calculatedSign !== sign) {
return {
valid: false,
reason: 'Invalid signature',
}
}
return {
valid: true,
}
}
// 生成随机数
generateNonce(length = 16) {
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let nonce = ''
for (let i = 0; i < length; i++) {
nonce += chars.charAt(Math.floor(Math.random() * chars.length))
}
return nonce
}
// 生成请求ID
generateRequestId() {
return `${Date.now()}_${this.generateNonce(8)}`
}
// Axios拦截器配置
setupAxiosInterceptor(axios) {
axios.interceptors.request.use((config) => {
// 对于GET请求,签名params
if (config.method === 'get') {
config.params = this.signRequest(config.params || {})
}
// 对于POST请求,签名body
if (config.method === 'post' && config.data) {
const signedData = this.signRequest(config.data)
config.data = signedData
}
return config
})
}
}
export default new APISignature()
2.2 接口签名演示组件
SignatureDemo.vue
<script setup>
import { ref, computed } from 'vue'
import { APISignature } from './api-signature.js'
const apiSignature = new APISignature({
appKey: 'demo-app-123',
appSecret: 'demo-secret-key-456',
})
const requestParams = ref({
user_id: '10001',
action: 'get_user_info',
page: '1',
})
const signedParams = ref(null)
const verificationResult = ref(null)
// 生成签名
const generateSignature = () => {
signedParams.value = apiSignature.signRequest(requestParams.value)
}
// 验证签名
const verifySignature = () => {
if (!signedParams.value) return
verificationResult.value = apiSignature.verifySignature(signedParams.value)
}
// 模拟篡改参数
const tamperParams = () => {
if (!signedParams.value) return
signedParams.value.user_id = '99999'
verifySignature()
}
// 签名过程展示
const signatureProcess = computed(() => {
if (!signedParams.value) return []
const { sign, timestamp, nonce, app_key, ...params } = signedParams.value
return [
{
step: 1,
title: '添加公共参数',
content: JSON.stringify(
{ ...params, app_key, timestamp, nonce },
null,
2
),
},
{
step: 2,
title: '参数排序',
content: Object.keys({ ...params, app_key, timestamp, nonce })
.sort()
.join(', '),
},
{
step: 3,
title: '拼接参数字符串',
content: Object.keys({ ...params, app_key, timestamp, nonce })
.sort()
.map((key) => `${key}=${signedParams.value[key]}`)
.join('&'),
},
{
step: 4,
title: '添加密钥并计算签名',
content: `HMAC-SHA256(参数字符串 + &key=密钥, 密钥)`,
},
{
step: 5,
title: '最终签名',
content: sign,
},
]
})
// 添加参数
const newKey = ref('')
const newValue = ref('')
const addParam = () => {
if (newKey.value && newValue.value) {
requestParams.value[newKey.value] = newValue.value
newKey.value = ''
newValue.value = ''
}
}
// 删除参数
const removeParam = (key) => {
delete requestParams.value[key]
}
</script>
<template>
<div class="signature-demo">
<div class="demo-header">
<h2>接口签名算法演示</h2>
<p class="subtitle">演示HMAC-SHA256签名过程</p>
</div>
<!-- 配置信息 -->
<div class="config-section">
<h3>配置信息</h3>
<div class="config-grid">
<div class="config-item">
<label>App Key:</label>
<code>demo-app-123</code>
</div>
<div class="config-item">
<label>App Secret:</label>
<code>demo-secret-key-456</code>
</div>
<div class="config-item">
<label>签名算法:</label>
<code>HMAC-SHA256</code>
</div>
<div class="config-item">
<label>有效期:</label>
<code>5分钟</code>
</div>
</div>
</div>
<!-- 请求参数 -->
<div class="params-section">
<h3>请求参数</h3>
<div class="add-param">
<input v-model="newKey" type="text" placeholder="参数名" />
<input v-model="newValue" type="text" placeholder="参数值" />
<button @click="addParam" class="add-btn">添加</button>
</div>
<div class="params-list">
<div
v-for="(value, key) in requestParams"
:key="key"
class="param-item"
>
<span class="param-key">{{ key }}</span>
<span class="param-value">{{ value }}</span>
<button @click="removeParam(key)" class="remove-btn">
删除
</button>
</div>
</div>
<button @click="generateSignature" class="generate-btn">
生成签名
</button>
</div>
<!-- 签名过程 -->
<div v-if="signedParams" class="process-section">
<h3>签名生成过程</h3>
<div class="process-steps">
<div
v-for="step in signatureProcess"
:key="step.step"
class="step-item"
>
<div class="step-number">步骤{{ step.step }}</div>
<div class="step-title">{{ step.title }}</div>
<pre class="step-content">{{ step.content }}</pre>
</div>
</div>
</div>
<!-- 签名结果 -->
<div v-if="signedParams" class="result-section">
<h3>签名结果</h3>
<div class="result-box">
<pre>{{ JSON.stringify(signedParams, null, 2) }}</pre>
</div>
<div class="action-buttons">
<button @click="verifySignature" class="verify-btn">
验证签名
</button>
<button @click="tamperParams" class="tamper-btn">
模拟篡改
</button>
</div>
</div>
<!-- 验证结果 -->
<div v-if="verificationResult" class="verification-section">
<h3>验证结果</h3>
<div
class="verification-box"
:class="{
valid: verificationResult.valid,
invalid: !verificationResult.valid,
}"
>
<div class="verification-status">
<span class="status-icon">
{{ verificationResult.valid ? '✅' : '❌' }}
</span>
<span class="status-text">
{{
verificationResult.valid
? '签名验证通过'
: '签名验证失败'
}}
</span>
</div>
<div
v-if="!verificationResult.valid"
class="verification-reason"
>
原因: {{ verificationResult.reason }}
</div>
</div>
</div>
<!-- 使用说明 -->
<div class="usage-section">
<h3>接口签名使用说明</h3>
<div class="usage-card">
<h4>前端实现</h4>
<pre><code>// 初始化签名工具
const apiSignature = new APISignature({
appKey: 'your-app-key',
appSecret: 'your-app-secret'
})
// 配置Axios拦截器
apiSignature.setupAxiosInterceptor(axios)
// 或手动签名请求
const signedParams = apiSignature.signRequest({
user_id: '123',
action: 'get_info'
})</code></pre>
</div>
<div class="usage-card">
<h4>服务端验证 (Node.js)</h4>
<pre><code>const crypto = require('crypto')
function verifySignature(params, appSecret) {
const { sign, ...restParams } = params
// 参数排序
const sortedKeys = Object.keys(restParams).sort()
const paramString = sortedKeys
.map(key => `${key}=${restParams[key]}`)
.join('&')
// 计算签名
const signString = `${paramString}&key=${appSecret}`
const calculatedSign = crypto
.createHmac('sha256', appSecret)
.update(signString)
.digest('hex')
// 验证
return calculatedSign === sign
}</code></pre>
</div>
</div>
<!-- 最佳实践 -->
<div class="best-practices-section">
<h3>签名安全最佳实践</h3>
<ul class="practices-list">
<li>密钥(appSecret)永远不要暴露在前端代码中</li>
<li>使用HTTPS传输,防止中间人攻击</li>
<li>添加时间戳,防止重放攻击</li>
<li>使用随机数(nonce),增加签名唯一性</li>
<li>参数排序保证签名一致性</li>
<li>服务端严格验证签名和时间戳</li>
<li>记录签名失败日志,监控异常请求</li>
<li>定期轮换密钥,降低泄露风险</li>
</ul>
</div>
</div>
</template>
<style scoped>
.signature-demo {
padding: 20px;
background: #f5f7fa;
min-height: 100vh;
}
.demo-header {
text-align: center;
margin-bottom: 40px;
}
.demo-header h2 {
margin: 0 0 10px 0;
font-size: 32px;
color: #303133;
}
.subtitle {
margin: 0;
font-size: 16px;
color: #909399;
}
/* 各个section */
.config-section,
.params-section,
.process-section,
.result-section,
.verification-section,
.usage-section,
.best-practices-section {
margin-bottom: 30px;
padding: 24px;
background: white;
border-radius: 8px;
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
color: #303133;
}
/* 配置信息 */
.config-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
}
.config-item {
padding: 16px;
background: #f5f7fa;
border-radius: 8px;
}
.config-item label {
display: block;
font-size: 14px;
color: #606266;
margin-bottom: 8px;
font-weight: 600;
}
.config-item code {
display: block;
padding: 8px;
background: white;
border-radius: 4px;
font-size: 13px;
color: #409eff;
}
/* 参数设置 */
.add-param {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.add-param input {
flex: 1;
padding: 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
}
.add-btn {
padding: 10px 24px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
.params-list {
margin-bottom: 20px;
}
.param-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #f5f7fa;
border-radius: 4px;
margin-bottom: 8px;
}
.param-key {
font-weight: 600;
color: #303133;
min-width: 120px;
}
.param-value {
flex: 1;
color: #606266;
}
.remove-btn {
padding: 6px 12px;
background: #f56c6c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.generate-btn {
padding: 12px 32px;
background: #67c23a;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
/* 签名过程 */
.process-steps {
display: flex;
flex-direction: column;
gap: 16px;
}
.step-item {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
border-left: 4px solid #409eff;
}
.step-number {
display: inline-block;
padding: 4px 12px;
background: #409eff;
color: white;
border-radius: 12px;
font-size: 12px;
font-weight: 700;
margin-bottom: 8px;
}
.step-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.step-content {
margin: 0;
padding: 12px;
background: white;
border-radius: 4px;
font-size: 13px;
color: #606266;
overflow-x: auto;
font-family: 'Courier New', monospace;
}
/* 结果显示 */
.result-box {
padding: 16px;
background: #282c34;
border-radius: 8px;
margin-bottom: 20px;
overflow-x: auto;
}
.result-box pre {
margin: 0;
color: #abb2bf;
font-size: 13px;
line-height: 1.6;
font-family: 'Courier New', monospace;
}
.action-buttons {
display: flex;
gap: 12px;
}
.verify-btn,
.tamper-btn {
padding: 10px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
.verify-btn {
background: #409eff;
color: white;
}
.tamper-btn {
background: #e6a23c;
color: white;
}
/* 验证结果 */
.verification-box {
padding: 24px;
border-radius: 8px;
}
.verification-box.valid {
background: #f0f9ff;
border: 2px solid #67c23a;
}
.verification-box.invalid {
background: #fef0f0;
border: 2px solid #f56c6c;
}
.verification-status {
display: flex;
align-items: center;
gap: 12px;
}
.status-icon {
font-size: 32px;
}
.status-text {
font-size: 18px;
font-weight: 600;
color: #303133;
}
.verification-reason {
margin-top: 12px;
padding: 12px;
background: rgba(245, 108, 108, 0.1);
border-radius: 4px;
color: #f56c6c;
font-size: 14px;
}
/* 使用说明 */
.usage-card {
margin-bottom: 24px;
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.usage-card h4 {
margin: 0 0 12px 0;
font-size: 16px;
color: #303133;
}
.usage-card pre {
margin: 0;
padding: 16px;
background: #282c34;
border-radius: 4px;
overflow-x: auto;
}
.usage-card code {
font-size: 13px;
line-height: 1.6;
color: #abb2bf;
font-family: 'Courier New', monospace;
}
/* 最佳实践 */
.practices-list {
margin: 0;
padding-left: 24px;
}
.practices-list li {
font-size: 14px;
line-height: 2;
color: #606266;
}
</style>
三、前端代码混淆
3.1 代码混淆配置
webpack.obfuscate.config.js
const JavaScriptObfuscator = require('webpack-obfuscator')
module.exports = {
// Webpack配置
plugins: [
new JavaScriptObfuscator(
{
// 压缩代码
compact: true,
// 控制流扁平化
controlFlowFlattening: true,
controlFlowFlatteningThreshold: 0.75,
// 死代码注入
deadCodeInjection: true,
deadCodeInjectionThreshold: 0.4,
// 调试保护
debugProtection: true,
debugProtectionInterval: 2000,
// 禁用控制台输出
disableConsoleOutput: true,
// 标识符重命名
identifierNamesGenerator: 'hexadecimal',
// 日志输出
log: false,
// 数字转换为表达式
numbersToExpressions: true,
// 重命名全局变量
renameGlobals: false,
// 自我防御
selfDefending: true,
// 简化字符串数组
simplify: true,
// 字符串数组
stringArray: true,
stringArrayCallsTransform: true,
stringArrayEncoding: ['base64'],
stringArrayIndexShift: true,
stringArrayRotate: true,
stringArrayShuffle: true,
stringArrayWrappersCount: 2,
stringArrayWrappersChainedCalls: true,
stringArrayWrappersParametersMaxCount: 4,
stringArrayWrappersType: 'function',
stringArrayThreshold: 0.75,
// Unicode转义
unicodeEscapeSequence: false,
// 目标环境
target: 'browser',
},
[]
),
],
}
3.2 混淆前后对比
原始代码
function calculateTotal(items) {
let total = 0
for (let item of items) {
if (item.price > 0) {
total += item.price * item.quantity
}
}
return total
}
const API_KEY = 'sk-1234567890abcdef'
function fetchUserData(userId) {
return fetch(`/api/users/${userId}`, {
headers: {
'X-API-Key': API_KEY,
},
}).then((res) => res.json())
}
混淆后代码
var _0x4a2b = ['price', 'quantity', 'length', 'X-API-Key', '/api/users/']
;(function (_0x1f8d3e, _0x4a2b7c) {
var _0x5c8e91 = function (_0x3d7b88) {
while (--_0x3d7b88) {
_0x1f8d3e['push'](_0x1f8d3e['shift']())
}
}
_0x5c8e91(++_0x4a2b7c)
})(_0x4a2b, 0x1f4)
var _0x5c8e = function (_0x1f8d3e, _0x4a2b7c) {
_0x1f8d3e = _0x1f8d3e - 0x0
var _0x5c8e91 = _0x4a2b[_0x1f8d3e]
return _0x5c8e91
}
function _0x3d7b(_0x1f8d3e) {
var _0x4a2b7c = 0x0
for (
var _0x5c8e91 = 0x0;
_0x5c8e91 < _0x1f8d3e[_0x5c8e('0x2')];
_0x5c8e91++
) {
if (_0x1f8d3e[_0x5c8e91][_0x5c8e('0x0')] > 0x0) {
_0x4a2b7c +=
_0x1f8d3e[_0x5c8e91][_0x5c8e('0x0')] *
_0x1f8d3e[_0x5c8e91][_0x5c8e('0x1')]
}
}
return _0x4a2b7c
}
const _0x2f1a8e = 'sk-1234567890abcdef'
function _0x8c3f(_0x1f8d3e) {
return fetch(_0x5c8e('0x4') + _0x1f8d3e, {
headers: { [_0x5c8e('0x3')]: _0x2f1a8e },
})['then']((_0x4a2b7c) => _0x4a2b7c['json']())
}
四、敏感数据加密
4.1 前端加密管理器
encryption-manager.js
import CryptoJS from 'crypto-js'
import JSEncrypt from 'jsencrypt'
export class EncryptionManager {
constructor() {
// AES密钥(实际项目中应该从服务端获取)
this.aesKey = CryptoJS.lib.WordArray.random(128 / 8).toString()
// RSA公钥(用于加密AES密钥)
this.rsaPublicKey = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...
-----END PUBLIC KEY-----`
}
// AES加密
encryptAES(plaintext, key = this.aesKey) {
const encrypted = CryptoJS.AES.encrypt(plaintext, key)
return encrypted.toString()
}
// AES解密
decryptAES(ciphertext, key = this.aesKey) {
const decrypted = CryptoJS.AES.decrypt(ciphertext, key)
return decrypted.toString(CryptoJS.enc.Utf8)
}
// RSA加密
encryptRSA(plaintext) {
const encrypt = new JSEncrypt()
encrypt.setPublicKey(this.rsaPublicKey)
return encrypt.encrypt(plaintext)
}
// 混合加密(RSA+AES)
hybridEncrypt(data) {
// 1. 生成随机AES密钥
const aesKey = CryptoJS.lib.WordArray.random(128 / 8).toString()
// 2. 用AES加密数据
const encryptedData = this.encryptAES(JSON.stringify(data), aesKey)
// 3. 用RSA加密AES密钥
const encryptedKey = this.encryptRSA(aesKey)
return {
data: encryptedData,
key: encryptedKey,
}
}
// 加密localStorage存储
setEncryptedStorage(key, value) {
const encrypted = this.encryptAES(JSON.stringify(value))
localStorage.setItem(key, encrypted)
}
// 解密localStorage读取
getEncryptedStorage(key) {
const encrypted = localStorage.getItem(key)
if (!encrypted) return null
try {
const decrypted = this.decryptAES(encrypted)
return JSON.parse(decrypted)
} catch (e) {
console.error('Decrypt storage failed:', e)
return null
}
}
// 加密表单数据
encryptFormData(formData) {
const encrypted = {}
for (const [key, value] of Object.entries(formData)) {
// 敏感字段加密
if (this.isSensitiveField(key)) {
encrypted[key] = this.encryptAES(value)
} else {
encrypted[key] = value
}
}
return encrypted
}
// 判断是否为敏感字段
isSensitiveField(fieldName) {
const sensitiveFields = [
'password',
'card_no',
'id_card',
'phone',
'email',
'bank_account',
]
return sensitiveFields.some((field) =>
fieldName.toLowerCase().includes(field)
)
}
// 密码加密(加盐哈希)
hashPassword(password, salt) {
if (!salt) {
salt = CryptoJS.lib.WordArray.random(128 / 8).toString()
}
const hash = CryptoJS.PBKDF2(password, salt, {
keySize: 256 / 32,
iterations: 10000,
}).toString()
return {
hash: hash,
salt: salt,
}
}
}
export default new EncryptionManager()
4.2 加密演示组件
EncryptionDemo.vue
<script setup>
import { ref } from 'vue'
import encryptionManager from './encryption-manager.js'
const plaintext = ref('Hello, World!')
const aesEncrypted = ref('')
const aesDecrypted = ref('')
const formData = ref({
username: 'john_doe',
password: '123456',
email: 'john@example.com',
card_no: '6222021234567890',
})
const encryptedForm = ref(null)
// AES加密
const encryptAES = () => {
aesEncrypted.value = encryptionManager.encryptAES(plaintext.value)
}
// AES解密
const decryptAES = () => {
aesDecrypted.value = encryptionManager.decryptAES(aesEncrypted.value)
}
// 加密表单
const encryptForm = () => {
encryptedForm.value = encryptionManager.encryptFormData(formData.value)
}
// 测试存储加密
const testStorageEncryption = () => {
const testData = {
userId: '12345',
token: 'secret-token-abc123',
settings: { theme: 'dark' },
}
encryptionManager.setEncryptedStorage('user_data', testData)
const retrieved = encryptionManager.getEncryptedStorage('user_data')
alert(JSON.stringify(retrieved, null, 2))
}
</script>
<template>
<div class="encryption-demo">
<h2>数据加密演示</h2>
<!-- AES加密 -->
<section class="demo-section">
<h3>AES对称加密</h3>
<div class="input-group">
<input v-model="plaintext" placeholder="输入明文" />
<button @click="encryptAES">加密</button>
</div>
<div v-if="aesEncrypted" class="result">
<label>加密结果:</label>
<code>{{ aesEncrypted }}</code>
<button @click="decryptAES">解密</button>
</div>
<div v-if="aesDecrypted" class="result">
<label>解密结果:</label>
<span>{{ aesDecrypted }}</span>
</div>
</section>
<!-- 表单加密 -->
<section class="demo-section">
<h3>表单数据加密</h3>
<div class="form-data">
<div
v-for="(value, key) in formData"
:key="key"
class="form-item"
>
<label>{{ key }}:</label>
<input v-model="formData[key]" />
</div>
</div>
<button @click="encryptForm" class="encrypt-btn">
加密敏感字段
</button>
<div v-if="encryptedForm" class="encrypted-result">
<pre>{{ JSON.stringify(encryptedForm, null, 2) }}</pre>
</div>
</section>
<!-- 存储加密 -->
<section class="demo-section">
<h3>LocalStorage加密存储</h3>
<button @click="testStorageEncryption" class="test-btn">
测试加密存储
</button>
<p class="hint">
点击后会在localStorage中加密存储数据,然后读取并解密
</p>
</section>
</div>
</template>
<style scoped>
.encryption-demo {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.demo-section {
margin-bottom: 40px;
padding: 24px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.input-group {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.input-group input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.result {
margin-top: 16px;
padding: 16px;
background: #f5f7fa;
border-radius: 4px;
}
code {
display: block;
padding: 8px;
background: #282c34;
color: #abb2bf;
border-radius: 4px;
overflow-x: auto;
margin: 8px 0;
}
</style>
五、数字水印防截图
5.1 水印管理器
watermark-manager.js
export class WatermarkManager {
constructor(options = {}) {
this.text = options.text || 'Confidential'
this.fontSize = options.fontSize || 14
this.color = options.color || 'rgba(0, 0, 0, 0.1)'
this.rotate = options.rotate || -20
this.zIndex = options.zIndex || 9999
this.watermarkDiv = null
this.observer = null
}
// 创建水印
create() {
// 创建canvas生成水印图片
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = 300
canvas.height = 200
ctx.font = `${this.fontSize}px Arial`
ctx.fillStyle = this.color
ctx.rotate((this.rotate * Math.PI) / 180)
ctx.fillText(this.text, 50, 100)
// 将canvas转为base64
const base64 = canvas.toDataURL()
// 创建水印div
this.watermarkDiv = document.createElement('div')
this.watermarkDiv.id = 'watermark-layer'
this.watermarkDiv.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: ${this.zIndex};
background-image: url(${base64});
background-repeat: repeat;
`
document.body.appendChild(this.watermarkDiv)
// 防止水印被删除
this.protectWatermark()
}
// 创建明水印
createVisibleWatermark(text, options = {}) {
const div = document.createElement('div')
div.className = 'visible-watermark'
div.textContent = text
div.style.cssText = `
position: fixed;
bottom: ${options.bottom || 20}px;
right: ${options.right || 20}px;
padding: 8px 16px;
background: rgba(0, 0, 0, 0.7);
color: white;
border-radius: 4px;
font-size: 12px;
z-index: ${this.zIndex};
pointer-events: none;
`
document.body.appendChild(div)
return div
}
// 创建暗水印(Canvas指纹)
createCanvasFingerprint(userId) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = 200
canvas.height = 50
// 绘制文本
ctx.font = '14px Arial'
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)' // 几乎不可见
ctx.fillText(`ID:${userId}`, 10, 30)
return canvas.toDataURL()
}
// 防止水印被删除(使用MutationObserver)
protectWatermark() {
this.observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
// 检查水印div是否被删除
if (mutation.type === 'childList') {
const watermark = document.getElementById('watermark-layer')
if (!watermark) {
console.warn('Watermark removed, recreating...')
this.create()
}
}
// 检查水印样式是否被修改
if (mutation.type === 'attributes') {
if (mutation.target.id === 'watermark-layer') {
console.warn('Watermark modified, recreating...')
this.remove()
this.create()
}
}
})
})
this.observer.observe(document.body, {
childList: true,
attributes: true,
subtree: true,
})
}
// 移除水印
remove() {
if (this.watermarkDiv) {
this.watermarkDiv.remove()
this.watermarkDiv = null
}
if (this.observer) {
this.observer.disconnect()
this.observer = null
}
}
// 更新水印内容
update(newText) {
this.text = newText
this.remove()
this.create()
}
// 防止截图(检测截图快捷键)
preventScreenshot() {
document.addEventListener('keydown', (e) => {
// Windows: PrtSc, Alt+PrtSc, Win+Shift+S
// Mac: Cmd+Shift+3/4/5
if (
e.key === 'PrintScreen' ||
(e.metaKey && e.shiftKey && ['3', '4', '5'].includes(e.key)) ||
(e.metaKey && e.shiftKey && e.key === 's')
) {
e.preventDefault()
alert('截图功能已被禁用')
// 记录截图尝试
this.logScreenshotAttempt()
}
})
// 检测失焦(可能切换到截图工具)
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.warn('Page hidden, possible screenshot attempt')
}
})
}
// 记录截图尝试
logScreenshotAttempt() {
const log = {
type: 'screenshot_attempt',
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
}
// 发送到服务器
fetch('/api/security/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(log),
})
}
}
export default new WatermarkManager()
5.2 水印演示组件
WatermarkDemo.vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { WatermarkManager } from './watermark-manager.js'
const watermarkManager = ref(null)
const watermarkText = ref('Confidential Document')
const showWatermark = ref(false)
const watermarkStyle = ref({
fontSize: 16,
color: 'rgba(0, 0, 0, 0.1)',
rotate: -20,
})
const userId = ref('USER_12345')
// 创建水印
const createWatermark = () => {
if (watermarkManager.value) {
watermarkManager.value.remove()
}
watermarkManager.value = new WatermarkManager({
text: watermarkText.value,
fontSize: watermarkStyle.value.fontSize,
color: watermarkStyle.value.color,
rotate: watermarkStyle.value.rotate,
})
watermarkManager.value.create()
showWatermark.value = true
}
// 移除水印
const removeWatermark = () => {
if (watermarkManager.value) {
watermarkManager.value.remove()
showWatermark.value = false
}
}
// 启用防截图
const enableScreenshotProtection = () => {
if (watermarkManager.value) {
watermarkManager.value.preventScreenshot()
alert('已启用防截图保护,按PrintScreen或截图快捷键将被拦截')
}
}
// 尝试删除水印(演示防护)
const tryRemoveWatermark = () => {
const watermarkDiv = document.getElementById('watermark-layer')
if (watermarkDiv) {
watermarkDiv.remove()
setTimeout(() => {
alert('水印被删除后会自动恢复')
}, 1000)
}
}
onMounted(() => {
createWatermark()
})
onUnmounted(() => {
removeWatermark()
})
</script>
<template>
<div class="watermark-demo">
<div class="demo-header">
<h2>数字水印防截图演示</h2>
<p class="subtitle">演示水印创建和防护机制</p>
</div>
<!-- 水印配置 -->
<div class="config-section">
<h3>水印配置</h3>
<div class="config-form">
<div class="form-group">
<label>水印文本:</label>
<input v-model="watermarkText" type="text" />
</div>
<div class="form-group">
<label>字体大小:</label>
<input
v-model.number="watermarkStyle.fontSize"
type="range"
min="10"
max="30"
/>
<span>{{ watermarkStyle.fontSize }}px</span>
</div>
<div class="form-group">
<label>旋转角度:</label>
<input
v-model.number="watermarkStyle.rotate"
type="range"
min="-45"
max="0"
/>
<span>{{ watermarkStyle.rotate }}°</span>
</div>
<div class="form-group">
<label>用户ID (暗水印):</label>
<input v-model="userId" type="text" />
</div>
</div>
<div class="action-buttons">
<button @click="createWatermark" class="create-btn">
{{ showWatermark ? '更新水印' : '创建水印' }}
</button>
<button
@click="removeWatermark"
class="remove-btn"
v-if="showWatermark"
>
移除水印
</button>
</div>
</div>
<!-- 防护功能 -->
<div class="protection-section">
<h3>防护功能</h3>
<div class="protection-grid">
<div class="protection-card">
<h4>防删除保护</h4>
<p>使用MutationObserver监听DOM变化,水印被删除后自动恢复</p>
<button @click="tryRemoveWatermark" class="test-btn">
测试删除水印
</button>
</div>
<div class="protection-card">
<h4>防截图保护</h4>
<p>拦截截图快捷键,记录截图尝试</p>
<button
@click="enableScreenshotProtection"
class="test-btn"
>
启用防截图
</button>
</div>
<div class="protection-card">
<h4>用户追踪</h4>
<p>在水印中嵌入用户ID,泄露后可追溯</p>
<div class="user-info">
当前用户: <code>{{ userId }}</code>
</div>
</div>
</div>
</div>
<!-- 内容区域(演示水印效果) -->
<div class="content-section">
<h3>机密文档内容</h3>
<div class="document-content">
<h4>公司机密文档</h4>
<p>
这是一份机密文档的内容。页面上的水印可以防止未授权的截图和复制。
</p>
<p>
水印包含文档标识和用户ID,如果内容泄露可以追溯到具体用户。
</p>
<p>请注意保护文档安全,不要将内容分享给未授权人员。</p>
<div class="data-table">
<table>
<thead>
<tr>
<th>项目</th>
<th>数据</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr>
<td>季度营收</td>
<td>¥5,000,000</td>
<td>机密</td>
</tr>
<tr>
<td>客户数据</td>
<td>10,000+</td>
<td>机密</td>
</tr>
<tr>
<td>市场份额</td>
<td>25%</td>
<td>机密</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 技术说明 -->
<div class="tech-doc-section">
<h3>技术实现说明</h3>
<div class="tech-card">
<h4>1. Canvas水印生成</h4>
<pre><code>const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.font = '14px Arial'
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'
ctx.rotate(-20 * Math.PI / 180)
ctx.fillText('Confidential', 50, 100)
const base64 = canvas.toDataURL()</code></pre>
</div>
<div class="tech-card">
<h4>2. MutationObserver防护</h4>
<pre><code>const observer = new MutationObserver((mutations) => {
const watermark = document.getElementById('watermark')
if (!watermark) {
// 水印被删除,重新创建
createWatermark()
}
})
observer.observe(document.body, {
childList: true,
attributes: true,
subtree: true
})</code></pre>
</div>
<div class="tech-card">
<h4>3. 截图检测</h4>
<pre><code>document.addEventListener('keydown', (e) => {
if (e.key === 'PrintScreen') {
e.preventDefault()
// 记录截图尝试
logScreenshotAttempt()
}
})</code></pre>
</div>
</div>
<!-- 最佳实践 -->
<div class="best-practices-section">
<h3>水印使用最佳实践</h3>
<ul class="practices-list">
<li>水印透明度要适中,既要不影响阅读,又要足够清晰</li>
<li>在水印中嵌入用户ID和时间戳,便于追溯</li>
<li>使用MutationObserver防止水印被删除</li>
<li>结合Canvas指纹技术实现暗水印</li>
<li>监听截图快捷键,记录截图行为</li>
<li>对于高度机密内容,可以禁用右键和选择</li>
<li>定期审计水印日志,发现异常行为</li>
<li>水印要覆盖整个页面,避免裁剪后去除</li>
</ul>
</div>
</div>
</template>
<style scoped>
.watermark-demo {
padding: 20px;
background: #f5f7fa;
min-height: 100vh;
}
.demo-header {
text-align: center;
margin-bottom: 40px;
}
.demo-header h2 {
margin: 0 0 10px 0;
font-size: 32px;
color: #303133;
}
.subtitle {
margin: 0;
font-size: 16px;
color: #909399;
}
/* 各个section */
.config-section,
.protection-section,
.content-section,
.tech-doc-section,
.best-practices-section {
margin-bottom: 30px;
padding: 24px;
background: white;
border-radius: 8px;
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
color: #303133;
}
/* 配置表单 */
.config-form {
margin-bottom: 20px;
}
.form-group {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.form-group label {
min-width: 120px;
font-size: 14px;
color: #606266;
font-weight: 600;
}
.form-group input[type='text'] {
flex: 1;
padding: 8px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
.form-group input[type='range'] {
flex: 1;
}
.action-buttons {
display: flex;
gap: 12px;
}
.create-btn,
.remove-btn {
padding: 10px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
.create-btn {
background: #67c23a;
color: white;
}
.remove-btn {
background: #f56c6c;
color: white;
}
/* 防护功能 */
.protection-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.protection-card {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
border-left: 4px solid #409eff;
}
.protection-card h4 {
margin: 0 0 12px 0;
font-size: 16px;
color: #303133;
}
.protection-card p {
margin: 0 0 16px 0;
font-size: 14px;
color: #606266;
line-height: 1.6;
}
.test-btn {
padding: 8px 16px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.user-info {
font-size: 14px;
color: #606266;
}
.user-info code {
padding: 4px 8px;
background: white;
border-radius: 4px;
color: #409eff;
font-family: 'Courier New', monospace;
}
/* 内容区域 */
.document-content {
padding: 24px;
background: #fafafa;
border-radius: 8px;
border: 1px solid #e4e7ed;
}
.document-content h4 {
margin: 0 0 16px 0;
font-size: 20px;
color: #303133;
}
.document-content p {
margin: 0 0 16px 0;
font-size: 14px;
line-height: 1.8;
color: #606266;
}
.data-table {
margin-top: 24px;
overflow-x: auto;
}
.data-table table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
padding: 12px;
text-align: left;
border: 1px solid #e4e7ed;
}
.data-table th {
background: #f5f7fa;
font-weight: 600;
color: #303133;
}
.data-table td {
color: #606266;
}
/* 技术说明 */
.tech-card {
margin-bottom: 24px;
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.tech-card h4 {
margin: 0 0 12px 0;
font-size: 16px;
color: #303133;
}
.tech-card pre {
margin: 0;
padding: 16px;
background: #282c34;
border-radius: 4px;
overflow-x: auto;
}
.tech-card code {
font-size: 13px;
line-height: 1.6;
color: #abb2bf;
font-family: 'Courier New', monospace;
}
/* 最佳实践 */
.practices-list {
margin: 0;
padding-left: 24px;
}
.practices-list li {
font-size: 14px;
line-height: 2;
color: #606266;
}
</style>
六、简历描述模板
前端加密与安全防护 (2024.08 - 至今)
负责公司前端数据加密和安全防护体系建设,实现接口签名、代码混淆、数据加密和数字水印等安全方案。
核心职责
- 实现HMAC-SHA256接口签名算法,防止接口被恶意调用
- 配置JavaScript代码混淆,保护核心业务逻辑
- 开发数据加密模块,实现AES/RSA混合加密
- 实施数字水印方案,防止敏感内容截图泄露
- 建立前端安全防护机制,包括防调试、反爬虫
技术实现
- 使用CryptoJS实现HMAC-SHA256签名,添加时间戳和nonce防重放
- 通过javascript-obfuscator进行代码混淆,配置控制流扁平化
- 实现AES对称加密和RSA非对称加密的混合加密方案
- 基于Canvas API生成数字水印,使用MutationObserver防删除
- 配置Webpack打包时自动混淆和压缩代码
项目成果
- 接口签名实施后,恶意请求拦截率100%
- 代码混淆后,逆向分析难度提升80%
- 敏感数据加密存储,通过安全审计
- 数字水印覆盖核心页面,有效防止内容泄露
- 整体安全防护能力提升,未发生安全事故
七、SOP标准回答
面试问题: 如何实现接口签名防止被恶意调用?
标准回答
"接口签名是防止接口被恶意调用的重要手段。我实现的方案基于HMAC-SHA256算法。
首先,客户端和服务端约定好一个密钥appSecret,这个密钥只保存在服务端,前端不能暴露。前端只存储appKey用于标识应用。
签名流程分五步。第一步,添加公共参数,包括appKey、时间戳和随机数nonce。时间戳用于防止重放攻击,nonce保证签名唯一性。
第二步,参数排序。把所有参数按key的字母顺序排序,保证客户端和服务端计算签名时参数顺序一致。
第三步,拼接参数字符串。格式是key1=value1&key2=value2,然后加上&key=密钥。
第四步,计算签名。用HMAC-SHA256算法,把参数字符串和密钥一起计算哈希值。
第五步,把签名添加到请求参数中,一起发送给服务端。
服务端验证时,先检查时间戳是否在有效期内,比如5分钟。然后用相同的方法重新计算签名,对比客户端传来的签名是否一致。如果不一致说明参数被篡改,拒绝请求。
为了方便使用,我封装了Axios拦截器,自动给每个请求添加签名。开发人员不需要关心签名细节,直接调用接口就行。
这个方案的核心是密钥不能泄露。实际项目中,appSecret存在服务端环境变量里,前端根本接触不到。即使有人抓包拿到签名,也无法伪造新的请求,因为每次请求的时间戳和nonce都不同。
实施后,我们拦截了所有未签名的恶意请求,接口安全性大幅提升。"
面试问题: 前端代码混淆有什么用?如何实现?
标准回答
"代码混淆的主要目的是保护核心业务逻辑,提高逆向分析的难度。
前端代码部署到生产环境后,任何人都能通过浏览器开发者工具查看源码。如果不做混淆,核心算法、关键逻辑、甚至某些密钥都可能被竞争对手轻易获取。混淆可以让代码变得难以理解,增加破解成本。
我用的混淆工具是javascript-obfuscator,它有很多混淆策略。
第一是标识符重命名。把变量名、函数名都替换成_0x1a2b这种16进制字符串,让代码失去可读性。
第二是控制流扁平化。把if-else、switch等控制流改成状态机,通过一个大的switch-case循环执行,破坏代码的结构。
第三是字符串加密。把所有字符串收集到一个数组中,用base64编码,运行时再解码。这样直接搜索字符串找不到关键逻辑。
第四是死代码注入。插入一些永远不会执行的代码,干扰分析。
第五是自我防御。在代码中插入检测,如果发现被调试或被格式化,就停止运行。
第六是禁用console。把所有console.log、console.debug删除或替换,防止调试时输出信息。
在Webpack配置中,我添加了javascript-obfuscator插件,在生产环境构建时自动混淆。开发环境不混淆,方便调试。
需要注意的是,混淆不是加密,只是提高破解难度,不能完全防止逆向。真正敏感的逻辑应该放在服务端。但对于必须在前端实现的算法,比如数据可视化、图表计算等,混淆能起到一定保护作用。
混淆后,代码体积会增加20-30%,运行性能可能略有下降,需要权衡。对于核心模块强混淆,非核心模块轻混淆。"
八、难点与亮点分析
难点1: 如何在前端实现安全的密钥管理?
问题场景: 前端加密需要密钥,但密钥不能暴露在代码中。
解决方案
class SecureKeyManager {
constructor() {
this.sessionKey = null
}
// 从服务端获取临时会话密钥
async fetchSessionKey() {
// 1. 客户端生成密钥对
const keyPair = await this.generateKeyPair()
// 2. 将公钥发送给服务端
const response = await fetch('/api/key/exchange', {
method: 'POST',
body: JSON.stringify({
publicKey: keyPair.publicKey,
}),
})
const data = await response.json()
// 3. 服务端用客户端公钥加密AES密钥返回
// 4. 客户端用私钥解密得到AES密钥
this.sessionKey = await this.decryptWithPrivateKey(
data.encryptedKey,
keyPair.privateKey
)
return this.sessionKey
}
// 使用Web Crypto API生成密钥对
async generateKeyPair() {
const keyPair = await crypto.subtle.generateKey(
{
name: 'RSA-OAEP',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256',
},
true,
['encrypt', 'decrypt']
)
const publicKey = await crypto.subtle.exportKey(
'spki',
keyPair.publicKey
)
return {
publicKey: this.arrayBufferToBase64(publicKey),
privateKey: keyPair.privateKey,
}
}
// 用私钥解密
async decryptWithPrivateKey(encryptedData, privateKey) {
const decrypted = await crypto.subtle.decrypt(
{ name: 'RSA-OAEP' },
privateKey,
this.base64ToArrayBuffer(encryptedData)
)
return new TextDecoder().decode(decrypted)
}
// 辅助方法
arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer)
let binary = ''
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i])
}
return btoa(binary)
}
base64ToArrayBuffer(base64) {
const binary = atob(base64)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i)
}
return bytes.buffer
}
}
关键点
- 密钥不存在前端代码中
- 每次会话生成新的临时密钥
- 使用非对称加密传输对称密钥
- 私钥只存在内存,不持久化
难点2: 如何实现真正有效的防调试?
问题场景: 普通的debugger语句很容易被跳过。
解决方案
class AntiDebug {
constructor() {
this.isDebuggerPresent = false
}
// 方法1: 无限debugger
infiniteDebugger() {
setInterval(() => {
debugger
}, 100)
}
// 方法2: 检测控制台
detectDevTools() {
const threshold = 160
const devtools = /./
devtools.toString = function () {
this.isOpen = true
}
setInterval(() => {
const before = new Date()
// 触发toString
console.log(devtools)
const after = new Date()
// 如果打开了控制台,console.log会有延迟
if (after - before > threshold) {
this.onDebugDetected()
}
}, 1000)
}
// 方法3: 检测窗口尺寸变化
detectWindowResize() {
let lastWidth = window.outerWidth
let lastHeight = window.outerHeight
setInterval(() => {
// 开发者工具会改变窗口尺寸
if (
window.outerWidth !== lastWidth ||
window.outerHeight !== lastHeight
) {
this.onDebugDetected()
}
lastWidth = window.outerWidth
lastHeight = window.outerHeight
}, 500)
}
// 方法4: 检测函数toString
protectFunction(fn) {
const originalToString = Function.prototype.toString
Function.prototype.toString = function () {
if (this === fn) {
this.onDebugDetected()
return 'function () { [native code] }'
}
return originalToString.call(this)
}
}
// 检测到调试时的处理
onDebugDetected() {
// 方案1: 跳转到其他页面
window.location.href = 'about:blank'
// 方案2: 清空页面内容
// document.body.innerHTML = ''
// 方案3: 执行无限循环
// while(true) {}
}
// 启动所有防护
enable() {
this.detectDevTools()
this.detectWindowResize()
// 在关键函数上添加保护
const criticalFunctions = [window.fetch, XMLHttpRequest.prototype.open]
criticalFunctions.forEach((fn) => {
this.protectFunction(fn)
})
}
}
亮点: 智能水印系统
创新点
- 根据用户权限动态调整水印
- 水印包含加密的用户信息
- 支持明暗水印结合
class SmartWatermark {
constructor() {
this.config = this.loadConfig()
}
// 根据用户权限配置水印
loadConfig() {
const userLevel = this.getUserLevel()
switch (userLevel) {
case 'guest':
return {
visible: true,
text: '访客模式 - 仅供预览',
opacity: 0.3,
preventScreenshot: true,
}
case 'normal':
return {
visible: true,
text: `${this.getUserName()} - 内部资料`,
opacity: 0.15,
preventScreenshot: true,
}
case 'vip':
return {
visible: false,
darkWatermark: true,
preventScreenshot: false,
}
default:
return {
visible: true,
text: '未授权访问',
opacity: 0.5,
preventScreenshot: true,
}
}
}
// 创建智能水印
create() {
if (this.config.visible) {
this.createVisibleWatermark()
}
if (this.config.darkWatermark) {
this.createDarkWatermark()
}
if (this.config.preventScreenshot) {
this.preventScreenshot()
}
}
// 创建暗水印(用户不可见,但截图会显示)
createDarkWatermark() {
const userId = this.getUserId()
const timestamp = Date.now()
// 加密用户信息
const encoded = btoa(`${userId}:${timestamp}`)
// 在页面插入不可见div
const div = document.createElement('div')
div.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
`
div.setAttribute('data-user-trace', encoded)
document.body.appendChild(div)
}
getUserLevel() {
// 从token或后端获取用户等级
return 'normal'
}
getUserName() {
return 'John Doe'
}
getUserId() {
return 'USER_12345'
}
}