package captcha import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "errors" "fmt" "io" "time" "github.com/alibabacloud-go/tea/tea" captcha20230305 "github.com/alibabacloud-go/captcha-20230305/client" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" ) var ( ErrCaptchaVerifyFailed = errors.New("图形验证码校验失败") ErrCaptchaConfig = errors.New("验证码配置错误") ErrCaptchaEncryptMissing = errors.New("加密模式需要配置 EncryptKey(控制台 ekey)") ) // CaptchaConfig 阿里云验证码配置 type CaptchaConfig struct { AccessKeyID string AccessKeySecret string EndpointURL string SceneID string // EncryptKey 加密模式使用的密钥(控制台 ekey,Base64 编码的 32 字节),用于生成 EncryptedSceneId EncryptKey string } // CaptchaService 阿里云验证码服务 type CaptchaService struct { config CaptchaConfig } // NewCaptchaService 创建验证码服务实例 func NewCaptchaService(config CaptchaConfig) *CaptchaService { return &CaptchaService{ config: config, } } // Verify 验证滑块验证码 func (s *CaptchaService) Verify(captchaVerifyParam string) error { if captchaVerifyParam == "" { return ErrCaptchaVerifyFailed } if s.config.AccessKeyID == "" || s.config.AccessKeySecret == "" { return ErrCaptchaConfig } clientCfg := &openapi.Config{ AccessKeyId: tea.String(s.config.AccessKeyID), AccessKeySecret: tea.String(s.config.AccessKeySecret), } clientCfg.Endpoint = tea.String(s.config.EndpointURL) client, err := captcha20230305.NewClient(clientCfg) if err != nil { return errors.Join(ErrCaptchaConfig, err) } req := &captcha20230305.VerifyIntelligentCaptchaRequest{ SceneId: tea.String(s.config.SceneID), CaptchaVerifyParam: tea.String(captchaVerifyParam), } resp, err := client.VerifyIntelligentCaptcha(req) if err != nil { return errors.Join(ErrCaptchaVerifyFailed, err) } if resp.Body == nil || !tea.BoolValue(resp.Body.Result.VerifyResult) { return ErrCaptchaVerifyFailed } return nil } // GetEncryptedSceneId 生成加密场景 ID(EncryptedSceneId),供前端加密模式初始化验证码使用。 // 算法:AES-256-CBC,明文 sceneId×tamp&expireTime,密钥为控制台 ekey(Base64 解码后 32 字节)。 // expireTimeSec 有效期为 1~86400 秒。 func (s *CaptchaService) GetEncryptedSceneId(expireTimeSec int) (string, error) { if expireTimeSec <= 0 || expireTimeSec > 86400 { return "", fmt.Errorf("expireTimeSec 必须在 1~86400 之间") } if s.config.EncryptKey == "" { return "", ErrCaptchaEncryptMissing } if s.config.SceneID == "" { return "", ErrCaptchaConfig } keyBytes, err := base64.StdEncoding.DecodeString(s.config.EncryptKey) if err != nil || len(keyBytes) != 32 { return "", errors.Join(ErrCaptchaConfig, fmt.Errorf("EncryptKey 必须为 Base64 编码的 32 字节")) } plaintext := fmt.Sprintf("%s&%d&%d", s.config.SceneID, time.Now().Unix(), expireTimeSec) plainBytes := []byte(plaintext) plainBytes = pkcs7Pad(plainBytes, aes.BlockSize) block, err := aes.NewCipher(keyBytes) if err != nil { return "", errors.Join(ErrCaptchaConfig, err) } iv := make([]byte, aes.BlockSize) if _, err := io.ReadFull(rand.Reader, iv); err != nil { return "", err } mode := cipher.NewCBCEncrypter(block, iv) ciphertext := make([]byte, len(plainBytes)) mode.CryptBlocks(ciphertext, plainBytes) result := make([]byte, len(iv)+len(ciphertext)) copy(result, iv) copy(result[len(iv):], ciphertext) return base64.StdEncoding.EncodeToString(result), nil } func pkcs7Pad(data []byte, blockSize int) []byte { n := blockSize - (len(data) % blockSize) pad := make([]byte, n) for i := range pad { pad[i] = byte(n) } return append(data, pad...) }