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 持久化与权限存储

相关推荐
IT_陈寒2 分钟前
Redis的KEYS命令把我搞崩溃了,改用SCAN才活过来
前端·人工智能·后端
Jackson__33 分钟前
为什么 Agent 越聊越慢?聊聊 Context(上下文)管理
前端·agent·ai编程
跨境数据猎手41 分钟前
反向海淘实战|独立站搭建+国内电商API对接
开发语言·爬虫·架构
KaMeidebaby1 小时前
卡梅德生物技术快报|抗体合成:多肽抗体合成工程化方案:Nsp2 保守肽多抗制备与多维度验证
前端·网络·数据库·人工智能·算法
青禾网络1 小时前
前端做音画匹配这件事,我从"随机塞"到"AI 自动对齐"
前端·github
没钥匙的锁11 小时前
05-Java面向对象构造器与封装
java·开发语言
仙人球部落1 小时前
-python-LangGraph框架(3-31-LangGraph 「合并式状态管理」的原理与实践)
开发语言·javascript·python
2401_894915531 小时前
Geo搜索优化排名源码部署搭建全流程详解
android·开发语言·flask·php·精选
2601_963771372 小时前
Hardening Enterprise WordPress Sites Against REST API Leaks and Bad Headers
前端·windows·word·php
蓝银草同学2 小时前
Stream 实战:博客列表排序、过滤与分页(AI 辅助学习 Java 8)
java·前端·后端