P19 | 前端加密通信层 pikachuNetwork.js 完整实现
💰 付费文章 | 第三阶段:Web 管理台
pikachuNetwork.js 是什么?
它是前端所有 API 请求的统一入口,负责:
- 请求加密:AES+RSA 双重加密
- 响应解密:自动解密后端返回的数据
- Token 管理:自动附加 Authorization 头
- 错误处理:统一处理 401/500 等错误
- 请求/响应拦截:埋点、日志等
所有页面和组件不应该直接使用 axios ,而是通过 post() 方法发请求。
完整实现
// pikachuNetwork.js
import axios from 'axios'
import JSEncrypt from 'jsencrypt'
import CryptoJS from 'crypto-js'
import { useUserStore } from '@/stores/userStore'
import { ElMessage, ElMessageBox } from 'element-plus'
import router from '@/router'
const BASE_URL = import.meta.env.VITE_API_BASE_URL || '/pikachu'
// AES 密钥长度 16 字节
const AES_KEY_LENGTH = 16
// ============ 加密工具 ============
// 生成随机 AES 密钥
function generateAESKey() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let key = ''
for (let i = 0; i < AES_KEY_LENGTH; i++) {
key += chars.charAt(Math.floor(Math.random() * chars.length))
}
return key
}
// AES 加密(ECB 模式,PKCS7 填充)
function aesEncrypt(data, key) {
const encrypted = CryptoJS.AES.encrypt(
CryptoJS.enc.Utf8.parse(data),
CryptoJS.enc.Utf8.parse(key),
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}
)
return encrypted.toString() // Base64
}
// AES 解密
function aesDecrypt(encryptedData, key) {
const decrypted = CryptoJS.AES.decrypt(
encryptedData,
CryptoJS.enc.Utf8.parse(key),
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}
)
return decrypted.toString(CryptoJS.enc.Utf8)
}
// RSA 加密(用服务器公钥)
function rsaEncrypt(data, publicKey) {
const encrypt = new JSEncrypt()
encrypt.setPublicKey(publicKey)
const encrypted = encrypt.encrypt(data)
if (!encrypted) throw new Error('RSA 加密失败,请检查公钥')
return encrypted
}
// ============ 核心请求方法 ============
// 主请求方法:所有 API 调用都走这里
export async function post(url, params, options = {}) {
const { skipEncrypt = false, skipToken = false } = options
const userStore = useUserStore()
// 1. 构建请求体
let requestBody = params
let aesKey = ''
// 2. 加密(可跳过,如获取公钥的接口)
if (!skipEncrypt && userStore.serverPublicKey) {
aesKey = generateAESKey()
const encryptKey = rsaEncrypt(aesKey, userStore.serverPublicKey)
const encryptData = aesEncrypt(JSON.stringify(params), aesKey)
requestBody = { encryptKey, encryptData }
}
// 3. 构建请求头
const headers = { 'Content-Type': 'application/json' }
if (!skipToken && userStore.token) {
headers['Authorization'] = `Bearer ${userStore.token}`
}
if (aesKey) {
headers['X-AES-Key'] = aesKey // 把 AES 密钥传给后端用于加密响应
}
// 4. 发送请求
const startTime = Date.now()
let response
try {
response = await axios({
method: 'POST',
url: `${BASE_URL}${url}`,
data: requestBody,
headers,
timeout: 30000
})
} catch (error) {
// 网络错误
ElMessage.error('网络请求失败,请检查网络连接')
throw error
}
// 5. 埋点:API 调用记录
if (window.__trackSdk) {
window.__trackSdk.trackApiCall(url, 'POST', response.status, Date.now() - startTime)
}
// 6. 解密响应
let result
if (!skipEncrypt && response.data?.encryptData) {
const decrypted = aesDecrypt(response.data.encryptData, aesKey)
result = JSON.parse(decrypted)
} else {
result = response.data
}
// 7. 处理业务状态码
if (result.code === 200) {
return result.body // 成功:直接返回 body 数据
} else if (result.code === 401) {
// 未登录 / Token 过期
userStore.clearToken()
const currentPath = router.currentRoute.value.fullPath
if (currentPath !== '/login') {
ElMessageBox.alert('登录已过期,请重新登录', '提示', {
confirmButtonText: '去登录',
callback: () => router.push('/login')
})
}
throw new Error(result.msg || '未登录')
} else {
ElMessage.error(result.msg || '操作失败')
throw new Error(result.msg)
}
}
// GET 请求(较少使用,统一转 POST)
export async function get(url, params = {}) {
return post(url, params)
}
// 文件上传(不走加密通道)
export async function upload(url, formData) {
const userStore = useUserStore()
const headers = {}
if (userStore.token) {
headers['Authorization'] = `Bearer ${userStore.token}`
}
const response = await axios.post(`${BASE_URL}${url}`, formData, {
headers,
timeout: 120000 // 上传超时2分钟
})
if (response.data.code === 200) {
return response.data.body
} else {
throw new Error(response.data.msg)
}
}
export default { post, get, upload }
使用示例
// api/townApi.js
import { post } from './pikachuNetwork'
// 景点列表
export const getAttractionList = (params) => post('/web/attraction/list', params)
// 景点详情
export const getAttractionDetail = (id) => post('/web/attraction/detail', { id })
// 搜索景点
export const searchAttractions = (params) => post('/web/attraction/search', params)
// 收藏/取消收藏
export const toggleFavorite = (attractionId) => post('/web/favorite/toggle', { attractionId })
// 获取收藏列表
export const getFavoriteList = (params) => post('/web/favorite/list', params)
<!-- 在组件中使用 -->
<script setup>
import { getAttractionList } from '@/api/townApi'
import { ref, onMounted } from 'vue'
const list = ref([])
const total = ref(0)
async function loadData() {
const result = await getAttractionList({ page: 1, pageSize: 10 })
list.value = result.records
total.value = result.total
}
onMounted(loadData)
</script>