让七月迭代具备可直接部署的配置基线
补齐三套环境配置、测试环境部署门禁与 system_config 初始化,并按当前无企微应用的约束将企微凭据改为明文存储。 Constraint: 当前测试环境尚无企微应用及历史企微密文数据 Rejected: 使用启动环境变量加密企微凭据 | 用户明确要求直接明文保存并移除加密密钥 Confidence: high Scope-risk: moderate Directive: 企微真实闭环完成前保持两个旧审批入口开关为 true Tested: gofmt;git diff --check;bash -n;docker compose config;Gitea workflow YAML 解析;OpenAPI 重新生成 Not-tested: go test;常规 go build;LSP;实际数据库迁移;真实测试环境部署;企微真实联调
This commit is contained in:
@@ -44,15 +44,15 @@ func (r *ApplicationRepository) Create(ctx context.Context, tx *gorm.DB, applica
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update 更新企业微信应用配置及密文凭据。
|
||||
// Update 更新企业微信应用配置及连接凭据。
|
||||
func (r *ApplicationRepository) Update(ctx context.Context, tx *gorm.DB, application *model.WeComApplication) error {
|
||||
updates := map[string]any{
|
||||
"name": application.Name, "secret_ciphertext": application.SecretCiphertext,
|
||||
"callback_token_ciphertext": application.CallbackTokenCiphertext,
|
||||
"encoding_aes_key_ciphertext": application.EncodingAESKeyCiphertext,
|
||||
"default_creator_userid": application.DefaultCreatorUserID,
|
||||
"default_creator_name": application.DefaultCreatorName,
|
||||
"status": application.Status, "updated_by": application.UpdatedBy, "updated_at": application.UpdatedAt,
|
||||
"name": application.Name, "secret": application.Secret,
|
||||
"callback_token": application.CallbackToken,
|
||||
"encoding_aes_key": application.EncodingAESKey,
|
||||
"default_creator_userid": application.DefaultCreatorUserID,
|
||||
"default_creator_name": application.DefaultCreatorName,
|
||||
"status": application.Status, "updated_by": application.UpdatedBy, "updated_at": application.UpdatedAt,
|
||||
}
|
||||
if err := tx.WithContext(ctx).Model(&model.WeComApplication{}).Where("id = ?", application.ID).Updates(updates).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新企业微信应用配置失败")
|
||||
@@ -73,7 +73,7 @@ func (r *ApplicationRepository) UpdateDefaultCreator(ctx context.Context, tx *go
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 分页查询企业微信应用配置及密文凭据。
|
||||
// List 分页查询企业微信应用配置及连接凭据。
|
||||
func (r *ApplicationRepository) List(ctx context.Context, page, pageSize int) ([]model.WeComApplication, int64, error) {
|
||||
query := r.db.WithContext(ctx).Model(&model.WeComApplication{})
|
||||
var total int64
|
||||
@@ -87,7 +87,7 @@ func (r *ApplicationRepository) List(ctx context.Context, page, pageSize int) ([
|
||||
return applications, total, nil
|
||||
}
|
||||
|
||||
// GetEnabled 查询启用的企业微信应用及密文凭据。
|
||||
// GetEnabled 查询启用的企业微信应用及连接凭据。
|
||||
func (r *ApplicationRepository) GetEnabled(ctx context.Context, applicationID uint) (*model.WeComApplication, error) {
|
||||
var application model.WeComApplication
|
||||
if err := r.db.WithContext(ctx).Where("id = ? AND status = ?", applicationID, constants.StatusEnabled).First(&application).Error; err != nil {
|
||||
|
||||
@@ -40,14 +40,13 @@ type ApprovalDetailSyncTask struct {
|
||||
// CallbackService 校验解密企微回调、记录入站幂等事实并异步拉取详情。
|
||||
type CallbackService struct {
|
||||
applications *ApplicationRepository
|
||||
cipher *CredentialCipher
|
||||
integration callbackIntegrationLog
|
||||
queue *queue.Client
|
||||
}
|
||||
|
||||
// NewCallbackService 创建企业微信审批回调服务。
|
||||
func NewCallbackService(applications *ApplicationRepository, cipher *CredentialCipher, integration callbackIntegrationLog, queueClient *queue.Client) *CallbackService {
|
||||
return &CallbackService{applications: applications, cipher: cipher, integration: integration, queue: queueClient}
|
||||
func NewCallbackService(applications *ApplicationRepository, integration callbackIntegrationLog, queueClient *queue.Client) *CallbackService {
|
||||
return &CallbackService{applications: applications, integration: integration, queue: queueClient}
|
||||
}
|
||||
|
||||
// VerifyURL 校验企微回调 URL 并返回 echostr 明文。
|
||||
@@ -99,24 +98,16 @@ func (s *CallbackService) Receive(ctx context.Context, applicationID uint, signa
|
||||
})
|
||||
}
|
||||
|
||||
// callbackCrypto 每次从数据库密文解出当前回调凭据,使凭据轮换无需重启进程。
|
||||
// callbackCrypto 每次读取数据库当前回调凭据,使凭据更新无需重启进程。
|
||||
func (s *CallbackService) callbackCrypto(ctx context.Context, applicationID uint) (*CallbackCrypto, error) {
|
||||
if s == nil || s.applications == nil || s.cipher == nil || applicationID == 0 {
|
||||
if s == nil || s.applications == nil || applicationID == 0 {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "企业微信审批回调服务未配置")
|
||||
}
|
||||
application, err := s.applications.GetEnabled(ctx, applicationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := s.cipher.Decrypt(application.CallbackTokenCiphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aesKey, err := s.cipher.Decrypt(application.EncodingAESKeyCiphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewCallbackCrypto(token, aesKey, application.CorpID)
|
||||
return NewCallbackCrypto(application.CallbackToken, application.EncodingAESKey, application.CorpID)
|
||||
}
|
||||
|
||||
func applicationCallbackIdempotencyKey(applicationID uint, signature string) string {
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
// Package wecom 提供企业微信外部系统 Adapter。
|
||||
package wecom
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
var credentialAAD = []byte("junhong-wecom-credential-v1")
|
||||
|
||||
// CredentialCipher 使用 AES-256-GCM 加解密企业微信敏感凭据。
|
||||
type CredentialCipher struct {
|
||||
aead cipher.AEAD
|
||||
}
|
||||
|
||||
// NewCredentialCipher 从 32 字节 Base64 密钥创建凭据加密器。
|
||||
func NewCredentialCipher(encodedKey string) (*CredentialCipher, error) {
|
||||
key, err := base64.StdEncoding.DecodeString(encodedKey)
|
||||
if err != nil || len(key) != 32 {
|
||||
return nil, errors.New(errors.CodeWeComCredentialInvalid, "企业微信凭据加密密钥必须是 32 字节 Base64")
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeWeComCredentialInvalid, err, "初始化企业微信凭据加密器失败")
|
||||
}
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeWeComCredentialInvalid, err, "初始化企业微信凭据加密模式失败")
|
||||
}
|
||||
return &CredentialCipher{aead: aead}, nil
|
||||
}
|
||||
|
||||
// Encrypt 加密单个敏感凭据,返回 nonce 与密文组合。
|
||||
func (c *CredentialCipher) Encrypt(plaintext string) ([]byte, error) {
|
||||
if c == nil || c.aead == nil || plaintext == "" {
|
||||
return nil, errors.New(errors.CodeWeComCredentialInvalid)
|
||||
}
|
||||
nonce := make([]byte, c.aead.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "生成企业微信凭据随机数失败")
|
||||
}
|
||||
return c.aead.Seal(nonce, nonce, []byte(plaintext), credentialAAD), nil
|
||||
}
|
||||
|
||||
// Decrypt 解密单个企业微信敏感凭据。
|
||||
func (c *CredentialCipher) Decrypt(ciphertext []byte) (string, error) {
|
||||
if c == nil || c.aead == nil || len(ciphertext) <= c.aead.NonceSize() {
|
||||
return "", errors.New(errors.CodeWeComCredentialInvalid)
|
||||
}
|
||||
nonce := ciphertext[:c.aead.NonceSize()]
|
||||
plaintext, err := c.aead.Open(nil, nonce, ciphertext[c.aead.NonceSize():], credentialAAD)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(errors.CodeWeComCredentialInvalid, err, "解密企业微信凭据失败")
|
||||
}
|
||||
return string(plaintext), nil
|
||||
}
|
||||
@@ -34,11 +34,6 @@ type TokenApplicationRepository interface {
|
||||
MarkConnected(ctx context.Context, applicationID uint, connectedAt time.Time) error
|
||||
}
|
||||
|
||||
// TokenCredentialCipher 定义 access_token Provider 所需的凭据解密边界。
|
||||
type TokenCredentialCipher interface {
|
||||
Decrypt(ciphertext []byte) (string, error)
|
||||
}
|
||||
|
||||
// TokenIntegrationLog 定义企微外呼的 Integration Log 边界。
|
||||
type TokenIntegrationLog interface {
|
||||
Start(ctx context.Context, input integrationlog.Attempt) (*model.IntegrationLog, error)
|
||||
@@ -48,7 +43,6 @@ type TokenIntegrationLog interface {
|
||||
// TokenProvider 按应用缓存企业微信 access_token,并用 Redis 短锁抑制并发回源。
|
||||
type TokenProvider struct {
|
||||
repo TokenApplicationRepository
|
||||
cipher TokenCredentialCipher
|
||||
redis *redis.Client
|
||||
integration TokenIntegrationLog
|
||||
httpClient *http.Client
|
||||
@@ -60,7 +54,6 @@ type TokenProvider struct {
|
||||
// NewTokenProvider 创建企业微信 access_token Provider。
|
||||
func NewTokenProvider(
|
||||
repo TokenApplicationRepository,
|
||||
cipher TokenCredentialCipher,
|
||||
redisClient *redis.Client,
|
||||
integration TokenIntegrationLog,
|
||||
baseURL string,
|
||||
@@ -71,7 +64,7 @@ func NewTokenProvider(
|
||||
timeout = constants.WeComDefaultHTTPTimeout
|
||||
}
|
||||
return &TokenProvider{
|
||||
repo: repo, cipher: cipher, redis: redisClient, integration: integration,
|
||||
repo: repo, redis: redisClient, integration: integration,
|
||||
httpClient: &http.Client{Timeout: timeout}, baseURL: strings.TrimRight(baseURL, "/"),
|
||||
logger: logger, now: time.Now,
|
||||
}
|
||||
@@ -79,7 +72,7 @@ func NewTokenProvider(
|
||||
|
||||
// GetAccessToken 优先读取应用缓存,未命中时串行调用企业微信 Token 接口。
|
||||
func (p *TokenProvider) GetAccessToken(ctx context.Context, applicationID uint) (string, error) {
|
||||
if p == nil || p.repo == nil || p.cipher == nil || p.integration == nil || p.httpClient == nil || applicationID == 0 {
|
||||
if p == nil || p.repo == nil || p.integration == nil || p.httpClient == nil || applicationID == 0 {
|
||||
return "", errors.New(errors.CodeWeComCredentialInvalid)
|
||||
}
|
||||
cacheKey := constants.RedisWeComAccessTokenKey(applicationID)
|
||||
@@ -162,11 +155,7 @@ func (p *TokenProvider) fetchAndCache(ctx context.Context, applicationID uint, c
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
secret, err := p.cipher.Decrypt(application.SecretCiphertext)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
request, err := p.newTokenRequest(ctx, application.CorpID, secret)
|
||||
request, err := p.newTokenRequest(ctx, application.CorpID, application.Secret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user