feat: add localforage

This commit is contained in:
landaiqing
2024-04-29 16:00:47 +08:00
parent edcff9a7c7
commit 9762e15b8b
17 changed files with 726 additions and 147 deletions

View File

@@ -2,7 +2,7 @@
VITE_NODE_ENV='development' VITE_NODE_ENV='development'
# 开发环境 # 开发环境
VITE_APP_BASE_API='/dev-api' VITE_APP_BASE_API='/api'
# 页面 title 前缀 # 页面 title 前缀
VITE_APP_TITLE=开发环境 VITE_APP_TITLE=开发环境
@@ -11,3 +11,9 @@ VITE_APP_TITLE=开发环境
VITE_API_BASE_URL='http://127.0.0.1:3000' VITE_API_BASE_URL='http://127.0.0.1:3000'
VITE_TITLE_NAME='五味子云存储' VITE_TITLE_NAME='五味子云存储'
# token key
VITE_APP_TOKEN_KEY='token'
# the upload url
VITE_UPLOAD_URL='http://127.0.0.1:3000'

View File

@@ -1,7 +1,7 @@
# 生产环境配置 # 生产环境配置
VITE_NODE_ENV= 'production' VITE_NODE_ENV='production'
# 生产环境 # 生产环境
VITE_APP_BASE_API = '/api' VITE_APP_BASE_API='/api'
# 页面 title 前缀 # 页面 title 前缀
VITE_APP_TITLE=生产环境 VITE_APP_TITLE=生产环境
@@ -10,3 +10,9 @@ VITE_APP_TITLE=生产环境
VITE_API_BASE_URL='' VITE_API_BASE_URL=''
VITE_TITLE_NAME='五味子云存储' VITE_TITLE_NAME='五味子云存储'
# token key
VITE_APP_TOKEN_KEY='token'
# the upload url
VITE_UPLOAD_URL='http://127.0.0.1:3000'

View File

@@ -50,5 +50,6 @@ module.exports = {
'@typescript-eslint/no-empty-interface': ['off'], '@typescript-eslint/no-empty-interface': ['off'],
'@typescript-eslint/no-unused-vars': ['off'], '@typescript-eslint/no-unused-vars': ['off'],
'@typescript-eslint/no-non-null-assertion': ['off'], '@typescript-eslint/no-non-null-assertion': ['off'],
"no-control-regex": "off"
} }
} }

View File

@@ -20,6 +20,8 @@
"axios": "^1.6.8", "axios": "^1.6.8",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"gsap": "^3.12.5", "gsap": "^3.12.5",
"jsencrypt": "^3.3.2",
"localforage": "^1.10.0",
"mobx": "^6.12.3", "mobx": "^6.12.3",
"mobx-persist-store": "^1.1.4", "mobx-persist-store": "^1.1.4",
"mobx-react": "^9.1.1", "mobx-react": "^9.1.1",

26
pnpm-lock.yaml generated
View File

@@ -35,6 +35,12 @@ dependencies:
gsap: gsap:
specifier: ^3.12.5 specifier: ^3.12.5
version: 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: mobx:
specifier: ^6.12.3 specifier: ^6.12.3
version: 6.12.3 version: 6.12.3
@@ -5382,6 +5388,10 @@ packages:
hasBin: true hasBin: true
requiresBuild: true requiresBuild: true
/immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
dev: false
/import-fresh@3.3.0: /import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -5739,6 +5749,10 @@ packages:
argparse: 2.0.1 argparse: 2.0.1
dev: true dev: true
/jsencrypt@3.3.2:
resolution: {integrity: sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==}
dev: false
/jsesc@0.5.0: /jsesc@0.5.0:
resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==}
hasBin: true hasBin: true
@@ -5870,6 +5884,12 @@ packages:
type-check: 0.4.0 type-check: 0.4.0
dev: true dev: true
/lie@3.1.1:
resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==}
dependencies:
immediate: 3.0.6
dev: false
/lines-and-columns@1.2.4: /lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@@ -5890,6 +5910,12 @@ packages:
pkg-types: 1.0.3 pkg-types: 1.0.3
dev: true 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: /locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'} engines: {node: '>=10'}

View File

@@ -1,5 +1,5 @@
import service from '@/utils/axios/service.ts' import web from '@/utils/axios/web.ts'
export function getUserInfo(params: object) { export const getPublicKey = () => {
return service.get('/user/info', params) return web.get('/encrypt/getPublicKey')
} }

View File

