Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c8c98b756 | |||
| 2ec41eb3a5 | |||
| d36d66bbf7 | |||
| d3978e1f60 | |||
| 616abd2364 |
287
pkg/cache/helper.go
vendored
Normal file
287
pkg/cache/helper.go
vendored
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ========== 分布式锁 ==========
|
||||||
|
|
||||||
|
// Lock 分布式锁
|
||||||
|
type Lock struct {
|
||||||
|
repo RedisRepo
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
expiry time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLock 创建分布式锁
|
||||||
|
func NewLock(repo RedisRepo, key, value string, expiry time.Duration) *Lock {
|
||||||
|
return &Lock{
|
||||||
|
repo: repo,
|
||||||
|
key: key,
|
||||||
|
value: value,
|
||||||
|
expiry: expiry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acquire 获取锁
|
||||||
|
func (l *Lock) Acquire(ctx context.Context) (bool, error) {
|
||||||
|
return l.repo.SetNX(ctx, l.key, l.value, l.expiry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release 释放锁
|
||||||
|
func (l *Lock) Release(ctx context.Context) error {
|
||||||
|
script := redis.NewScript(`
|
||||||
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
||||||
|
return redis.call("del", KEYS[1])
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
`)
|
||||||
|
|
||||||
|
return script.Run(ctx, l.repo.Client(), []string{l.key}, l.value).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh 刷新锁
|
||||||
|
func (l *Lock) Refresh(ctx context.Context) (bool, error) {
|
||||||
|
script := redis.NewScript(`
|
||||||
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
||||||
|
return redis.call("pexpire", KEYS[1], ARGV[2])
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
`)
|
||||||
|
|
||||||
|
result, err := script.Run(ctx, l.repo.Client(), []string{l.key}, l.value, l.expiry.Milliseconds()).Result()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.(int64) == 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== JSON 序列化 ==========
|
||||||
|
|
||||||
|
// SetJSON 设置JSON对象
|
||||||
|
func SetJSON(ctx context.Context, repo RedisRepo, key string, value interface{}, ttl time.Duration) error {
|
||||||
|
data, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("json marshal 失败: %w", err)
|
||||||
|
}
|
||||||
|
return repo.Set(ctx, key, string(data), ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJSON 获取JSON对象
|
||||||
|
func GetJSON(ctx context.Context, repo RedisRepo, key string, dest interface{}) error {
|
||||||
|
val, err := repo.Get(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if val == "" {
|
||||||
|
return errors.New("key 不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(val), dest); err != nil {
|
||||||
|
return fmt.Errorf("json unmarshal 失败: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 限流器 ==========
|
||||||
|
|
||||||
|
// RateLimiter 限流器(令牌桶算法)
|
||||||
|
type RateLimiter struct {
|
||||||
|
repo RedisRepo
|
||||||
|
key string
|
||||||
|
limit int64 // 最大令牌数
|
||||||
|
interval time.Duration // 时间窗口
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRateLimiter 创建限流器
|
||||||
|
func NewRateLimiter(repo RedisRepo, key string, limit int64, interval time.Duration) *RateLimiter {
|
||||||
|
return &RateLimiter{
|
||||||
|
repo: repo,
|
||||||
|
key: key,
|
||||||
|
limit: limit,
|
||||||
|
interval: interval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow 检查是否允许(简单计数器实现)
|
||||||
|
func (r *RateLimiter) Allow(ctx context.Context) (bool, error) {
|
||||||
|
pipe := r.repo.Pipeline()
|
||||||
|
|
||||||
|
incrCmd := pipe.Incr(ctx, r.key)
|
||||||
|
pipe.Expire(ctx, r.key, r.interval)
|
||||||
|
|
||||||
|
if _, err := pipe.Exec(ctx); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := incrCmd.Result()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count <= r.limit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowN 检查是否允许N次
|
||||||
|
func (r *RateLimiter) AllowN(ctx context.Context, n int64) (bool, error) {
|
||||||
|
pipe := r.repo.Pipeline()
|
||||||
|
|
||||||
|
incrCmd := pipe.IncrBy(ctx, r.key, n)
|
||||||
|
pipe.Expire(ctx, r.key, r.interval)
|
||||||
|
|
||||||
|
if _, err := pipe.Exec(ctx); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := incrCmd.Result()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count <= r.limit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remaining 获取剩余次数
|
||||||
|
func (r *RateLimiter) Remaining(ctx context.Context) (int64, error) {
|
||||||
|
val, err := r.repo.Get(ctx, r.key)
|
||||||
|
if err != nil {
|
||||||
|
return r.limit, nil
|
||||||
|
}
|
||||||
|
if val == "" {
|
||||||
|
return r.limit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
if _, err := fmt.Sscanf(val, "%d", &count); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining := r.limit - count
|
||||||
|
if remaining < 0 {
|
||||||
|
remaining = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return remaining, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset 重置限流器
|
||||||
|
func (r *RateLimiter) Reset(ctx context.Context) error {
|
||||||
|
_, err := r.repo.Del(ctx, r.key)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 缓存装饰器 ==========
|
||||||
|
|
||||||
|
// CacheDecorator 缓存装饰器
|
||||||
|
type CacheDecorator struct {
|
||||||
|
repo RedisRepo
|
||||||
|
ttl time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCacheDecorator 创建缓存装饰器
|
||||||
|
func NewCacheDecorator(repo RedisRepo, ttl time.Duration) *CacheDecorator {
|
||||||
|
return &CacheDecorator{
|
||||||
|
repo: repo,
|
||||||
|
ttl: ttl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrSet 获取或设置缓存
|
||||||
|
func (c *CacheDecorator) GetOrSet(ctx context.Context, key string, dest interface{}, loader func() (interface{}, error)) error {
|
||||||
|
// 尝试从缓存获取
|
||||||
|
err := GetJSON(ctx, c.repo, key, dest)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存未命中,执行加载函数
|
||||||
|
data, err := loader()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loader 执行失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置缓存
|
||||||
|
if err := SetJSON(ctx, c.repo, key, data, c.ttl); err != nil {
|
||||||
|
// 缓存设置失败不影响主流程
|
||||||
|
// 可以记录日志
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将数据赋值给dest
|
||||||
|
dataBytes, _ := json.Marshal(data)
|
||||||
|
return json.Unmarshal(dataBytes, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 布隆过滤器(简单实现) ==========
|
||||||
|
|
||||||
|
// BloomFilter 布隆过滤器
|
||||||
|
type BloomFilter struct {
|
||||||
|
repo RedisRepo
|
||||||
|
key string
|
||||||
|
size uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBloomFilter 创建布隆过滤器
|
||||||
|
func NewBloomFilter(repo RedisRepo, key string, size uint64) *BloomFilter {
|
||||||
|
return &BloomFilter{
|
||||||
|
repo: repo,
|
||||||
|
key: key,
|
||||||
|
size: size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add 添加元素
|
||||||
|
func (b *BloomFilter) Add(ctx context.Context, value string) error {
|
||||||
|
// 简单hash
|
||||||
|
hash1 := hashString(value, 0) % b.size
|
||||||
|
hash2 := hashString(value, 1) % b.size
|
||||||
|
hash3 := hashString(value, 2) % b.size
|
||||||
|
|
||||||
|
pipe := b.repo.Pipeline()
|
||||||
|
pipe.SetBit(ctx, b.key, int64(hash1), 1)
|
||||||
|
pipe.SetBit(ctx, b.key, int64(hash2), 1)
|
||||||
|
pipe.SetBit(ctx, b.key, int64(hash3), 1)
|
||||||
|
|
||||||
|
_, err := pipe.Exec(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists 检查元素是否存在
|
||||||
|
func (b *BloomFilter) Exists(ctx context.Context, value string) (bool, error) {
|
||||||
|
hash1 := hashString(value, 0) % b.size
|
||||||
|
hash2 := hashString(value, 1) % b.size
|
||||||
|
hash3 := hashString(value, 2) % b.size
|
||||||
|
|
||||||
|
pipe := b.repo.Pipeline()
|
||||||
|
bit1Cmd := pipe.GetBit(ctx, b.key, int64(hash1))
|
||||||
|
bit2Cmd := pipe.GetBit(ctx, b.key, int64(hash2))
|
||||||
|
bit3Cmd := pipe.GetBit(ctx, b.key, int64(hash3))
|
||||||
|
|
||||||
|
if _, err := pipe.Exec(ctx); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bit1, _ := bit1Cmd.Result()
|
||||||
|
bit2, _ := bit2Cmd.Result()
|
||||||
|
bit3, _ := bit3Cmd.Result()
|
||||||
|
|
||||||
|
return bit1 == 1 && bit2 == 1 && bit3 == 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashString 简单字符串hash
|
||||||
|
func hashString(s string, seed uint64) uint64 {
|
||||||
|
hash := seed
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
hash = hash*31 + uint64(s[i])
|
||||||
|
}
|
||||||
|
return hash
|
||||||
|
}
|
||||||
1127
pkg/cache/redis.go
vendored
1127
pkg/cache/redis.go
vendored
File diff suppressed because it is too large
Load Diff
202
pkg/crypto/client/axios/encryptedAxios.ts
Normal file
202
pkg/crypto/client/axios/encryptedAxios.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||||
|
import { IEncryptor, ISigner, CryptoConfig, EncryptedRequest, EncryptedResponse } from '../crypto/interface';
|
||||||
|
import { uint8ArrayToBase64, base64ToUint8Array, stringToUint8Array, uint8ArrayToString } from '../utils/base64';
|
||||||
|
import { generateUUID } from '../utils/uuid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密Axios实例
|
||||||
|
*/
|
||||||
|
export class EncryptedAxios {
|
||||||
|
private axiosInstance: AxiosInstance;
|
||||||
|
private encryptor: IEncryptor | null = null;
|
||||||
|
private signer: ISigner | null = null;
|
||||||
|
private config: CryptoConfig;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
encryptor?: IEncryptor,
|
||||||
|
signer?: ISigner,
|
||||||
|
config: CryptoConfig = {},
|
||||||
|
axiosConfig?: AxiosRequestConfig
|
||||||
|
) {
|
||||||
|
this.encryptor = encryptor || null;
|
||||||
|
this.signer = signer || null;
|
||||||
|
this.config = {
|
||||||
|
timestampWindow: 5 * 60 * 1000, // 默认5分钟
|
||||||
|
enableTimestamp: true,
|
||||||
|
enableSignature: true,
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建axios实例
|
||||||
|
this.axiosInstance = axios.create(axiosConfig);
|
||||||
|
|
||||||
|
// 添加请求拦截器
|
||||||
|
this.axiosInstance.interceptors.request.use(
|
||||||
|
this.encryptRequestInterceptor.bind(this),
|
||||||
|
(error) => Promise.reject(error)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加响应拦截器
|
||||||
|
this.axiosInstance.interceptors.response.use(
|
||||||
|
this.decryptResponseInterceptor.bind(this),
|
||||||
|
(error) => Promise.reject(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求拦截器 - 加密请求数据
|
||||||
|
*/
|
||||||
|
private async encryptRequestInterceptor(
|
||||||
|
config: InternalAxiosRequestConfig
|
||||||
|
): Promise<InternalAxiosRequestConfig> {
|
||||||
|
// 放行GET和OPTIONS请求
|
||||||
|
if (config.method?.toUpperCase() === 'GET' || config.method?.toUpperCase() === 'OPTIONS') {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有配置加密器,直接返回
|
||||||
|
if (!this.encryptor) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 将请求数据转换为JSON字符串
|
||||||
|
const plaintext = JSON.stringify(config.data || {});
|
||||||
|
const plaintextBytes = stringToUint8Array(plaintext);
|
||||||
|
|
||||||
|
// 加密数据
|
||||||
|
const ciphertext = await this.encryptor.encrypt(plaintextBytes);
|
||||||
|
const encryptedData = uint8ArrayToBase64(ciphertext);
|
||||||
|
|
||||||
|
// 构建加密请求体
|
||||||
|
const encryptedRequest: EncryptedRequest = {
|
||||||
|
data: encryptedData,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
request_id: generateUUID(),
|
||||||
|
algorithm: this.encryptor.name(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成签名
|
||||||
|
if (this.config.enableSignature && this.signer) {
|
||||||
|
const signature = await this.signer.sign(plaintextBytes);
|
||||||
|
encryptedRequest.signature = uint8ArrayToBase64(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替换请求数据
|
||||||
|
config.data = encryptedRequest;
|
||||||
|
config.headers['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
|
// 保存request_id供响应使用
|
||||||
|
config.headers['X-Request-ID'] = encryptedRequest.request_id;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加密请求失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应拦截器 - 解密响应数据
|
||||||
|
*/
|
||||||
|
private async decryptResponseInterceptor(
|
||||||
|
response: AxiosResponse
|
||||||
|
): Promise<AxiosResponse> {
|
||||||
|
// 如果没有配置加密器或响应不是加密格式,直接返回
|
||||||
|
if (!this.encryptor || !response.data || typeof response.data !== 'object') {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是加密响应
|
||||||
|
const encryptedResponse = response.data as EncryptedResponse;
|
||||||
|
if (!encryptedResponse.data || !encryptedResponse.request_id) {
|
||||||
|
// 不是加密响应,直接返回
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 验证时间戳
|
||||||
|
if (this.config.enableTimestamp) {
|
||||||
|
this.verifyTimestamp(encryptedResponse.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解密数据
|
||||||
|
const ciphertext = base64ToUint8Array(encryptedResponse.data);
|
||||||
|
const plaintext = await this.encryptor.decrypt(ciphertext);
|
||||||
|
|
||||||
|
// 验证签名
|
||||||
|
if (this.config.enableSignature && this.signer && encryptedResponse.signature) {
|
||||||
|
const signature = base64ToUint8Array(encryptedResponse.signature);
|
||||||
|
const isValid = await this.signer.verify(plaintext, signature);
|
||||||
|
if (!isValid) {
|
||||||
|
throw new Error('签名验证失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将解密后的数据转换为JSON对象
|
||||||
|
const decryptedData = uint8ArrayToString(plaintext);
|
||||||
|
response.data = JSON.parse(decryptedData);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解密响应失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证时间戳
|
||||||
|
*/
|
||||||
|
private verifyTimestamp(timestamp: number): void {
|
||||||
|
const now = Date.now();
|
||||||
|
const diff = Math.abs(now - timestamp);
|
||||||
|
|
||||||
|
if (diff > (this.config.timestampWindow || 5 * 60 * 1000)) {
|
||||||
|
throw new Error('请求超时');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取axios实例
|
||||||
|
*/
|
||||||
|
getInstance(): AxiosInstance {
|
||||||
|
return this.axiosInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET 请求
|
||||||
|
*/
|
||||||
|
get<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||||
|
return this.axiosInstance.get<T>(url, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST 请求
|
||||||
|
*/
|
||||||
|
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||||
|
return this.axiosInstance.post<T>(url, data, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT 请求
|
||||||
|
*/
|
||||||
|
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||||
|
return this.axiosInstance.put<T>(url, data, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE 请求
|
||||||
|
*/
|
||||||
|
delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||||
|
return this.axiosInstance.delete<T>(url, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH 请求
|
||||||
|
*/
|
||||||
|
patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||||
|
return this.axiosInstance.patch<T>(url, data, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
pkg/crypto/client/crypto/aes.ts
Normal file
90
pkg/crypto/client/crypto/aes.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { IEncryptor } from './interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES-GCM加密器
|
||||||
|
*/
|
||||||
|
export class AESEncryptor implements IEncryptor {
|
||||||
|
private key: CryptoKey | null = null;
|
||||||
|
|
||||||
|
constructor(keyString: string) {
|
||||||
|
this.importKey(keyString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入密钥
|
||||||
|
*/
|
||||||
|
private async importKey(keyString: string): Promise<void> {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const keyData = encoder.encode(keyString.padEnd(32, '0').substring(0, 32));
|
||||||
|
|
||||||
|
this.key = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
keyData,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 256,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密数据
|
||||||
|
*/
|
||||||
|
async encrypt(plaintext: Uint8Array): Promise<Uint8Array> {
|
||||||
|
if (!this.key) {
|
||||||
|
throw new Error('密钥未设置');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机IV
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
|
||||||
|
const encrypted = await crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: iv,
|
||||||
|
},
|
||||||
|
this.key,
|
||||||
|
plaintext
|
||||||
|
);
|
||||||
|
|
||||||
|
// 将IV和密文拼接在一起
|
||||||
|
const result = new Uint8Array(iv.length + encrypted.byteLength);
|
||||||
|
result.set(iv, 0);
|
||||||
|
result.set(new Uint8Array(encrypted), iv.length);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密数据
|
||||||
|
*/
|
||||||
|
async decrypt(ciphertext: Uint8Array): Promise<Uint8Array> {
|
||||||
|
if (!this.key) {
|
||||||
|
throw new Error('密钥未设置');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取IV
|
||||||
|
const iv = ciphertext.slice(0, 12);
|
||||||
|
const data = ciphertext.slice(12);
|
||||||
|
|
||||||
|
const decrypted = await crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: iv,
|
||||||
|
},
|
||||||
|
this.key,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Uint8Array(decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回算法名称
|
||||||
|
*/
|
||||||
|
name(): string {
|
||||||
|
return 'AES-GCM-256';
|
||||||
|
}
|
||||||
|
}
|
||||||
54
pkg/crypto/client/crypto/hmac.ts
Normal file
54
pkg/crypto/client/crypto/hmac.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { ISigner } from './interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HMAC签名器
|
||||||
|
*/
|
||||||
|
export class HMACSigner implements ISigner {
|
||||||
|
private key: CryptoKey | null = null;
|
||||||
|
|
||||||
|
constructor(keyString: string) {
|
||||||
|
this.importKey(keyString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入密钥
|
||||||
|
*/
|
||||||
|
private async importKey(keyString: string): Promise<void> {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const keyData = encoder.encode(keyString);
|
||||||
|
|
||||||
|
this.key = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
keyData,
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: 'SHA-256',
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['sign', 'verify']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成签名
|
||||||
|
*/
|
||||||
|
async sign(data: Uint8Array): Promise<Uint8Array> {
|
||||||
|
if (!this.key) {
|
||||||
|
throw new Error('密钥未设置');
|
||||||
|
}
|
||||||
|
|
||||||
|
const signature = await crypto.subtle.sign('HMAC', this.key, data);
|
||||||
|
return new Uint8Array(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证签名
|
||||||
|
*/
|
||||||
|
async verify(data: Uint8Array, signature: Uint8Array): Promise<boolean> {
|
||||||
|
if (!this.key) {
|
||||||
|
throw new Error('密钥未设置');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await crypto.subtle.verify('HMAC', this.key, signature, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
pkg/crypto/client/crypto/interface.ts
Normal file
50
pkg/crypto/client/crypto/interface.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* 加密器接口
|
||||||
|
*/
|
||||||
|
export interface IEncryptor {
|
||||||
|
encrypt(plaintext: Uint8Array): Promise<Uint8Array>;
|
||||||
|
decrypt(ciphertext: Uint8Array): Promise<Uint8Array>;
|
||||||
|
name(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名器接口
|
||||||
|
*/
|
||||||
|
export interface ISigner {
|
||||||
|
sign(data: Uint8Array): Promise<Uint8Array>;
|
||||||
|
verify(data: Uint8Array, signature: Uint8Array): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置选项
|
||||||
|
*/
|
||||||
|
export interface CryptoConfig {
|
||||||
|
secretKey?: string; // 对称加密密钥
|
||||||
|
signKey?: string; // 签名密钥
|
||||||
|
publicKey?: string; // RSA公钥(PEM格式)
|
||||||
|
privateKey?: string; // RSA私钥(PEM格式)
|
||||||
|
timestampWindow?: number; // 时间戳窗口(毫秒)
|
||||||
|
enableTimestamp?: boolean; // 是否启用时间戳验证
|
||||||
|
enableSignature?: boolean; // 是否启用签名
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密请求体
|
||||||
|
*/
|
||||||
|
export interface EncryptedRequest {
|
||||||
|
data: string; // Base64编码的加密数据
|
||||||
|
signature?: string; // Base64编码的签名
|
||||||
|
timestamp: number; // 时间戳
|
||||||
|
request_id: string; // 请求ID
|
||||||
|
algorithm: string; // 加密算法名称
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密响应体
|
||||||
|
*/
|
||||||
|
export interface EncryptedResponse {
|
||||||
|
data: string; // Base64编码的加密数据
|
||||||
|
signature?: string; // Base64编码的签名
|
||||||
|
timestamp: number; // 时间戳
|
||||||
|
request_id: string; // 请求ID
|
||||||
|
}
|
||||||
125
pkg/crypto/client/crypto/rsa.ts
Normal file
125
pkg/crypto/client/crypto/rsa.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { IEncryptor } from './interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSA加密器(使用Web Crypto API)
|
||||||
|
*/
|
||||||
|
export class RSAEncryptor implements IEncryptor {
|
||||||
|
private publicKey: CryptoKey | null = null;
|
||||||
|
private privateKey: CryptoKey | null = null;
|
||||||
|
|
||||||
|
constructor(publicKeyPEM?: string, privateKeyPEM?: string) {
|
||||||
|
if (publicKeyPEM) {
|
||||||
|
this.importPublicKey(publicKeyPEM);
|
||||||
|
}
|
||||||
|
if (privateKeyPEM) {
|
||||||
|
this.importPrivateKey(privateKeyPEM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入公钥(PEM格式)
|
||||||
|
*/
|
||||||
|
private async importPublicKey(pem: string): Promise<void> {
|
||||||
|
const pemHeader = '-----BEGIN PUBLIC KEY-----';
|
||||||
|
const pemFooter = '-----END PUBLIC KEY-----';
|
||||||
|
const pemContents = pem
|
||||||
|
.replace(pemHeader, '')
|
||||||
|
.replace(pemFooter, '')
|
||||||
|
.replace(/\s/g, '');
|
||||||
|
|
||||||
|
const binaryDer = this.base64ToArrayBuffer(pemContents);
|
||||||
|
|
||||||
|
this.publicKey = await crypto.subtle.importKey(
|
||||||
|
'spki',
|
||||||
|
binaryDer,
|
||||||
|
{
|
||||||
|
name: 'RSA-OAEP',
|
||||||
|
hash: 'SHA-256',
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['encrypt']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入私钥(PEM格式)
|
||||||
|
*/
|
||||||
|
private async importPrivateKey(pem: string): Promise<void> {
|
||||||
|
const pemHeader = '-----BEGIN PRIVATE KEY-----';
|
||||||
|
const pemFooter = '-----END PRIVATE KEY-----';
|
||||||
|
const pemContents = pem
|
||||||
|
.replace(pemHeader, '')
|
||||||
|
.replace(pemFooter, '')
|
||||||
|
.replace(/\s/g, '');
|
||||||
|
|
||||||
|
const binaryDer = this.base64ToArrayBuffer(pemContents);
|
||||||
|
|
||||||
|
this.privateKey = await crypto.subtle.importKey(
|
||||||
|
'pkcs8',
|
||||||
|
binaryDer,
|
||||||
|
{
|
||||||
|
name: 'RSA-OAEP',
|
||||||
|
hash: 'SHA-256',
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['decrypt']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64转ArrayBuffer
|
||||||
|
*/
|
||||||
|
private base64ToArrayBuffer(base64: string): ArrayBuffer {
|
||||||
|
const binaryString = atob(base64);
|
||||||
|
const bytes = new Uint8Array(binaryString.length);
|
||||||
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密数据
|
||||||
|
*/
|
||||||
|
async encrypt(plaintext: Uint8Array): Promise<Uint8Array> {
|
||||||
|
if (!this.publicKey) {
|
||||||
|
throw new Error('公钥未设置');
|
||||||
|
}
|
||||||
|
|
||||||
|
const encrypted = await crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'RSA-OAEP',
|
||||||
|
},
|
||||||
|
this.publicKey,
|
||||||
|
plaintext
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Uint8Array(encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密数据
|
||||||
|
*/
|
||||||
|
async decrypt(ciphertext: Uint8Array): Promise<Uint8Array> {
|
||||||
|
if (!this.privateKey) {
|
||||||
|
throw new Error('私钥未设置');
|
||||||
|
}
|
||||||
|
|
||||||
|
const decrypted = await crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'RSA-OAEP',
|
||||||
|
},
|
||||||
|
this.privateKey,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Uint8Array(decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回算法名称
|
||||||
|
*/
|
||||||
|
name(): string {
|
||||||
|
return 'RSA-OAEP-SHA256';
|
||||||
|
}
|
||||||
|
}
|
||||||
7
pkg/crypto/client/index.ts
Normal file
7
pkg/crypto/client/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export * from './crypto/interface';
|
||||||
|
export * from './crypto/rsa';
|
||||||
|
export * from './crypto/hmac';
|
||||||
|
export * from './crypto/aes';
|
||||||
|
export * from './axios/encryptedAxios';
|
||||||
|
export * from './utils/base64';
|
||||||
|
export * from './utils/uuid';
|
||||||
38
pkg/crypto/client/utils/base64.ts
Normal file
38
pkg/crypto/client/utils/base64.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Uint8Array 转 Base64
|
||||||
|
*/
|
||||||
|
export function uint8ArrayToBase64(bytes: Uint8Array): string {
|
||||||
|
let binary = '';
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64 转 Uint8Array
|
||||||
|
*/
|
||||||
|
export function base64ToUint8Array(base64: string): Uint8Array {
|
||||||
|
const binary = atob(base64);
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字符串转 Uint8Array
|
||||||
|
*/
|
||||||
|
export function stringToUint8Array(str: string): Uint8Array {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
return encoder.encode(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uint8Array 转字符串
|
||||||
|
*/
|
||||||
|
export function uint8ArrayToString(bytes: Uint8Array): string {
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
return decoder.decode(bytes);
|
||||||
|
}
|
||||||
10
pkg/crypto/client/utils/uuid.ts
Normal file
10
pkg/crypto/client/utils/uuid.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* 生成UUID v4
|
||||||
|
*/
|
||||||
|
export function generateUUID(): string {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||||
|
const r = (Math.random() * 16) | 0;
|
||||||
|
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package crypto
|
package crypto
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
// Encryptor 加密器接口
|
// Encryptor 加密器接口
|
||||||
type Encryptor interface {
|
type Encryptor interface {
|
||||||
// Encrypt 加密数据
|
// Encrypt 加密数据
|
||||||
@@ -20,3 +22,27 @@ type Signer interface {
|
|||||||
// Verify 验证签名
|
// Verify 验证签名
|
||||||
Verify(data, signature []byte) error
|
Verify(data, signature []byte) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
SecretKey string // 对称加密密钥
|
||||||
|
SignKey string // 签名密钥
|
||||||
|
TimestampWindow time.Duration // 时间戳允许的时间窗口
|
||||||
|
EnableTimestamp bool // 是否启用时间戳验证
|
||||||
|
EnableSignature bool // 是否启用签名
|
||||||
|
}
|
||||||
|
|
||||||
|
type EncryptedRequest struct {
|
||||||
|
Data string `json:"data"` // 加密后的数据(Base64)
|
||||||
|
Signature string `json:"signature"` // 签名(Base64)
|
||||||
|
Timestamp int64 `json:"timestamp"` // 时间戳
|
||||||
|
RequestID string `json:"request_id"` // 请求ID
|
||||||
|
Algorithm string `json:"algorithm"` // 加密算法
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptedResponse 加密响应体
|
||||||
|
type EncryptedResponse struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
RequestID string `json:"request_id"`
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user