一、签名验证的意义
签名验证用于确保数据在传输过程中未被篡改,常见于API请求和消息传递。通过生成签名(哈希值),接收方可用相同算法和密钥重新计算并校验签名,从而保证数据的完整性和真实性。二、服务端(PHP)签名生成流程
- 移除原有签名字段:unset($data['sign']);
- 拼接参数字符串: 使用 http_build_query 将所有参数按 参数=参数值 形式用 & 连接。
functionsign_create($data, $key){ unset($data['sign']); ksort($data); $string = http_build_query($data, '', '&'); $string = $string.'&key='.$key; $string = hash('sha256', $string); return mb_strtoupper($string);}functionsign_verify($data, $key){ if(empty($data['sign'])) { throw new ValidateException('缺少sign参数'); } if(!isset($data['timestamp'])) { throw new ValidateException('缺少timestamp参数'); } if(!isset($data['nonce']) || 32 != \strlen($data['nonce'])) { throw new ValidateException('缺少随机字符串或长度不符合要求'); } if(abs(time() * 1000 - $data['timestamp']) > 60000) { throw new ValidateException('签名已过期'); } if(self::create($data, $key) != $data['sign']) { throw new ValidateException('签名不一致'); } return true;}$get = $request->get();$post = $request->post();$data = Arr::only($request->header(), ['sign', 'timestamp', 'nonce']);$data['token'] = $request->token();$data['params'] = http_build_query($get);$data['data'] = http_build_query($post);$data['platform'] = $request->platform();sign_create($data, $secret);
三、前端(Vue)签名生成流程
前端通过当前请求时间、随机字符串、GET/POST参数、登录token、加密密钥等关键参数参与签名验证,防止数据被篡改。import CryptoJS from 'crypto-js'import qs from 'qs'export function createSign(data) { const token = getToken() || '' const timestamp = Math.floor(Date.now()) const nonce = randomStr(32) const key = '加密秘钥,于服务端约定好规则' const platform = 'pcadmin' data.token = token data.nonce = nonce data.timestamp = timestamp data.platform = platform data = ksort(data) const signStr = httpBuildQuery(data) + '&key=' + key const sign = CryptoJS.SHA256(signStr).toString().toUpperCase() return { sign, nonce, timestamp, platform }}
常用工具函数
- httpBuildQuery:适配PHP的http_build_query,使用qs库实现序列化。
- buildHeaderData:根据请求参数构建签名和请求头。
- ksort:模拟 PHP 的 ksort 函数:按对象/数组的键名升序排序
export function httpBuildQuery(data) { return qs.stringify(data, { format: 'RFC1738' })}export function randomStr(length) { let result = '' const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' for (let i = 0; i < length; i++) { const randomIndex = Math.floor(Math.random() * characters.length) result += characters.charAt(randomIndex) } return result}export function buildHeaderData(params = {}, data = {}) { const signData = { params: httpBuildQuery(params || {}), data: httpBuildQuery(data || {}) } return createSign(signData)}/** * 模拟 PHP 的 ksort 函数:按对象/数组的键名升序排序 * @param {Object|Array} obj 要排序的对象/数组 * @param {boolean} [preserveNumericKeys=true] 是否保留数字键的原始类型(true=保留,false=转为字符串) * @returns {Object} 按键名升序排序后的新对象 */export function ksort(obj, preserveNumericKeys = true) { // 1. 提取键值对并处理键名类型(区分数字/字符串) const entries = Object.entries(obj).map(([key, value]) => { // 判断是否为纯数字键(如 '123' → 123,'123a' → 保留字符串) const isNumericKey = /^\d+$/.test(key) const processedKey = isNumericKey ? Number(key) : key return { key: processedKey, originalKey: key, value } }) // 2. 按键名排序(核心逻辑) entries.sort((a, b) => { const keyA = a.key const keyB = b.key // 数字键 vs 数字键:按数值大小排序 if (typeof keyA === 'number' && typeof keyB === 'number') { return keyA - keyB } // 数字键 vs 字符串键:数字键在前 if (typeof keyA === 'number') return -1 if (typeof keyB === 'number') return 1 // 字符串键 vs 字符串键:按 UTF-16 编码升序(兼容 PHP 的字符排序) return keyA.localeCompare(keyB, 'en-US', { sensitivity: 'base' }) }) // 3. 重构排序后的对象(保留原键名) const sortedObj = {} entries.forEach(({ originalKey, value }) => { // 可选:是否将数字键转回字符串(如 1 → '1',与原生对象键名格式一致) const finalKey = preserveNumericKeys ? originalKey : String(originalKey) sortedObj[finalKey] = value }) return sortedObj}
四、依赖说明
qs:用于解析和序列化查询字符串,保证与PHP兼容。crypto-js:流行的JavaScript加密库,支持多种加密算法(如SHA256)。总结
- 推荐采用SHA256等安全算法,并将结果统一为大写,提升安全性和兼容性。