Commit 1372a19d authored by 郑磊's avatar 郑磊

更新

parent 43f52225
node_modules/
/coverage
{
"arrowParens": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"endOfLine": "auto",
"printWidth": 100,
"quoteProps": "as-needed",
"semi": false,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "all",
"useTabs": false
}
export default {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts'],
}
{
"name": "@3ya/web-lib",
"version": "1.0.0",
"type": "module",
"main": "src/index.ts",
"private": true,
"scripts": {
"test": "jest ."
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/jest": "^29.5.12",
"@types/lodash-es": "^4.17.12",
"axios": "^1.6.5",
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"typescript": "^5.3.3"
},
"peerDependencies": {
......
import {
AxiosRequestHeaders,
InternalAxiosRequestConfig,
type AxiosInstance,
} from 'axios'
import { sample } from 'lodash-es'
import {
aesDecrypt,
aesEncrypt,
base64Decode,
base64Encode,
md5,
} from './security'
/**
* 接口调用配置项
*/
export interface ApiOptions {
/**
* 接口请求的基地址
*/
baseURL?: string
/**
* 包名
*/
source?: string
/**
* 设备类型
*/
device?: string
/**
* 版本号
*/
appversion?: string
/**
* 附带在请求头的token或者token获取函数
*/
token?: string | (() => string | undefined | null)
/**
* 请求体是否加密
*/
encryption?: boolean
}
/**
* 请求加解密密钥
*/
export interface SignKey {
name: string
key: string
iv: string
}
/**
* 默认的请求加解密密钥列表
*/
export const DEFAULT_SIGN_KEYS: SignKey[] = [
{
key: 'k0f3JfxEWd3HC7pXQU8tmSkDheUXibmz',
iv: 'Fu2wU73MBcsEZWJk',
name: 'secret_key_1',
},
{
key: 'NdEHDuAZGCQV6C0oaURvBJJWA4z2QHyG',
iv: 'w0j9K4bswcGJKtj5',
name: 'secret_key_2',
},
{
key: 'lBsn3FrYN9hdoFqMdqqNV1dlpGutkcXk',
iv: 'NRHqJYTNKxH8Z9fU',
name: 'secret_key_3',
},
{
key: 'eVdEc44rke6P6rfn9SgEWGPNqkcipWN7',
iv: 'l5HNU6Q0bdc2piB3',
name: 'secret_key_4',
},
{
key: '53BSHwE29I1u4e5cJDMZGHUdi0A7L4E5',
iv: 'zUsjrk58p2RahP08',
name: 'secret_key_5',
},
{
key: 'vgizOWReMzVJA6LEsb9N36LEzPqcFdeO',
iv: 'e52YLbnvVv4HpGu7',
name: 'secret_key_6',
},
]
export const DEFAULT_API_OPTIONS: ApiOptions = {
source: 'xinxiuweb',
device: 'wap',
appversion: '1.0.0',
}
function checkHeader(
headers: AxiosRequestHeaders,
key: string,
...defaultValues: any[]
): void {
if (headers.has(key)) return
for (const value of defaultValues) {
if (typeof value === 'undefined' || value === null) continue
if (typeof value === 'function') {
const headerValue = value()
if (typeof headerValue === 'undefined' || headerValue === null)
continue
headers.set(key, headerValue)
}
headers.set(key, String(value))
}
}
/**
* 为axios实例注入追加公参的拦截器
* @param axios axios实例
* @param options 默认配置
*/
export function useCommonParamsInterceptor(
axios: AxiosInstance,
options: ApiOptions = {}
) {
options = {
...DEFAULT_API_OPTIONS,
...options,
}
axios.interceptors.request.use(
(request: InternalAxiosRequestConfig<any> & ApiOptions) => {
//包名
checkHeader(
request.headers,
'source',
request.source,
options.source
)
//设备类型
checkHeader(
request.headers,
'device',
request.device,
options.device
)
//版本号
checkHeader(
request.headers,
'appversion',
request.appversion,
options.appversion
)
//时间戳
checkHeader(
request.headers,
'timestamp',
Math.floor(Date.now() / 1000)
)
//请求标识
checkHeader(
request.headers,
'request_number',
md5(String(Date.now() + Math.random()))
)
return request
}
)
}
function getParamValue(value: any): string {
if (value === null || typeof value === 'undefined') {
return ''
}
if (typeof value === 'object') {
return JSON.stringify(value)
}
return String(value)
}
/**
* 为axios实例注入请求签名拦截器
* @param axios axios实例
* @param key 签名密钥,如果不传则使用默认密钥
*/
export function useSignatureInterceptor(axios: AxiosInstance, key?: string) {
axios.interceptors.request.use((request) => {
if (request.headers.has('signature')) return request
const body =
typeof request.data === 'object' &&
request.data !== null &&
!Array.isArray(request.data)
? request.data
: {}
//参与签名的数据
const params: Record<string, any> = {
...body,
source: request.headers.get('source'),
device: request.headers.get('device'),
timestamp: request.headers.get('timestamp'),
request_number: request.headers.get('request_number'),
appversion: request.headers.get('appversion'),
}
const entries = Object.entries(params).sort((k1, k2) =>
k1[0].localeCompare(k2[0])
)
let basestr = entries.map((t) => getParamValue(t[1])).join('')
if (key) {
basestr += key
} else if (request.headers.get('device') === 'wap') {
basestr += 'asdasgfdwqew'
} else {
basestr += 'WQdflHhKjdafsj21321jkl531l45'
}
basestr = md5(md5(basestr))
const randomStr = md5(String(Date.now() + Math.random()))
const signature =
randomStr.substring(0, 5) + basestr + randomStr.substring(6, 12)
request.headers.set('signature', signature)
return request
})
}
/**
* 为axios实例注入请求加解密拦截器
* @param axios axios实例
* @param signKeys 密钥列表,如果不传则使用默认密钥
*/
export function useEncryptInterceptor(
axios: AxiosInstance,
signKeys: SignKey[] = DEFAULT_SIGN_KEYS
) {
axios.interceptors.request.use(
(request: InternalAxiosRequestConfig<any> & ApiOptions) => {
if (request.encryption === false) return request
let data = request.data
const strData =
typeof data === 'string'
? data
: typeof data === 'undefined' || data === null
? '{}'
: JSON.stringify(data)
let sign_name = request.headers.get('sign_name') as string
let signKey: SignKey
if (sign_name) {
signKey = signKeys.find((item) => item.name === sign_name)!
if (!signKey) {
signKey = sample(signKeys)!
}
} else {
signKey = sample(signKeys)!
}
const { key, name, iv } = signKey
request.headers.set('sign_name', name)
const encrypted = aesEncrypt(strData, key, iv)
const base64 = base64Encode(encrypted)
const sign_data = encodeURIComponent(base64)
request.data = { sign_data }
return request
}
)
axios.interceptors.response.use((resp) => {
const sign_name = resp.config.headers.get('sign_name')
if (!sign_name || typeof resp.data !== 'object' || resp.data === null) {
return resp
}
const signKey = signKeys.find((t) => t.name === sign_name)
if (!signKey) return resp
if (typeof resp.data.sign_data !== 'string' || !resp.data.sign_data) {
return resp
}
let signData = resp.data.sign_data
signData = decodeURIComponent(signData)
signData = base64Decode(signData)
signData = aesDecrypt(signData, signKey.key, signKey.iv)
resp.data = JSON.parse(signData)
return resp
})
}
import axios, {
AxiosInstance,
AxiosInterceptorManager,
AxiosPromise,
CreateAxiosDefaults,
} from 'axios'
import decryptResponse from './decrypt-response'
import encryptRequest from './encrypt-request'
import injectCommonParams from './inject-common-params'
import injectSignature from './inject-signature'
import {
ApiAxiosRequestConfig,
ApiAxiosResponse,
ApiRequestData,
ApiRequestDefaultOptions,
GetterOrValue,
InternalApiAxiosRequestConfig,
} from './types'
import { sample } from 'lodash-es'
/**
* 用于执行接口请求的,拥有特殊选项的Axios请求实例
*/
export interface ApiAxiosInstance {
<TData = any, TExtend extends object = {}>(config: ApiAxiosRequestConfig): Promise<
ApiAxiosResponse<TData, TExtend>
>
request<TData = any, TExtend extends object = {}>(
config: ApiAxiosRequestConfig,
): Promise<ApiAxiosResponse<TData, TExtend>>
/**
* 设置在实例上的默认配置项
*/
defaults: AxiosInstance['defaults'] & ApiRequestDefaultOptions
/**
* 拦截器
*/
interceptors: {
/**
* 请求拦截器
*/
request: AxiosInterceptorManager<InternalApiAxiosRequestConfig>
/**
* 响应拦截器
*/
response: AxiosInterceptorManager<ApiAxiosResponse>
}
}
export interface CreateInstanceDefaults
extends CreateAxiosDefaults<ApiRequestData>,
ApiRequestDefaultOptions {}
/**
* 可以被简单处理的内置请求配置项
*/
const INTERNAL_OPTION_KEYS: (keyof ApiRequestDefaultOptions)[] = [
'source',
'device',
'app_version',
'token',
'partner_key',
'sdk_version',
'signature_secret',
]
/**
* 创建用于请求接口的Axios实例
*/
export function createAxiosInstance(): ApiAxiosInstance {
const instance = axios.create()
//按倒序注入请求拦截器,最后处理的最先注入
//加密请求体
instance.interceptors.request.use(encryptRequest)
//计算签名并注入
instance.interceptors.request.use(injectSignature)
//自动填充公参
instance.interceptors.request.use(injectCommonParams)
//响应拦截器,解密响应体
instance.interceptors.response.use(decryptResponse)
/**
* 实际的接口调用方法
* @param config
*/
const request = async (config: ApiAxiosRequestConfig): AxiosPromise => {
const new_config = { ...config }
//先对配置项进行初始化,把默认配置项里的函数型配置项,转换为具体的值
//可以被自动转换的配置
for (const key of INTERNAL_OPTION_KEYS) {
new_config[key] = await getValues<any>(
config[key],
(instance.defaults as ApiRequestDefaultOptions)[key],
)
}
//特殊的加解密密钥配置
const encryption_key = await getValues(
config.encryption_key,
(instance.defaults as ApiRequestDefaultOptions).encryption_key,
)
if (Array.isArray(encryption_key)) {
new_config.encryption_key = sample(encryption_key)
} else {
new_config.encryption_key = encryption_key
}
//最后调用原始axios实例上的方法
return instance.request(new_config)
}
Object.defineProperties(request, {
defaults: {
value: instance.defaults,
writable: false,
},
interceptors: {
value: instance.interceptors,
writable: false,
},
request: {
value: request,
writable: false,
},
})
return request as unknown as ApiAxiosInstance
}
/**
* 读取一个配置项的值,传入的可以是具体的值,也可以是获取的方法
* @param getterOrValue
*/
async function getValue<T>(getterOrValue: GetterOrValue<T> | undefined): Promise<T | undefined> {
return typeof getterOrValue === 'function' ? (getterOrValue as () => T)() : getterOrValue
}
/**
* 读取多个配置项的值,返回找到的第一个有效的配置项,传入的可以是具体的值,也可以是获取的方法
* @param values
* @returns
*/
async function getValues<T>(...values: (GetterOrValue<T> | undefined)[]): Promise<T | undefined> {
for (const getterOrValue of values) {
const value = await getValue(getterOrValue)
if (typeof value !== 'undefined') return value
}
}
import { aesDecrypt, base64Decode } from '../utils/security'
import { ApiAxiosResponse } from './types'
/**
* 解密响应体数据
* @param response
*/
export default function decryptResponse(
response: ApiAxiosResponse<any, { encrypted_data?: string | null }>,
): ApiAxiosResponse {
//响应体不是对象
if (typeof response.data !== 'object' || !response.data) return response
//响应体上没有encrypted_data属性
if (typeof response.data.encrypted_data !== 'string' || !response.data.encrypted_data)
return response
//请求配置上没有加解密参数
if (!response.config.encryption_key) return response
//进行解密,先做一次aes解密
const { key, iv } = response.config.encryption_key
const decrypted = aesDecrypt(response.data.encrypted_data, key, iv)
//然后做一次base64解码
const base64_decoded = base64Decode(decrypted)
//最后做一次url解码
const url_decoded = decodeURIComponent(base64_decoded)
//JSON解析并覆盖原data属性
response.data.data = JSON.parse(url_decoded)
return response
}
import { aesEncrypt, base64Encode } from '../utils/security'
import { InternalApiAxiosRequestConfig } from './types'
/**
* 加密请求的数据
*/
export default function encryptRequest(
config: InternalApiAxiosRequestConfig,
): InternalApiAxiosRequestConfig {
const { encryption_key, data } = config
//如果没有设置密钥就不加密了
if (!encryption_key) return config
const { name, key, iv } = encryption_key
//先得到原始请求体的JSON字符串
const json = JSON.stringify(data ?? {})
//先进行aes加密
const encrypted = aesEncrypt(json, key, iv)
//接下来是base64转码
const base64 = base64Encode(encrypted)
//最后做一次urlencode
const urlEncoded = encodeURIComponent(base64)
//写入请求体
config.data = {
sign_data: urlEncoded,
}
//在请求头中写入密钥名
config.headers.set('sign_name', name)
return config
}
export * from './create'
export * from './types'
import { AxiosRequestHeaders } from 'axios'
import { unixTimestamp } from '../utils/datetime'
import { md5 } from '../utils/security'
import { InternalApiAxiosRequestConfig } from './types'
/**
* 自动向请求中注入公参
* @param config
*/
export default function injectCommonParams(
config: InternalApiAxiosRequestConfig,
): InternalApiAxiosRequestConfig {
fillHeader(config.headers, 'source', () => config.source)
fillHeader(config.headers, 'device', () => config.device)
fillHeader(config.headers, 'appversion', () => config.app_version)
fillHeader(config.headers, 'token', () => config.token)
fillHeader(config.headers, 'partner_key', () => config.partner_key)
fillHeader(config.headers, 'sdk_version', () => config.sdk_version)
fillHeader(config.headers, 'timestamp', () => unixTimestamp().toString())
fillHeader(config.headers, 'request_number', () => md5((Date.now() + Math.random()).toString()))
return config
}
/**
* 向请求头中注入公参,如果已经设置了对应的请求头则不再覆盖
*/
export function fillHeader(
headers: AxiosRequestHeaders,
name: string,
creator: () => string | undefined,
): void {
if (headers.has(name)) return
const value = creator()
if (value) {
headers.set(name, value)
}
}
import { md5 } from '../utils/security'
import { InternalApiAxiosRequestConfig } from './types'
/**
* 请求头中参与签名的字段
*/
const HEADER_KEYS = [
'source',
'device',
'app_version',
'token',
'partner_key',
'sdk_version',
'timestamp',
'request_number',
]
/**
* 注入请求签名
* @param config
*/
export default function injectSignature(
config: InternalApiAxiosRequestConfig,
): InternalApiAxiosRequestConfig {
//先获取签名密钥,如果没有密钥则不签名
const { signature_secret } = config
if (typeof signature_secret !== 'string' || signature_secret === '') return config
/**
* 待签名的数据
*/
const data = {
...config.data,
}
//取出请求头中参与签名的字段
HEADER_KEYS.forEach((key) => {
if (config.headers.has(key)) {
data[key] = config.headers.get(key)
}
})
config.headers.set('signature', createSignature(data, signature_secret))
return config
}
/**
* 计算签名
* @param data
* @param secret
*/
export function createSignature(data: Record<string, any>, secret: string): string {
//先对参数名按字典序排个序
const entries = Object.entries(data).sort((t1, t2) => t1[0].localeCompare(t2[0]))
//再根据参数值,生成对应的字符串值
const values = entries.map(([_, value]) => {
if (
typeof value === 'undefined' ||
typeof value === 'function' ||
typeof value === 'symbol' ||
value === null
) {
return ''
} else if (typeof value === 'object') {
return JSON.stringify(value) ?? ''
} else {
return String(value)
}
})
//拼接原始字符串值,并在后面追加签名密钥,得到基础签名串
const baseStr = values.join('') + secret
//进行2次md5,得到最终签名串
return md5(md5(baseStr))
}
import {
AxiosInstance,
AxiosInterceptorManager,
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios'
/**
* 接口传递给服务端的数据类型
*/
export type ApiRequestData = Record<string, any>
/**
* 接口响应基础数据结构
*/
export interface ApiRespBase<TData = any> {
/**
* 接口响应码,200表示请求成功,非200表示请求失败
*/
code: number
/**
* 错误消息
*/
msg: string
/**
* 响应数据
*/
data: TData
}
/**
* 接口响应完整数据结构
*/
export type ApiResp<TData = any, TExtend extends object = {}> = TExtend & ApiRespBase<TData>
/**
* 接口请求数据加解密密钥
*/
export interface ApiEncryptionKey {
/**
* 密钥名
*/
name: string
/**
* 密钥值
*/
key: string
/**
* 向量
*/
iv: string
}
/**
* 接口请求的配置项
*/
export interface ApiRequestOptions {
/**
* 请求的包名,如果不设置,默认为`xinxiuweb`
*/
source?: string
/**
* 请求的设备类型,如果不设置,默认为`wap`
*/
device?: string
/**
* 当前客户端的版本号,如果不设置,默认为`1.0.0`
*/
app_version?: string
/**
* 用户的token
*/
token?: string
/**
* 渠道标识
*/
partner_key?: string
/**
* 渠道SDK版本号
*/
sdk_version?: string
/**
* 用于计算签名的密钥,如果不设置,默认为`asdasgfdwqew`
*/
signature_secret?: string
/**
* 接口数据加解密密钥
*/
encryption_key?: ApiEncryptionKey
}
export type GetterOrValue<T> = T | (() => T | Promise<T>)
/**
* 接口请求的默认配置项
*/
export type ApiRequestDefaultOptions = {
[K in keyof ApiRequestOptions]?: GetterOrValue<ApiRequestOptions[K]>
} & {
/**
* 接口数据加解密密钥
*/
encryption_key?: GetterOrValue<ApiEncryptionKey | ApiEncryptionKey[] | undefined>
}
export type InternalApiAxiosRequestConfig = InternalAxiosRequestConfig<ApiRequestData> &
ApiRequestOptions
export interface ApiAxiosResponse<TData = any, TExtend extends object = {}>
extends AxiosResponse<ApiResp<TData, TExtend>, ApiRequestData> {
config: InternalApiAxiosRequestConfig
}
export interface ApiAxiosRequestConfig
extends AxiosRequestConfig<ApiRequestData>,
ApiRequestOptions {
url: string
}
export {
type ApiOptions,
type SignKey,
DEFAULT_SIGN_KEYS,
DEFAULT_API_OPTIONS,
useCommonParamsInterceptor,
useSignatureInterceptor,
useEncryptInterceptor,
} from './api'
export * from './api'
export * from './utils'
/**
* 获取unix时间戳
* @param time
* @returns
*/
export function unixTimestamp(time: number | Date = Date.now()) {
if (time instanceof Date) {
time = time.valueOf()
}
return Math.floor(time / 1000)
}
export * from './datetime'
export * from './security'
......@@ -21,7 +21,7 @@ export function base64Decode(input: string): string {
}
/**
* AES-CBC-128加密
* AES-CBC-256加密
* @param input 待加密的数据
* @param key 密钥
* @param iv 向量
......@@ -35,7 +35,7 @@ export function aesEncrypt(input: string, key: string, iv: string): string {
}
/**
* AES-CBC-128解密
* AES-CBC-256解密
* @param input 待加密的数据
* @param key 密钥
* @param iv 向量
......
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"strict": true,
"allowJs": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"moduleResolution": "node",
"skipLibCheck": true,
"useUnknownInCatchVariables": true
},
"include": ["src/**/*.ts"]
}
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment