P19 | 前端加密通信层 pikachuNetwork.js 完整实现

P19 | 前端加密通信层 pikachuNetwork.js 完整实现

💰 付费文章 | 第三阶段:Web 管理台


pikachuNetwork.js 是什么?

它是前端所有 API 请求的统一入口,负责:

  1. 请求加密:AES+RSA 双重加密
  2. 响应解密:自动解密后端返回的数据
  3. Token 管理:自动附加 Authorization 头
  4. 错误处理:统一处理 401/500 等错误
  5. 请求/响应拦截:埋点、日志等

所有页面和组件不应该直接使用 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>

下一篇

P20 → Pinia 状态管理:Token 持久化与权限存储

相关推荐
不爱吃炸鸡柳2 小时前
数据结构精讲:树 → 二叉树 → 堆 从入门到实战
开发语言·数据结构
网络安全许木2 小时前
自学渗透测试第21天(基础命令复盘与DVWA熟悉)
开发语言·网络安全·渗透测试·php
t***5442 小时前
如何在Dev-C++中使用Clang编译器
开发语言·c++
码界筑梦坊2 小时前
93-基于Python的中药药材数据可视化分析系统
开发语言·python·信息可视化
qq_12084093712 小时前
Three.js 工程向:Draw Call 预算治理与渲染批处理实践
前端·javascript
Cosmoshhhyyy3 小时前
《Effective Java》解读第49条:检查参数的有效性
java·开发语言
棋子入局3 小时前
C语言制作消消乐游戏(2)
c语言·开发语言·游戏
布谷歌3 小时前
常见的OOM错误 ( OutOfMemoryError全类型详解)
java·开发语言
WangJunXiang63 小时前
GFS分布式文件系统
开发语言·php