From 9762e15b8b83f61fb640a05ab3fefa692f89b230 Mon Sep 17 00:00:00 2001 From: landaiqing <3517283258@qq.com> Date: Mon, 29 Apr 2024 16:00:47 +0800 Subject: [PATCH] feat: add localforage --- .env.development | 8 +- .env.production | 10 +- .eslintrc.cjs | 1 + package.json | 2 + pnpm-lock.yaml | 26 +++++ src/api/user/index.ts | 6 +- src/main.tsx | 12 +- src/store/modules/user.ts | 26 ++--- src/utils/axios/request.ts | 184 +++++++++++++++++++++++++++++++ src/utils/axios/service.ts | 139 ++++++++++++++++++----- src/utils/axios/web.ts | 14 +++ src/utils/encrypt/encrypt.ts | 169 ++++++++++++++++++++++++++++ src/utils/localStorage/config.ts | 174 ++++++++++++++--------------- src/utils/localforage/index.ts | 74 +++++++++++++ src/views/Home/index.tsx | 15 ++- src/vite-env.d.ts | 2 + vite.config.ts | 11 ++ 17 files changed, 726 insertions(+), 147 deletions(-) create mode 100644 src/utils/axios/request.ts create mode 100644 src/utils/axios/web.ts create mode 100644 src/utils/encrypt/encrypt.ts create mode 100644 src/utils/localforage/index.ts diff --git a/.env.development b/.env.development index 25d7e10..665180d 100644 --- a/.env.development +++ b/.env.development @@ -2,7 +2,7 @@ VITE_NODE_ENV='development' # 开发环境 -VITE_APP_BASE_API='/dev-api' +VITE_APP_BASE_API='/api' # 页面 title 前缀 VITE_APP_TITLE=开发环境 @@ -11,3 +11,9 @@ VITE_APP_TITLE=开发环境 VITE_API_BASE_URL='http://127.0.0.1:3000' VITE_TITLE_NAME='五味子云存储' + +# token key +VITE_APP_TOKEN_KEY='token' + +# the upload url +VITE_UPLOAD_URL='http://127.0.0.1:3000' diff --git a/.env.production b/.env.production index 2d51d7b..e79dc52 100644 --- a/.env.production +++ b/.env.production @@ -1,7 +1,7 @@ # 生产环境配置 -VITE_NODE_ENV= 'production' +VITE_NODE_ENV='production' # 生产环境 -VITE_APP_BASE_API = '/api' +VITE_APP_BASE_API='/api' # 页面 title 前缀 VITE_APP_TITLE=生产环境 @@ -10,3 +10,9 @@ VITE_APP_TITLE=生产环境 VITE_API_BASE_URL='' VITE_TITLE_NAME='五味子云存储' + +# token key +VITE_APP_TOKEN_KEY='token' + +# the upload url +VITE_UPLOAD_URL='http://127.0.0.1:3000' diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 255e182..a115b57 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -50,5 +50,6 @@ module.exports = { '@typescript-eslint/no-empty-interface': ['off'], '@typescript-eslint/no-unused-vars': ['off'], '@typescript-eslint/no-non-null-assertion': ['off'], + "no-control-regex": "off" } } diff --git a/package.json b/package.json index f77e0da..9d2c042 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "axios": "^1.6.8", "crypto-js": "^4.2.0", "gsap": "^3.12.5", + "jsencrypt": "^3.3.2", + "localforage": "^1.10.0", "mobx": "^6.12.3", "mobx-persist-store": "^1.1.4", "mobx-react": "^9.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9c8e2c..0d5f259 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,12 @@ dependencies: gsap: specifier: ^3.12.5 version: 3.12.5 + jsencrypt: + specifier: ^3.3.2 + version: 3.3.2 + localforage: + specifier: ^1.10.0 + version: 1.10.0 mobx: specifier: ^6.12.3 version: 6.12.3 @@ -5382,6 +5388,10 @@ packages: hasBin: true requiresBuild: true + /immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + dev: false + /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -5739,6 +5749,10 @@ packages: argparse: 2.0.1 dev: true + /jsencrypt@3.3.2: + resolution: {integrity: sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==} + dev: false + /jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true @@ -5870,6 +5884,12 @@ packages: type-check: 0.4.0 dev: true + /lie@3.1.1: + resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + dependencies: + immediate: 3.0.6 + dev: false + /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -5890,6 +5910,12 @@ packages: pkg-types: 1.0.3 dev: true + /localforage@1.10.0: + resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} + dependencies: + lie: 3.1.1 + dev: false + /locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} diff --git a/src/api/user/index.ts b/src/api/user/index.ts index ca6ddb8..7b553ca 100644 --- a/src/api/user/index.ts +++ b/src/api/user/index.ts @@ -1,5 +1,5 @@ -import service from '@/utils/axios/service.ts' +import web from '@/utils/axios/web.ts' -export function getUserInfo(params: object) { - return service.get('/user/info', params) +export const getPublicKey = () => { + return web.get('/encrypt/getPublicKey') } diff --git a/src/main.tsx b/src/main.tsx index a352083..fe75bb6 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,4 +1,4 @@ -import React from 'react' +// import React from 'react' import ReactDOM from 'react-dom/client' import { RouterProvider, createBrowserRouter } from 'react-router-dom' import './assets/styles/index.less' @@ -8,9 +8,9 @@ import { Provider as MobxProvider } from 'mobx-react' import { RootStore } from '@/store' const router = createBrowserRouter(routeConfig) ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - , + // + + + , + // , ) diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index f33052a..9a69a9e 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -1,22 +1,23 @@ import { action, makeAutoObservable } from 'mobx' import { makePersistable, isHydrated } from 'mobx-persist-store' - +import { handleLocalforage } from '@/utils/localforage' export class useUserStore { - user: any = {} + token: any = '' constructor() { makeAutoObservable( this, { - setUserInfo: action, + setToken: action, }, { autoBind: true }, ) // eslint-disable-next-line @typescript-eslint/ban-ts-comment - makePersistable(this, { + // @ts-ignore + makePersistable(this, { // 在构造函数内使用 makePersistable - name: 'userStore', // 保存的name,用于在storage中的名称标识,只要不和storage中其他名称重复就可以 - properties: ['user'], // 要保存的字段,这些字段会被保存在name对应的storage中,注意:不写在这里面的字段将不会被保存,刷新页面也将丢失:get字段例外。get数据会在数据返回后再自动计算 - storage: window.localStorage, // 保存的位置:看自己的业务情况选择,可以是localStorage,sessionstorage + name: 'token', // 保存的name,用于在storage中的名称标识,只要不和storage中其他名称重复就可以 + properties: ['token'], // 要保存的字段,这些字段会被保存在name对应的storage中,注意:不写在这里面的字段将不会被保存,刷新页面也将丢失:get字段例外。get数据会在数据返回后再自动计算 + storage: handleLocalforage, // 保存的位置:看自己的业务情况选择,可以是localStorage,sessionstorage // 。。还有一些其他配置参数,例如数据过期时间等等,可以康康文档,像storage这种字段可以配置在全局配置里,详见文档 }).then( action(() => { @@ -25,16 +26,13 @@ export class useUserStore { }), ) } - get getUserInfo() { - return this.user - } - get token() { - return this.user ? this.user.token : '' + get getToken() { + return this.token ? this.token : null } get isHydrated() { return isHydrated(this) } - setUserInfo(user: object) { - this.user = user + setToken(token: string) { + this.token = token } } diff --git a/src/utils/axios/request.ts b/src/utils/axios/request.ts new file mode 100644 index 0000000..bdf2e93 --- /dev/null +++ b/src/utils/axios/request.ts @@ -0,0 +1,184 @@ +import axios, { AxiosInstance, AxiosRequestConfig } from 'axios' +import { message } from 'antd' +import { + aesDecrypt, + aesEncrypt, + get16RandomNum, + getRsaKeys, + rsaDecrypt, + rsaEncrypt, +} from '@/utils/encrypt/encrypt.ts' +import { handleLocalforage } from '@/utils/localforage' + +let frontPrivateKey: any +let afterPublicKey: any +async function getAfterPublicKey() { + afterPublicKey = await handleLocalforage.getItem('afterPublicKey') +} +class Request { + private instance: AxiosInstance | undefined + + constructor(config: AxiosRequestConfig) { + this.instance = axios.create(config) + // 全局请求拦截 + this.instance.interceptors.request.use( + (config) => { + if (config.headers['isEncrypt']) { + if (config.method === 'post' || config.method === 'put') { + let privateKey: any + let publicKey: any + getRsaKeys().then((res: any) => { + privateKey = res.privateKey + publicKey = res.publicKey + }) + getAfterPublicKey() + frontPrivateKey = privateKey + + //每次请求生成aeskey + const aesKey = get16RandomNum() + if (afterPublicKey) { + //用登陆后后端生成并返回给前端的的RSA密钥对的公钥将AES16位密钥进行加密 + const aesKeyByRsa = rsaEncrypt(aesKey, afterPublicKey) + + //使用AES16位的密钥将请求报文加密(使用的是加密前的aes密钥) + if (config.data) { + const data = aesEncrypt(aesKey, JSON.stringify(config.data)) + config.data = { + data: data, + aeskey: aesKeyByRsa, + frontPublicKey: publicKey, + } + } + if (config.params) { + const data = aesEncrypt(aesKey, JSON.stringify(config.params)) + config.params = { + params: data, + aeskey: aesKeyByRsa, + frontPublicKey: publicKey, + } + } + } + } + } + return config + }, + (error) => { + return Promise.reject(error) + }, + ) + + // 全局响应拦截 + this.instance.interceptors.response.use( + (res) => { + if (res.data.code && res.data.code !== 200) { + message.error(res.data.message) + return Promise.reject(res.data) + } + //后端返回的通过rsa加密后的aes密钥 + const aesKeyByRsa: any = res.data.aesKeyByRsa + if (aesKeyByRsa) { + localStorage.setItem('afterPublicKey', aesKeyByRsa) + //通过rsa的私钥对后端返回的加密的aeskey进行解密 + const aesKey: any = rsaDecrypt(aesKeyByRsa, frontPrivateKey) + //使用解密后的aeskey对加密的返回报文进行解密 + res.data = JSON.parse(JSON.parse(aesDecrypt(aesKey, res.data))) + + return res.data + } + return res.data + }, + (error) => { + const { response } = error + if (response) { + this.handleCode(response.status) + } + if (!window.navigator.onLine) { + message.error('网络连接失败') + // return router.push({ + // path: '/404', + // }) + return Promise.reject(error) + } + }, + ) + } + handleCode(code: number): void { + switch (code) { + case 400: + message.error('请求错误(400)') + break + case 401: + message.error('未授权,请重新登录(401)') + break + case 403: + message.error('拒绝访问(403)') + break + case 404: + message.error('请求出错(404)') + break + case 408: + message.error('请求超时(408)') + break + case 500: + message.error('服务器错误(500)') + break + case 501: + message.error('服务未实现(501)') + break + case 502: + message.error('网络错误(502)') + break + case 503: + message.error('服务不可用(503)') + break + case 504: + message.error('网络超时(504)') + break + case 505: + message.error('HTTP版本不受支持(505)') + break + default: + message.error(`连接出错(${code})!`) + break + } + } + request(config: AxiosRequestConfig): Promise { + return new Promise((resolve, reject) => { + this.instance + ?.request(config) + .then((res) => { + resolve(res) + }) + .catch((err) => { + reject(err) + }) + }) + } + + get(url: string) { + return new Promise((resolve, reject) => { + this.instance + ?.get(url) + .then((res) => { + resolve(res) + }) + .catch((err) => { + reject(err) + }) + }) + } + + post(url: string, data = {}) { + return new Promise((resolve, reject) => { + this.instance + ?.post(url, data) + .then((res) => { + resolve(res) + }) + .catch((err) => { + reject(err) + }) + }) + } +} +export default Request diff --git a/src/utils/axios/service.ts b/src/utils/axios/service.ts index e57b863..ea2f800 100644 --- a/src/utils/axios/service.ts +++ b/src/utils/axios/service.ts @@ -1,16 +1,30 @@ -import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios' +import axios, { + AxiosError, + AxiosInstance, + AxiosRequestConfig, + AxiosResponse, + InternalAxiosRequestConfig, +} from 'axios' import { message } from 'antd' +import router from '@/router' +// import { aesEncrypt, get16RandomNum, getRsaKeys, rsaEncrypt } from '@/utils/encrypt/encrypt.ts' // 数据返回的接口 // 定义请求响应参数,不含data -interface Result { - code: number - msg: string -} +// interface Result { +// code: number +// msg: string +// } // 请求响应参数,包含data -interface ResultData extends Result { - data?: T +// interface ResultData extends Result { +// data?: T +// } +interface UploadFileItemModel { + name: string + value: string | Blob } +type UploadRequestConfig = Omit + const URL = import.meta.env.VITE_API_BASE_URL enum RequestEnums { TIMEOUT = 20000, @@ -26,6 +40,7 @@ const config = { // 跨域时候允许携带凭证 withCredentials: true, } +// let frontPrivateKey :string = '' class RequestHttp { // 定义成员变量并指定类型 service: AxiosInstance @@ -37,18 +52,49 @@ class RequestHttp { * 客户端发送请求 -> [请求拦截器] -> 服务器 */ this.service.interceptors.request.use( - (config: AxiosRequestConfig | any) => { - const token = localStorage.getItem('token') || '' - return { - ...config, - headers: { - 'x-access-token': token, // 请求头中携带token信息 - }, + (config: InternalAxiosRequestConfig | any) => { + if (localStorage.getItem('token')) { + const token = localStorage.getItem('token') || '' + return { + ...config, + headers: { + 'x-access-token': token, // 请求头中携带token信息 + }, + } } + // if (config.headers['isEncrypt']) { + // config.headers['Content-Type'] = 'application/json;charset=utf-8' + // if (config.method === 'post' || config.method === 'put') { + // const { privateKey, publicKey } = await getRsaKeys() + // const afterPublicKey = sessionStorage.getItem('afterPublicKey') + // frontPrivateKey = privateKey + // //每次请求生成aeskey + // const aesKey = get16RandomNum() + // //用登陆后后端生成并返回给前端的的RSA密钥对的公钥将AES16位密钥进行加密 + // const aesKeyByRsa = rsaEncrypt(aesKey, afterPublicKey) + // //使用AES16位的密钥将请求报文加密(使用的是加密前的aes密钥) + // if (config.data) { + // const data = aesEncrypt(aesKey, JSON.stringify(config.data)) + // config.data = { + // data: data, + // aesKey: aesKeyByRsa, + // frontPublicKey: publicKey, + // } + // } + // if (config.params) { + // const data = aesEncrypt(aesKey, JSON.stringify(config.params)) + // config.params = { + // params: data, + // aesKey: aesKeyByRsa, + // frontPublicKey: publicKey, + // } + // } + // } + // } }, (error: AxiosError) => { // 请求报错 - Promise.reject(error) + return Promise.reject(error) }, ) @@ -81,9 +127,9 @@ class RequestHttp { if (!window.navigator.onLine) { message.error('网络连接失败') // 可以跳转到错误页面,也可以不做操作 - // return router.replace({ - // path: '/404' - // }); + return router.push({ + path: '/404', + }) } }, ) @@ -128,18 +174,59 @@ class RequestHttp { break } } + + request(config: AxiosRequestConfig): Promise { + /** + * TODO: execute other methods according to config + */ + return new Promise((resolve, reject) => { + try { + this.service + .request(config) + .then((res: any) => { + resolve(res) + }) + .catch((err) => { + reject(err) + }) + } catch (err) { + return Promise.reject(err) + } + }) + } // 常用方法封装 - get(url: string, params?: object): Promise> { - return this.service.get(url, { params }) + get(config: AxiosRequestConfig): Promise { + return this.request({ method: 'GET', ...config }) } - post(url: string, params?: object): Promise> { - return this.service.post(url, params) + post(config: AxiosRequestConfig): Promise { + return this.request({ method: 'POST', ...config }) } - put(url: string, params?: object): Promise> { - return this.service.put(url, params) + put(config: AxiosRequestConfig): Promise { + return this.request({ method: 'PUT', ...config }) } - delete(url: string, params?: object): Promise> { - return this.service.delete(url, { params }) + delete(config: AxiosRequestConfig): Promise { + return this.request({ method: 'DELETE', ...config }) + } + upload( + fileItem: UploadFileItemModel, + config?: UploadRequestConfig, + ): Promise | null { + if (!import.meta.env.VITE_UPLOAD_URL) return null + + const fd = new FormData() + fd.append(fileItem.name, fileItem.value) + let configCopy: UploadRequestConfig + if (!config) { + configCopy = { + headers: { + 'Content-Type': 'multipart/form-data', + }, + } + } else { + config.headers!['Content-Type'] = 'multipart/form-data' + configCopy = config + } + return this.request({ url: import.meta.env.VITE_UPLOAD_URL, data: fd, ...configCopy }) } } // 导出一个实例对象 diff --git a/src/utils/axios/web.ts b/src/utils/axios/web.ts new file mode 100644 index 0000000..48dc710 --- /dev/null +++ b/src/utils/axios/web.ts @@ -0,0 +1,14 @@ +import Request from './request' +import { handleLocalforage } from '@/utils/localforage' + +const token = String(handleLocalforage.getItem('token')) +const web: Request = new Request({ + baseURL: import.meta.env.VITE_APP_BASE_API, + headers: { + 'Content-Type': 'application/json;charset=utf-8', + Accept: 'application/json', + Authorization: token, + }, +}) + +export default web diff --git a/src/utils/encrypt/encrypt.ts b/src/utils/encrypt/encrypt.ts new file mode 100644 index 0000000..4a79914 --- /dev/null +++ b/src/utils/encrypt/encrypt.ts @@ -0,0 +1,169 @@ +import JSEncrypt from 'jsencrypt' +import CryptoJS from 'crypto-js' + +// 加密 +export function rsaEncrypt(Str: string, afterPublicKey: string) { + const encryptor = new JSEncrypt() + encryptor.setPublicKey(afterPublicKey) // 设置公钥 + return encryptor.encrypt(Str) // 对数据进行加密 +} + +// 解密 +export function rsaDecrypt(Str: string, frontPrivateKey: string) { + const encryptor = new JSEncrypt() + encryptor.setPrivateKey(frontPrivateKey) // 设置私钥 + return encryptor.decrypt(Str) // 对数据进行解密 +} + +export function aesEncrypt(aeskey: string, Str: string) { + // 设置一个默认值,如果第二个参数为空采用默认值,不为空则采用新设置的密钥 + const key = CryptoJS.enc.Utf8.parse(aeskey) + const srcs = CryptoJS.enc.Utf8.parse(Str) + const encrypted = CryptoJS.AES.encrypt(srcs, key, { + // 切记 需要和后端算法模式一致 + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }) + + return encrypted.toString() +} + +export function aesDecrypt(aeskey: string, Str: string) { + const key = CryptoJS.enc.Utf8.parse(aeskey) + const decrypt = CryptoJS.AES.decrypt(Str, key, { + // 切记 需要和后端算法模式一致 + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }) + return CryptoJS.enc.Utf8.stringify(decrypt).toString() +} +/** + * 获取16位随机码AES + * @returns {string} + */ +export function get16RandomNum() { + const chars = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + 'O', + 'P', + 'Q', + 'R', + 'S', + 'T', + 'U', + 'V', + 'W', + 'X', + 'Y', + 'Z', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'q', + 'r', + 's', + 't', + 'u', + 'v', + 'w', + 'x', + 'y', + 'z', + ] + let nums = '' + //这个地方切记要选择16位,因为美国对密钥长度有限制,选择32位的话加解密会报错,需要根据jdk版本去修改相关jar包,有点恼火,选择16位就不用处理。 + for (let i = 0; i < 16; i++) { + const id = parseInt(String(Math.random() * 61)) + nums += chars[id] + } + return nums +} +//获取rsa密钥对 +export function getRsaKeys() { + return new Promise((resolve, reject) => { + window.crypto.subtle + .generateKey( + { + name: 'RSA-OAEP', + modulusLength: 2048, //can be 1024, 2048, or 4096 + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: { name: 'SHA-512' }, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512" + }, + true, //whether the key is extractable (i.e. can be used in exportKey) + ['encrypt', 'decrypt'], //must be ["encrypt", "decrypt"] or ["wrapKey", "unwrapKey"] + ) + .then(function (key) { + window.crypto.subtle + .exportKey('pkcs8', key.privateKey) + .then(function (keydata1) { + window.crypto.subtle + .exportKey('spki', key.publicKey) + .then(function (keydata2) { + const privateKey = RSA2text(keydata1, 1) + + const publicKey = RSA2text(keydata2) + + resolve({ privateKey, publicKey }) + }) + .catch(function (err) { + reject(err) + }) + }) + .catch(function (err) { + reject(err) + }) + }) + .catch(function (err) { + reject(err) + }) + }) +} + +function RSA2text(buffer: any, _isPrivate: number = 0) { + let binary = '' + const bytes = new Uint8Array(buffer) + const len = bytes.byteLength + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]) + } + const base64 = window.btoa(binary) + const text = base64.replace(/[^\x00-\xff]/g, '$&\x01').replace(/.{64}\x01?/g, '$&\n') + + return text +} diff --git a/src/utils/localStorage/config.ts b/src/utils/localStorage/config.ts index 57be96c..de6829d 100644 --- a/src/utils/localStorage/config.ts +++ b/src/utils/localStorage/config.ts @@ -1,102 +1,102 @@ -import { encrypt, decrypt } from './encry'; -import { globalConfig } from './interface'; +import { encrypt, decrypt } from './encry' +import { globalConfig } from './interface' const config: globalConfig = { - type: 'localStorage', //存储类型,localStorage | sessionStorage - prefix: 'react-view-ui_0.0.1', //版本号 - expire: 24 * 60, //过期时间,默认为一天,单位为分钟 - isEncrypt: true, //支持加密、解密数据处理 -}; + type: 'localStorage', //存储类型,localStorage | sessionStorage + prefix: 'schisandra_', //版本号 + expire: 24 * 60, //过期时间,默认为一天,单位为分钟 + isEncrypt: true, //支持加密、解密数据处理 +} const setStorage = (key: string, value: any, expire: number = 24 * 60): boolean => { - //设定值 - if (value === '' || value === null || value === undefined) { - //空值重置 - value = null; - } - if (isNaN(expire) || expire < 0) { - //过期时间值合理性判断 - throw new Error('Expire must be a number'); - } - const data = { - value, //存储值 - time: Date.now(), //存储日期 - expire: Date.now() + 1000 * 60 * expire, //过期时间 - }; - //是否需要加密,判断装载加密数据或原数据 - window[config.type].setItem( - autoAddPreFix(key), - config.isEncrypt ? encrypt(JSON.stringify(data)) : JSON.stringify(data), - ); - return true; -}; + //设定值 + if (value === '' || value === null || value === undefined) { + //空值重置 + value = null + } + if (isNaN(expire) || expire < 0) { + //过期时间值合理性判断 + throw new Error('Expire must be a number') + } + const data = { + value, //存储值 + time: Date.now(), //存储日期 + expire: Date.now() + 1000 * 60 * expire, //过期时间 + } + //是否需要加密,判断装载加密数据或原数据 + window[config.type].setItem( + autoAddPreFix(key), + config.isEncrypt ? encrypt(JSON.stringify(data)) : JSON.stringify(data), + ) + return true +} const getStorageFromKey = (key: string) => { - //获取指定值 - if (config.prefix) { - key = autoAddPreFix(key); - } - if (!window[config.type].getItem(key)) { - //不存在判断 - return null; - } + //获取指定值 + if (config.prefix) { + key = autoAddPreFix(key) + } + if (!window[config.type].getItem(key)) { + //不存在判断 + return null + } - const storageVal = config.isEncrypt - ? JSON.parse(decrypt(window[config.type].getItem(key) as string)) - : JSON.parse(window[config.type].getItem(key) as string); - const now = Date.now(); - if (now >= storageVal.expire) { - //过期销毁 - removeStorageFromKey(key); - return null; - //不过期回值 - } else { - return storageVal.value; - } -}; + const storageVal = config.isEncrypt + ? JSON.parse(decrypt(window[config.type].getItem(key) as string)) + : JSON.parse(window[config.type].getItem(key) as string) + const now = Date.now() + if (now >= storageVal.expire) { + //过期销毁 + removeStorageFromKey(key) + return null + //不过期回值 + } else { + return storageVal.value + } +} const getAllStorage = () => { - //获取所有值 - const storageList: any = {}; - const keys = Object.keys(window[config.type]); - keys.forEach((key) => { - const value = getStorageFromKey(autoRemovePreFix(key)); - if (value !== null) { - //如果值没有过期,加入到列表中 - storageList[autoRemovePreFix(key)] = value; - } - }); - return storageList; -}; + //获取所有值 + const storageList: any = {} + const keys = Object.keys(window[config.type]) + keys.forEach((key) => { + const value = getStorageFromKey(autoRemovePreFix(key)) + if (value !== null) { + //如果值没有过期,加入到列表中 + storageList[autoRemovePreFix(key)] = value + } + }) + return storageList +} const getStorageLength = () => { - //获取值列表长度 - return window[config.type].length; -}; + //获取值列表长度 + return window[config.type].length +} const removeStorageFromKey = (key: string) => { - //删除值 - if (config.prefix) { - key = autoAddPreFix(key); - } - window[config.type].removeItem(key); -}; + //删除值 + if (config.prefix) { + key = autoAddPreFix(key) + } + window[config.type].removeItem(key) +} const clearStorage = () => { - window[config.type].clear(); -}; + window[config.type].clear() +} const autoAddPreFix = (key: string) => { - //添加前缀,保持唯一性 - const prefix = config.prefix || ''; - return `${prefix}_${key}`; -}; + //添加前缀,保持唯一性 + const prefix = config.prefix || '' + return `${prefix}_${key}` +} const autoRemovePreFix = (key: string) => { - //删除前缀,进行增删改查 - const lineIndex = config.prefix.length + 1; - return key.substr(lineIndex); -}; + //删除前缀,进行增删改查 + const lineIndex = config.prefix.length + 1 + return key.substr(lineIndex) +} export { - setStorage, - getStorageFromKey, - getAllStorage, - getStorageLength, - removeStorageFromKey, - clearStorage, -}; + setStorage, + getStorageFromKey, + getAllStorage, + getStorageLength, + removeStorageFromKey, + clearStorage, +} diff --git a/src/utils/localforage/index.ts b/src/utils/localforage/index.ts new file mode 100644 index 0000000..488ef6f --- /dev/null +++ b/src/utils/localforage/index.ts @@ -0,0 +1,74 @@ +import localforage from 'localforage' +import CryptoJS from 'crypto-js' + +const SECRET_KEY = CryptoJS.enc.Utf8.parse('3333e6e143439161') //十六位十六进制数作为密钥 +const SECRET_IV = CryptoJS.enc.Utf8.parse('e3bbe7e3ba84431a') //十六位十六进制数作为密钥偏移量 +/** + * 加密 + * @param data + * @param output + */ +export const encrypt = (data: string, output?: any) => { + const dataHex = CryptoJS.enc.Utf8.parse(data) + const encrypted = CryptoJS.AES.encrypt(dataHex, SECRET_KEY, { + iv: SECRET_IV, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }) + return encrypted.ciphertext.toString(output) +} + +/** + * 解密 + * @param data + */ +export const decrypt = (data: string | null) => { + if (data === null) { + return + } + const encryptedHex = CryptoJS.enc.Hex.parse(data) + const encryptedHexStr = CryptoJS.enc.Base64.stringify(encryptedHex) + const decrypted = CryptoJS.AES.decrypt(encryptedHexStr, SECRET_KEY, { + iv: SECRET_IV, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }) + const decryptedStr = decrypted.toString(CryptoJS.enc.Utf8) + return decryptedStr.toString() +} + +export const handleLocalforage = { + config: async (options?: LocalForageOptions) => localforage.config(options || {}), + setItem: async (key: string, value: string): Promise => { + await localforage.setItem(key, encrypt(value)) + }, + getItem: async function getItem(key: string): Promise { + try { + const value: any = decrypt(await localforage.getItem(key)) as any + // 如果值是 undefined,返回 null + if (value === undefined) { + return null + } + // 如果值是 T 类型,直接返回 + if (typeof value === 'object' && value !== null) { + return value as T + } + // 如果值是 string 类型,直接返回 + return value as string + } catch (error) { + console.error('Error retrieving data from localforage:', error) + return null + } + }, + removeItem: async (key: string): Promise => { + await localforage.removeItem(key) + }, + clear: async () => { + return await localforage.clear() + }, + createInstance: async (name: string) => { + return localforage.createInstance({ + name, + }) + }, +} diff --git a/src/views/Home/index.tsx b/src/views/Home/index.tsx index da7604c..9994cd2 100644 --- a/src/views/Home/index.tsx +++ b/src/views/Home/index.tsx @@ -1,17 +1,16 @@ -// import FileSharing from '@/components/FileSharing' -// import DefaultLayOut from '@/layout/default' - import HomeIndex from '@/components/HomeIndex' - -// import Loading from '@/components/Loading' +import { useEffect } from 'react' +import { handleLocalforage } from '@/utils/localforage' export default () => { + useEffect(() => { + handleLocalforage.getItem('token').then((res: any) => { + console.log(JSON.parse(res)) + }) + }, []) return (
- {/**/} - {/**/} - {/**/}
) } diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 7985ae1..b316ff0 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -6,6 +6,8 @@ declare interface ImportMetaEnv { readonly VITE_API_BASE_URL: string readonly VITE_NODE_ENV: string readonly VITE_TITLE_NAME: string + readonly VITE_APP_TOKEN_KEY?: string + readonly VITE_UPLOAD_URL?: string } interface ImportMeta { diff --git a/vite.config.ts b/vite.config.ts index a81192d..59b4389 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -159,5 +159,16 @@ export default defineConfig(({ mode }) => { }, }, }, + server: { + proxy: { + [env.VITE_APP_BASE_API]: { + //后端接口的baseurl + target: env.VITE_API_BASE_URL, + //是否允许跨域 + changeOrigin: true, + rewrite: (path) => path.replace(RegExp(`^${env.VITE_APP_BASE_API}`), ''), + }, + }, + }, } })