🚧 improve image encryption and decryption

This commit is contained in:
2025-03-22 13:05:50 +08:00
parent 3a03224f8c
commit 781a71a27c
39 changed files with 1274 additions and 977 deletions

View File

@@ -35,13 +35,18 @@ func (l *GetCoordinateListLogic) GetCoordinateList() (resp *types.CoordinateList
storageLocation.ID,
storageLocation.Longitude,
storageLocation.Latitude,
storageLocation.Country,
storageLocation.Province,
storageLocation.City,
storageInfo.ID.Count().As("image_count"),
).Join(
storageInfo,
storageLocation.ID.EqCol(storageInfo.LocationID),
).Where(storageLocation.UserID.Eq(uid),
storageInfo.UserID.Eq(uid),
).Scan(&records)
).
Group(storageLocation.ID).
Scan(&records)
if err != nil {
return nil, err
}

View File

@@ -1,24 +1,20 @@
package storage
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/go-resty/resty/v2"
"github.com/redis/go-redis/v9"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
"gorm.io/gen"
"io"
"math/rand"
"net/http"
"gorm.io/gorm"
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
"schisandra-album-cloud-microservices/common/captcha/verify"
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
storageConfig "schisandra-album-cloud-microservices/common/storage/config"
"schisandra-album-cloud-microservices/common/utils"
"sort"
"sync"
"time"
@@ -31,9 +27,8 @@ import (
type GetPrivateImageListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
RestyClient *resty.Client
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetPrivateImageListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPrivateImageListLogic {
@@ -41,16 +36,6 @@ func NewGetPrivateImageListLogic(ctx context.Context, svcCtx *svc.ServiceContext
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
RestyClient: resty.New().
SetTimeout(30 * time.Second). // 总超时时间
SetRetryCount(3). // 重试次数
SetRetryWaitTime(5 * time.Second). // 重试等待时间
SetRetryMaxWaitTime(30 * time.Second). // 最大重试等待
AddRetryCondition(func(r *resty.Response, err error) bool {
return r.StatusCode() == http.StatusTooManyRequests ||
err != nil ||
r.StatusCode() >= 500
}),
}
}
@@ -59,8 +44,29 @@ func (l *GetPrivateImageListLogic) GetPrivateImageList(req *types.PrivateImageLi
if !ok {
return nil, errors.New("user_id not found")
}
captcha := verify.VerifyBasicTextCaptcha(req.Dots, req.Key, l.svcCtx.RedisClient, l.ctx)
if !captcha {
return nil, errors.New("验证错误")
}
if req.Password == "" {
return nil, errors.New("密码不能为空")
}
authUser := l.svcCtx.DB.ScaAuthUser
userInfo, err := authUser.
Select(authUser.UID, authUser.Password).
Where(authUser.UID.Eq(uid)).First()
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
if userInfo == nil {
return nil, errors.New("密码错误")
}
if !utils.Verify(userInfo.Password, req.Password) {
return nil, errors.New("密码错误")
}
storageInfo := l.svcCtx.DB.ScaStorageInfo
storageThumb := l.svcCtx.DB.ScaStorageThumb
conditions := []gen.Condition{
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
@@ -74,10 +80,16 @@ func (l *GetPrivateImageListLogic) GetPrivateImageList(req *types.PrivateImageLi
storageInfo.ID,
storageInfo.FileName,
storageInfo.CreatedAt,
storageInfo.Path).
storageThumb.ThumbPath,
storageInfo.Path,
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize,
).
LeftJoin(storageThumb, storageInfo.ID.EqCol(storageThumb.InfoID)).
Where(conditions...).
Order(storageInfo.CreatedAt.Desc()).Scan(&storageInfoList)
if err != nil {
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
if len(storageInfoList) == 0 {
@@ -110,48 +122,15 @@ func (l *GetPrivateImageListLogic) GetPrivateImageList(req *types.PrivateImageLi
g.Go(func() error {
defer sem.Release(1)
// 生成单条缓存键(包含文件唯一标识)
imageCacheKey := fmt.Sprintf("%s%s:%s:%s:%s:%v",
constant.ImageCachePrefix,
uid,
"list",
req.Provider,
req.Bucket,
dbFileInfo.ID)
// 尝试获取单条缓存
if cached, err := l.svcCtx.RedisClient.Get(l.ctx, imageCacheKey).Result(); err == nil {
var meta types.ImageMeta
if err := json.Unmarshal([]byte(cached), &meta); err == nil {
parse, err := time.Parse("2006-01-02 15:04:05", meta.CreatedAt)
if err == nil {
logx.Error("Parse Time Error:", err)
return nil
}
date := parse.Format("2006年1月2日 星期") + WeekdayMap[parse.Weekday()]
value, _ := groupedImages.LoadOrStore(date, []types.ImageMeta{})
images := value.([]types.ImageMeta)
images = append(images, meta)
groupedImages.Store(date, images)
return nil
}
}
weekday := WeekdayMap[dbFileInfo.CreatedAt.Weekday()]
date := dbFileInfo.CreatedAt.Format("2006年1月2日 星期" + weekday)
url, err := service.PresignedURL(l.ctx, ossConfig.BucketName, dbFileInfo.Path, time.Minute*30)
if err != nil {
logx.Error(err)
return err
}
imageBytes, err := l.DownloadAndDecrypt(l.ctx, url, uid)
if err != nil {
logx.Error(err)
return err
}
imageData, err := l.svcCtx.XCipher.Decrypt(imageBytes, []byte(uid))
thumbnailUrl, err := service.PresignedURL(l.ctx, ossConfig.BucketName, dbFileInfo.ThumbPath, time.Minute*30)
if err != nil {
logx.Error(err)
return err
}
// 使用 Load 或 Store 确保原子操作
value, _ := groupedImages.LoadOrStore(date, []types.ImageMeta{})
images := value.([]types.ImageMeta)
@@ -159,7 +138,7 @@ func (l *GetPrivateImageListLogic) GetPrivateImageList(req *types.PrivateImageLi
images = append(images, types.ImageMeta{
ID: dbFileInfo.ID,
FileName: dbFileInfo.FileName,
URL: base64.StdEncoding.EncodeToString(imageData),
Thumbnail: thumbnailUrl,
Width: dbFileInfo.ThumbW,
Height: dbFileInfo.ThumbH,
CreatedAt: dbFileInfo.CreatedAt.Format("2006-01-02 15:04:05"),
@@ -167,14 +146,6 @@ func (l *GetPrivateImageListLogic) GetPrivateImageList(req *types.PrivateImageLi
// 重新存储更新后的图像列表
groupedImages.Store(date, images)
// 缓存单条数据24小时基础缓存 + 随机防雪崩)
if data, err := json.Marshal(images); err == nil {
expire := 24*time.Hour + time.Duration(rand.Intn(3600))*time.Second
if err := l.svcCtx.RedisClient.Set(l.ctx, imageCacheKey, data, expire).Err(); err != nil {
logx.Error("Failed to cache image meta:", err)
}
}
return nil
})
}
@@ -263,27 +234,3 @@ func (l *GetPrivateImageListLogic) getOssConfigFromCacheOrDb(cacheKey, uid, prov
return ossConfig, nil
}
func (l *GetPrivateImageListLogic) DownloadAndDecrypt(ctx context.Context, url string, uid string) ([]byte, error) {
resp, err := l.RestyClient.R().
SetContext(ctx).
SetDoNotParseResponse(true). // 保持原始响应流
Get(url)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.RawBody().Close()
if resp.StatusCode() != http.StatusOK {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode(), resp.Status())
}
// 使用缓冲区分块读取
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, resp.RawBody()); err != nil {
return nil, fmt.Errorf("read response body failed: %w", err)
}
return buf.Bytes(), nil
}

View File

@@ -0,0 +1,218 @@
package storage
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/go-resty/resty/v2"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
"io"
"net/http"
"os"
"path/filepath"
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
"schisandra-album-cloud-microservices/common/hybrid_encrypt"
storageConfig "schisandra-album-cloud-microservices/common/storage/config"
"schisandra-album-cloud-microservices/common/utils"
"time"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetPrivateImageUrlLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
RestyClient *resty.Client
}
func NewGetPrivateImageUrlLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPrivateImageUrlLogic {
return &GetPrivateImageUrlLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
RestyClient: resty.New().
SetTimeout(30 * time.Second). // 总超时时间
SetRetryCount(3). // 重试次数
SetRetryWaitTime(5 * time.Second). // 重试等待时间
SetRetryMaxWaitTime(30 * time.Second). // 最大重试等待
AddRetryCondition(func(r *resty.Response, err error) bool {
return r.StatusCode() == http.StatusTooManyRequests ||
err != nil ||
r.StatusCode() >= 500
}),
}
}
// 修改函数签名和实现
func (l *GetPrivateImageUrlLogic) GetPrivateImageUrl(req *types.SinglePrivateImageRequest) (string, error) {
uid, ok := l.ctx.Value("user_id").(string)
if !ok {
return "", errors.New("user_id not found")
}
// 构建缓存key
cacheKey := fmt.Sprintf("%s%s:%s:%v", constant.ImageCachePrefix, uid, "encrypted", req.ID)
// 检查缓存
if cachedData, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result(); err == nil {
return cachedData, nil
}
storageInfo := l.svcCtx.DB.ScaStorageInfo
authUser := l.svcCtx.DB.ScaAuthUser
var result struct {
ID int64 `json:"id"`
Path string `json:"path"`
Password string `json:"password"`
FileType string `json:"file_type"`
}
err := storageInfo.
Select(
storageInfo.ID,
storageInfo.Path,
storageInfo.FileType,
authUser.Password).
LeftJoin(authUser, authUser.UID.EqCol(storageInfo.UserID)).
Where(storageInfo.ID.Eq(req.ID), storageInfo.UserID.Eq(uid),
storageInfo.IsEncrypted.Eq(constant.Encrypt), storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket), authUser.UID.Eq(uid)).
Group(storageInfo.ID).Scan(&result)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return "", err
}
if result.ID == 0 {
return "", errors.New("image not found")
}
verify := utils.Verify(result.Password, req.Password)
if !verify {
return "", errors.New("invalid password")
}
// 加载用户oss配置信息
cacheOssConfigKey := constant.UserOssConfigPrefix + uid + ":" + req.Provider
ossConfig, err := l.getOssConfigFromCacheOrDb(cacheOssConfigKey, uid, req.Provider)
if err != nil {
return "", err
}
service, err := l.svcCtx.StorageManager.GetStorage(uid, ossConfig)
if err != nil {
return "", errors.New("get storage failed")
}
url, err := service.PresignedURL(l.ctx, ossConfig.BucketName, result.Path, time.Minute*15)
if err != nil {
logx.Error(err)
return "", errors.New("get private image url failed")
}
resp, err := l.RestyClient.R().
SetContext(l.ctx).
SetDoNotParseResponse(true). // 保持原始响应流
Get(url)
if err != nil {
return "", fmt.Errorf("download private image failed: %w", err)
}
defer resp.RawBody().Close()
body, err := io.ReadAll(resp.RawBody())
if err != nil {
return "", err
}
dir, err := os.Getwd()
if err != nil {
return "", err
}
privateKeyPath := filepath.Join(dir, l.svcCtx.Config.Encrypt.PrivateKey)
privateKey, err := os.ReadFile(privateKeyPath)
if err != nil {
return "", err
}
pem, err := hybrid_encrypt.ImportPrivateKeyPEM(privateKey)
if err != nil {
return "", err
}
image, err := hybrid_encrypt.DecryptImage(pem, body)
if err != nil {
return "", err
}
base64Str := base64.StdEncoding.EncodeToString(image)
// 设置缓存过期时间设为12小时
err = l.svcCtx.RedisClient.Set(l.ctx, cacheKey, base64Str, 12*time.Hour).Err()
if err != nil {
logx.Errorf("cache private image failed: %v", err)
}
return base64Str, nil
}
// 提取解密操作为函数
func (l *GetPrivateImageUrlLogic) decryptConfig(config *model.ScaStorageConfig) (*storageConfig.StorageConfig, error) {
accessKey, err := encrypt.Decrypt(config.AccessKey, l.svcCtx.Config.Encrypt.Key)
if err != nil {
return nil, errors.New("decrypt access key failed")
}
secretKey, err := encrypt.Decrypt(config.SecretKey, l.svcCtx.Config.Encrypt.Key)
if err != nil {
return nil, errors.New("decrypt secret key failed")
}
return &storageConfig.StorageConfig{
Provider: config.Provider,
Endpoint: config.Endpoint,
AccessKey: accessKey,
SecretKey: secretKey,
BucketName: config.Bucket,
Region: config.Region,
}, nil
}
// 从缓存或数据库中获取 OSS 配置
func (l *GetPrivateImageUrlLogic) getOssConfigFromCacheOrDb(cacheKey, uid, provider string) (*storageConfig.StorageConfig, error) {
result, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result()
if err != nil && !errors.Is(err, redis.Nil) {
return nil, errors.New("get oss config failed")
}
var ossConfig *storageConfig.StorageConfig
if result != "" {
var redisOssConfig model.ScaStorageConfig
if err = json.Unmarshal([]byte(result), &redisOssConfig); err != nil {
return nil, errors.New("unmarshal oss config failed")
}
return l.decryptConfig(&redisOssConfig)
}
// 缓存未命中,从数据库中加载
scaOssConfig := l.svcCtx.DB.ScaStorageConfig
dbOssConfig, err := scaOssConfig.Where(scaOssConfig.UserID.Eq(uid), scaOssConfig.Provider.Eq(provider)).First()
if err != nil {
return nil, err
}
// 缓存数据库配置
ossConfig, err = l.decryptConfig(dbOssConfig)
if err != nil {
return nil, err
}
marshalData, err := json.Marshal(dbOssConfig)
if err != nil {
return nil, errors.New("marshal oss config failed")
}
err = l.svcCtx.RedisClient.Set(l.ctx, cacheKey, marshalData, 0).Err()
if err != nil {
return nil, errors.New("set oss config failed")
}
return ossConfig, nil
}

View File

@@ -14,6 +14,7 @@ import (
"io"
"mime/multipart"
"net/http"
"os"
"path"
"path/filepath"
"schisandra-album-cloud-microservices/app/aisvc/rpc/pb"
@@ -22,6 +23,7 @@ import (
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
"schisandra-album-cloud-microservices/common/hybrid_encrypt"
"schisandra-album-cloud-microservices/common/storage/config"
"strings"
"sync"
@@ -50,13 +52,16 @@ func (l *UploadFileLogic) UploadFile(r *http.Request) (resp string, err error) {
if err != nil {
return "", err
}
// 解析上传配置信息
settingResult, err := l.parseUploadSettingResult(r)
if err != nil {
return "", err
}
// 解析上传的文件
file, header, err := l.getUploadedFile(r)
if err != nil {
return "", err
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
@@ -76,12 +81,6 @@ func (l *UploadFileLogic) UploadFile(r *http.Request) (resp string, err error) {
return "", err
}
// 解析上传配置信息
settingResult, err := l.parseUploadSettingResult(r)
if err != nil {
return "", err
}
// 使用 `errgroup.Group` 处理并发任务
var (
faceId int64
@@ -110,13 +109,25 @@ func (l *UploadFileLogic) UploadFile(r *http.Request) (resp string, err error) {
return nil
})
}
var imageBytes []byte
var uploadReader io.Reader = bytes.NewReader(data)
if settingResult.Encrypt {
encryptedData, err := l.svcCtx.XCipher.Encrypt(data, []byte(uid))
dir, err := os.Getwd()
if err != nil {
return "", err
}
imageBytes = encryptedData
publicKeyPath := filepath.Join(dir, l.svcCtx.Config.Encrypt.PublicKey)
publicKey, err := os.ReadFile(publicKeyPath)
if err != nil {
return "", err
}
pem, err := hybrid_encrypt.ImportPublicKeyPEM(publicKey)
if err != nil {
return "", err
}
image, err := hybrid_encrypt.EncryptImage(pem, data)
uploadReader = bytes.NewReader(image)
}
// 上传文件到 OSS
@@ -126,7 +137,7 @@ func (l *UploadFileLogic) UploadFile(r *http.Request) (resp string, err error) {
}
defer sem.Release(1)
fileUrl, thumbUrl, err := l.uploadFileToOSS(uid, header, bytes.NewReader(imageBytes), thumbnail, result)
fileUrl, thumbUrl, err := l.uploadFileToOSS(uid, header, uploadReader, thumbnail, result, settingResult)
if err != nil {
return err
}
@@ -210,7 +221,7 @@ func (l *UploadFileLogic) parseUploadSettingResult(r *http.Request) (types.Uploa
}
// 上传文件到 OSS
func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHeader, file io.Reader, thumbnail io.Reader, result types.File) (string, string, error) {
func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHeader, file io.Reader, thumbnail io.Reader, result types.File, settingResult types.UploadSetting) (string, string, error) {
cacheKey := constant.UserOssConfigPrefix + uid + ":" + result.Provider
ossConfig, err := l.getOssConfigFromCacheOrDb(cacheKey, uid, result.Provider)
if err != nil {
@@ -220,7 +231,6 @@ func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHead
if err != nil {
return "", "", errors.New("get storage failed")
}
objectKey := path.Join(
constant.ImageSpace,
uid,
@@ -228,6 +238,15 @@ func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHead
l.classifyFile(result.FileType, result.IsScreenshot),
fmt.Sprintf("%s_%s%s", strings.TrimSuffix(header.Filename, filepath.Ext(header.Filename)), kgo.SimpleUuid(), filepath.Ext(header.Filename)),
)
if settingResult.Encrypt {
objectKey = path.Join(
constant.ImageSpace,
uid,
time.Now().Format("2006/01"), // 按年/月划分目录
"encrypted",
fmt.Sprintf("%s_%s%s", strings.TrimSuffix(header.Filename, filepath.Ext(header.Filename)), kgo.SimpleUuid(), ".enc"),
)
}
_, err = service.UploadFileSimple(l.ctx, ossConfig.BucketName, objectKey, file, map[string]string{
"Content-Type": header.Header.Get("Content-Type"),
@@ -241,7 +260,7 @@ func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHead
uid,
time.Now().Format("2006/01"), // 按年/月划分目录
l.classifyFile(result.FileType, result.IsScreenshot),
fmt.Sprintf("%s_%s%s", strings.TrimSuffix(header.Filename, filepath.Ext(header.Filename)), kgo.SimpleUuid(), filepath.Ext(header.Filename)),
fmt.Sprintf("%s_%s", strings.TrimSuffix(header.Filename, filepath.Ext(header.Filename)), kgo.SimpleUuid()),
)
_, err = service.UploadFileSimple(l.ctx, ossConfig.BucketName, thumbObjectKey, thumbnail, map[string]string{
"Content-Type": header.Header.Get("Content-Type"),
@@ -249,6 +268,7 @@ func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHead
if err != nil {
return "", "", errors.New("upload thumbnail file failed")
}
return objectKey, thumbObjectKey, nil
}