@@ -1,4 +1,4 @@
import React from 'react' // import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import { RouterProvider, createBrowserRouter } from 'react-router-dom' import { RouterProvider, createBrowserRouter } from 'react-router-dom'
import './assets/styles/index.less' import './assets/styles/index.less'
@@ -8,9 +8,9 @@ import { Provider as MobxProvider } from 'mobx-react'
import { RootStore } from '@/store' import { RootStore } from '@/store'
const router = createBrowserRouter(routeConfig) const router = createBrowserRouter(routeConfig)
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> // <React.StrictMode>
<MobxProvider {...RootStore}> <MobxProvider {...RootStore}>
<RouterProvider router={router} /> <RouterProvider router={router} />
</MobxProvider> </MobxProvider>,
</React.StrictMode>, // </React.StrictMode>,
) )

View File

@@ -1,22 +1,23 @@
import { action, makeAutoObservable } from 'mobx' import { action, makeAutoObservable } from 'mobx'
import { makePersistable, isHydrated } from 'mobx-persist-store' import { makePersistable, isHydrated } from 'mobx-persist-store'
import { handleLocalforage } from '@/utils/localforage'
export class useUserStore { export class useUserStore {
user: any = {} token: any = ''
constructor() { constructor() {
makeAutoObservable( makeAutoObservable(
this, this,
{ {
setUserInfo: action, setToken: action,
}, },
{ autoBind: true }, { autoBind: true },
) )
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
makePersistable(this, { makePersistable(this, {
// 在构造函数内使用 makePersistable // 在构造函数内使用 makePersistable
name: 'userStore', // 保存的name用于在storage中的名称标识只要不和storage中其他名称重复就可以 name: 'token', // 保存的name用于在storage中的名称标识只要不和storage中其他名称重复就可以
properties: ['user'], // 要保存的字段这些字段会被保存在name对应的storage中注意不写在这里面的字段将不会被保存刷新页面也将丢失get字段例外。get数据会在数据返回后再自动计算 properties: ['token'], // 要保存的字段这些字段会被保存在name对应的storage中注意不写在这里面的字段将不会被保存刷新页面也将丢失get字段例外。get数据会在数据返回后再自动计算
storage: window.localStorage, // 保存的位置看自己的业务情况选择可以是localStoragesessionstorage storage: handleLocalforage, // 保存的位置看自己的业务情况选择可以是localStoragesessionstorage
// 。。还有一些其他配置参数例如数据过期时间等等可以康康文档像storage这种字段可以配置在全局配置里详见文档 // 。。还有一些其他配置参数例如数据过期时间等等可以康康文档像storage这种字段可以配置在全局配置里详见文档
}).then( }).then(
action(() => { action(() => {
@@ -25,16 +26,13 @@ export class useUserStore {
}), }),
) )
} }
get getUserInfo() { get getToken() {
return this.user return this.token ? this.token : null
}
get token() {
return this.user ? this.user.token : ''
} }
get isHydrated() { get isHydrated() {
return isHydrated(this) return isHydrated(this)
} }
setUserInfo(user: object) { setToken(token: string) {
this.user = user this.token = token
} }
} }

184
src/utils/axios/request.ts Normal file
View File

@@ -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<T>(config: AxiosRequestConfig<T>): Promise<T> {
return new Promise<T>((resolve, reject) => {
this.instance
?.request<any, T>(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

View File

@@ -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 { message } from 'antd'
import router from '@/router'
// import { aesEncrypt, get16RandomNum, getRsaKeys, rsaEncrypt } from '@/utils/encrypt/encrypt.ts'
// 数据返回的接口 // 数据返回的接口
// 定义请求响应参数不含data // 定义请求响应参数不含data
interface Result { // interface Result {
code: number // code: number
msg: string // msg: string
} // }
// 请求响应参数包含data // 请求响应参数包含data
interface ResultData<T = never> extends Result { // interface ResultData<T = never> extends Result {
data?: T // data?: T
// }
interface UploadFileItemModel {
name: string
value: string | Blob
} }
type UploadRequestConfig = Omit<AxiosRequestConfig, 'url' | 'data'>
const URL = import.meta.env.VITE_API_BASE_URL const URL = import.meta.env.VITE_API_BASE_URL
enum RequestEnums { enum RequestEnums {
TIMEOUT = 20000, TIMEOUT = 20000,
@@ -26,6 +40,7 @@ const config = {
// 跨域时候允许携带凭证 // 跨域时候允许携带凭证
withCredentials: true, withCredentials: true,
} }
// let frontPrivateKey :string = ''
class RequestHttp { class RequestHttp {
// 定义成员变量并指定类型 // 定义成员变量并指定类型
service: AxiosInstance service: AxiosInstance
@@ -37,7 +52,8 @@ class RequestHttp {
* 客户端发送请求 -> [请求拦截器] -> 服务器 * 客户端发送请求 -> [请求拦截器] -> 服务器
*/ */
this.service.interceptors.request.use( this.service.interceptors.request.use(
(config: AxiosRequestConfig | any) => { (config: InternalAxiosRequestConfig | any) => {
if (localStorage.getItem('token')) {
const token = localStorage.getItem('token') || '' const token = localStorage.getItem('token') || ''
return { return {
...config, ...config,
@@ -45,10 +61,40 @@ class RequestHttp {
'x-access-token': token, // 请求头中携带token信息 '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) => { (error: AxiosError) => {
// 请求报错 // 请求报错
Promise.reject(error) return Promise.reject(error)
}, },
) )
@@ -81,9 +127,9 @@ class RequestHttp {
if (!window.navigator.onLine) { if (!window.navigator.onLine) {
message.error('网络连接失败') message.error('网络连接失败')
// 可以跳转到错误页面,也可以不做操作 // 可以跳转到错误页面,也可以不做操作
// return router.replace({ return router.push({
// path: '/404' path: '/404',
// }); })
} }
}, },
) )
@@ -128,18 +174,59 @@ class RequestHttp {
break break
} }
} }
request<T = any>(config: AxiosRequestConfig): Promise<T> {
/**
* TODO: execute other methods according to config
*/
return new Promise((resolve, reject) => {
try {
this.service
.request<T>(config)
.then((res: any) => {
resolve(res)
})
.catch((err) => {
reject(err)
})
} catch (err) {
return Promise.reject(err)
}
})
}
// 常用方法封装 // 常用方法封装
get<T>(url: string, params?: object): Promise<ResultData<T>> { get<T = any>(config: AxiosRequestConfig): Promise<T> {
return this.service.get(url, { params }) return this.request({ method: 'GET', ...config })
} }
post<T>(url: string, params?: object): Promise<ResultData<T>> { post<T = any>(config: AxiosRequestConfig): Promise<T> {
return this.service.post(url, params) return this.request({ method: 'POST', ...config })
} }
put<T>(url: string, params?: object): Promise<ResultData<T>> { put<T = any>(config: AxiosRequestConfig): Promise<T> {
return this.service.put(url, params) return this.request({ method: 'PUT', ...config })
} }
delete<T>(url: string, params?: object): Promise<ResultData<T>> { delete<T = any>(config: AxiosRequestConfig): Promise<T> {
return this.service.delete(url, { params }) return this.request({ method: 'DELETE', ...config })
}
upload<T = string>(
fileItem: UploadFileItemModel,
config?: UploadRequestConfig,
): Promise<T> | 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 })
} }
} }
// 导出一个实例对象 // 导出一个实例对象

14
src/utils/axios/web.ts Normal file
View File

@@ -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

View File

@@ -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
}

View File

@@ -1,96 +1,96 @@
import { encrypt, decrypt } from './encry'; import { encrypt, decrypt } from './encry'
import { globalConfig } from './interface'; import { globalConfig } from './interface'
const config: globalConfig = { const config: globalConfig = {
type: 'localStorage', //存储类型localStorage | sessionStorage type: 'localStorage', //存储类型localStorage | sessionStorage
prefix: 'react-view-ui_0.0.1', //版本号 prefix: 'schisandra_', //版本号
expire: 24 * 60, //过期时间,默认为一天,单位为分钟 expire: 24 * 60, //过期时间,默认为一天,单位为分钟
isEncrypt: true, //支持加密、解密数据处理 isEncrypt: true, //支持加密、解密数据处理
}; }
const setStorage = (key: string, value: any, expire: number = 24 * 60): boolean => { const setStorage = (key: string, value: any, expire: number = 24 * 60): boolean => {
//设定值 //设定值
if (value === '' || value === null || value === undefined) { if (value === '' || value === null || value === undefined) {
//空值重置 //空值重置
value = null; value = null
} }
if (isNaN(expire) || expire < 0) { if (isNaN(expire) || expire < 0) {
//过期时间值合理性判断 //过期时间值合理性判断
throw new Error('Expire must be a number'); throw new Error('Expire must be a number')
} }
const data = { const data = {
value, //存储值 value, //存储值
time: Date.now(), //存储日期 time: Date.now(), //存储日期
expire: Date.now() + 1000 * 60 * expire, //过期时间 expire: Date.now() + 1000 * 60 * expire, //过期时间
}; }
//是否需要加密,判断装载加密数据或原数据 //是否需要加密,判断装载加密数据或原数据
window[config.type].setItem( window[config.type].setItem(
autoAddPreFix(key), autoAddPreFix(key),
config.isEncrypt ? encrypt(JSON.stringify(data)) : JSON.stringify(data), config.isEncrypt ? encrypt(JSON.stringify(data)) : JSON.stringify(data),
); )
return true; return true
}; }
const getStorageFromKey = (key: string) => { const getStorageFromKey = (key: string) => {
//获取指定值 //获取指定值
if (config.prefix) { if (config.prefix) {
key = autoAddPreFix(key); key = autoAddPreFix(key)
} }
if (!window[config.type].getItem(key)) { if (!window[config.type].getItem(key)) {
//不存在判断 //不存在判断
return null; return null
} }
const storageVal = config.isEncrypt const storageVal = config.isEncrypt
? JSON.parse(decrypt(window[config.type].getItem(key) as string)) ? JSON.parse(decrypt(window[config.type].getItem(key) as string))
: JSON.parse(window[config.type].getItem(key) as string); : JSON.parse(window[config.type].getItem(key) as string)
const now = Date.now(); const now = Date.now()
if (now >= storageVal.expire) { if (now >= storageVal.expire) {
//过期销毁 //过期销毁
removeStorageFromKey(key); removeStorageFromKey(key)
return null; return null
//不过期回值 //不过期回值
} else { } else {
return storageVal.value; return storageVal.value
} }
}; }
const getAllStorage = () => { const getAllStorage = () => {
//获取所有值 //获取所有值
const storageList: any = {}; const storageList: any = {}
const keys = Object.keys(window[config.type]); const keys = Object.keys(window[config.type])
keys.forEach((key) => { keys.forEach((key) => {
const value = getStorageFromKey(autoRemovePreFix(key)); const value = getStorageFromKey(autoRemovePreFix(key))
if (value !== null) { if (value !== null) {
//如果值没有过期,加入到列表中 //如果值没有过期,加入到列表中
storageList[autoRemovePreFix(key)] = value; storageList[autoRemovePreFix(key)] = value
} }
}); })
return storageList; return storageList
}; }
const getStorageLength = () => { const getStorageLength = () => {
//获取值列表长度 //获取值列表长度
return window[config.type].length; return window[config.type].length
}; }
const removeStorageFromKey = (key: string) => { const removeStorageFromKey = (key: string) => {
//删除值 //删除值
if (config.prefix) { if (config.prefix) {
key = autoAddPreFix(key); key = autoAddPreFix(key)
} }
window[config.type].removeItem(key); window[config.type].removeItem(key)
}; }
const clearStorage = () => { const clearStorage = () => {
window[config.type].clear(); window[config.type].clear()
}; }
const autoAddPreFix = (key: string) => { const autoAddPreFix = (key: string) => {
//添加前缀,保持唯一性 //添加前缀,保持唯一性
const prefix = config.prefix || ''; const prefix = config.prefix || ''
return `${prefix}_${key}`; return `${prefix}_${key}`
}; }
const autoRemovePreFix = (key: string) => { const autoRemovePreFix = (key: string) => {
//删除前缀,进行增删改查 //删除前缀,进行增删改查
const lineIndex = config.prefix.length + 1; const lineIndex = config.prefix.length + 1
return key.substr(lineIndex); return key.substr(lineIndex)
}; }
export { export {
setStorage, setStorage,
@@ -99,4 +99,4 @@ export {
getStorageLength, getStorageLength,
removeStorageFromKey, removeStorageFromKey,
clearStorage, clearStorage,
}; }

View File

@@ -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<void> => {
await localforage.setItem(key, encrypt(value))
},
getItem: async function getItem<T>(key: string): Promise<T | string | null> {
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<void> => {
await localforage.removeItem(key)
},
clear: async () => {
return await localforage.clear()
},
createInstance: async (name: string) => {
return localforage.createInstance({
name,
})
},
}

View File

@@ -1,17 +1,16 @@
// import FileSharing from '@/components/FileSharing'
// import DefaultLayOut from '@/layout/default'
import HomeIndex from '@/components/HomeIndex' import HomeIndex from '@/components/HomeIndex'
import { useEffect } from 'react'
// import Loading from '@/components/Loading' import { handleLocalforage } from '@/utils/localforage'
export default () => { export default () => {
useEffect(() => {
handleLocalforage.getItem('token').then((res: any) => {
console.log(JSON.parse(res))
})
}, [])
return ( return (
<div> <div>
{/*<DefaultLayOut />*/}
{/*<BlurCard />*/}
<HomeIndex /> <HomeIndex />
{/*<Loading />*/}
</div> </div>
) )
} }

2
src/vite-env.d.ts vendored
View File

@@ -6,6 +6,8 @@ declare interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string readonly VITE_API_BASE_URL: string
readonly VITE_NODE_ENV: string readonly VITE_NODE_ENV: string
readonly VITE_TITLE_NAME: string readonly VITE_TITLE_NAME: string
readonly VITE_APP_TOKEN_KEY?: string
readonly VITE_UPLOAD_URL?: string
} }
interface ImportMeta { interface ImportMeta {

View File

@@ -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}`), ''),
},
},
},
} }
}) })