tokenFactory/service/aliyun_sms.go

164 lines
5.1 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 service
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"os"
"sort"
"strings"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/google/uuid"
)
const aliyunSMSAPIEndpoint = "https://dysmsapi.aliyuncs.com/"
// AliyunSMSConfig 阿里云短信发送配置。
type AliyunSMSConfig struct {
AccessKeyID string
AccessKeySecret string
SignName string
TemplateCode string
}
// LoadAliyunSMSConfig 读取阿里云短信配置(优先系统设置,环境变量兜底)。
func LoadAliyunSMSConfig() (*AliyunSMSConfig, error) {
accessKeyID := strings.TrimSpace(common.SMSAccessKeyID)
if accessKeyID == "" {
accessKeyID = strings.TrimSpace(os.Getenv("ALIYUN_SMS_ACCESS_KEY_ID"))
}
accessKeySecret := strings.TrimSpace(common.SMSAccessKeySecret)
if accessKeySecret == "" {
accessKeySecret = strings.TrimSpace(os.Getenv("ALIYUN_SMS_ACCESS_KEY_SECRET"))
}
signName := strings.TrimSpace(common.SMSCodeSignName)
if signName == "" {
signName = strings.TrimSpace(os.Getenv("ALIYUN_SMS_SIGN_NAME"))
}
templateCode := strings.TrimSpace(common.SMSCodeTemplateCode)
if templateCode == "" {
templateCode = strings.TrimSpace(os.Getenv("ALIYUN_SMS_TEMPLATE_CODE"))
}
cfg := &AliyunSMSConfig{
AccessKeyID: accessKeyID,
AccessKeySecret: accessKeySecret,
SignName: signName,
TemplateCode: templateCode,
}
if cfg.AccessKeyID == "" || cfg.AccessKeySecret == "" {
return nil, fmt.Errorf("短信服务未配置 AccessKey请在系统设置填写“短信API账号/短信API密钥”或设置 ALIYUN_SMS_ACCESS_KEY_ID / ALIYUN_SMS_ACCESS_KEY_SECRET")
}
if cfg.SignName == "" {
return nil, fmt.Errorf("短信服务未配置签名,请在系统设置填写“短信签名”或设置 ALIYUN_SMS_SIGN_NAME")
}
if cfg.TemplateCode == "" {
return nil, fmt.Errorf("短信服务未配置模板请在系统设置填写“短信模板Code”SMSCodeTemplateCode或设置 ALIYUN_SMS_TEMPLATE_CODE")
}
return cfg, nil
}
// SendAliyunSMSCode 通过阿里云短信服务发送验证码短信。
func SendAliyunSMSCode(phone, code string) error {
cfg, err := LoadAliyunSMSConfig()
if err != nil {
return err
}
templateParamBytes, err := common.Marshal(map[string]string{
"code": code,
})
if err != nil {
return fmt.Errorf("构造短信模板参数失败: %w", err)
}
params := map[string]string{
"Action": "SendSms",
"Format": "JSON",
"Version": "2017-05-25",
"AccessKeyId": cfg.AccessKeyID,
"SignatureMethod": "HMAC-SHA1",
"SignatureVersion": "1.0",
"SignatureNonce": uuid.NewString(),
"Timestamp": time.Now().UTC().Format("2006-01-02T15:04:05Z"),
"RegionId": "cn-hangzhou",
"PhoneNumbers": phone,
"SignName": cfg.SignName,
"TemplateCode": cfg.TemplateCode,
"TemplateParam": string(templateParamBytes),
}
signature, err := aliyunSignRPCRequest(params, cfg.AccessKeySecret)
if err != nil {
return err
}
values := url.Values{}
values.Set("Signature", signature)
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
values.Set(k, params[k])
}
reqURL := aliyunSMSAPIEndpoint + "?" + values.Encode()
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
if err != nil {
return fmt.Errorf("构建短信请求失败: %w", err)
}
resp, err := GetHttpClient().Do(req)
if err != nil {
return fmt.Errorf("短信请求失败: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 8192))
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("短信发送失败: HTTP %d %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
var result struct {
Code string `json:"Code"`
Message string `json:"Message"`
}
if err := common.Unmarshal(body, &result); err != nil {
return fmt.Errorf("解析短信服务响应失败: %w", err)
}
if strings.ToUpper(result.Code) != "OK" {
return fmt.Errorf("短信发送失败: %s", strings.TrimSpace(result.Message))
}
return nil
}
// aliyunSignRPCRequest 按阿里云 RPC 协议计算 Signature。
func aliyunSignRPCRequest(params map[string]string, accessKeySecret string) (string, error) {
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
pairs := make([]string, 0, len(keys))
for _, k := range keys {
pairs = append(pairs, aliyunPercentEncode(k)+"="+aliyunPercentEncode(params[k]))
}
canonicalizedQuery := strings.Join(pairs, "&")
stringToSign := "GET&%2F&" + aliyunPercentEncode(canonicalizedQuery)
mac := hmac.New(sha1.New, []byte(accessKeySecret+"&"))
_, _ = mac.Write([]byte(stringToSign))
return base64.StdEncoding.EncodeToString(mac.Sum(nil)), nil
}
// aliyunPercentEncode 采用阿里云要求的 RFC3986 百分号编码规则。
func aliyunPercentEncode(s string) string {
escaped := url.QueryEscape(s)
escaped = strings.ReplaceAll(escaped, "+", "%20")
escaped = strings.ReplaceAll(escaped, "*", "%2A")
escaped = strings.ReplaceAll(escaped, "%7E", "~")
return escaped
}