Files
2026-02-27 14:49:29 +08:00

135 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 加密模式使用的密钥(控制台 ekeyBase64 编码的 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 生成加密场景 IDEncryptedSceneId供前端加密模式初始化验证码使用。
// 算法AES-256-CBC明文 sceneId&timestamp&expireTime密钥为控制台 ekeyBase64 解码后 32 字节)。
// expireTimeSec 有效期为 186400 秒。
func (s *CaptchaService) GetEncryptedSceneId(expireTimeSec int) (string, error) {
if expireTimeSec <= 0 || expireTimeSec > 86400 {
return "", fmt.Errorf("expireTimeSec 必须在 186400 之间")
}
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...)
}