f
This commit is contained in:
@@ -23,8 +23,25 @@ func SendSmsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
result.ParamValidateErrorResult(r, w, err)
|
||||
return
|
||||
}
|
||||
// 获取客户端真实 IP
|
||||
clientIP := getClientIP(r)
|
||||
l := auth.NewSendSmsLogic(r.Context(), svcCtx)
|
||||
err := l.SendSms(&req)
|
||||
err := l.SendSms(&req, clientIP)
|
||||
result.HttpResult(r, w, nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
// getClientIP 获取客户端真实 IP
|
||||
func getClientIP(r *http.Request) string {
|
||||
// 优先从 X-Forwarded-For 获取
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
// X-Forwarded-For 可能包含多个 IP,取第一个
|
||||
return xff
|
||||
}
|
||||
// 其次从 X-Real-IP 获取
|
||||
if xri := r.Header.Get("X-Real-IP"); xri != "" {
|
||||
return xri
|
||||
}
|
||||
// 最后使用 RemoteAddr
|
||||
return r.RemoteAddr
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"qnc-server/common/xerr"
|
||||
"qnc-server/pkg/captcha"
|
||||
"qnc-server/pkg/lzkit/crypto"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -35,24 +35,58 @@ func NewSendSmsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendSmsLo
|
||||
}
|
||||
}
|
||||
|
||||
func (l *SendSmsLogic) SendSms(req *types.SendSmsReq) error {
|
||||
// 1. 图形验证码校验
|
||||
cfg := l.svcCtx.Config.Captcha
|
||||
if err := captcha.Verify(captcha.Config{
|
||||
AccessKeyID: cfg.AccessKeyID,
|
||||
AccessKeySecret: cfg.AccessKeySecret,
|
||||
EndpointURL: cfg.EndpointURL,
|
||||
SceneID: cfg.SceneID,
|
||||
}, req.CaptchaVerifyParam); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *SendSmsLogic) SendSms(req *types.SendSmsReq, clientIP string) error {
|
||||
secretKey := l.svcCtx.Config.Encrypt.SecretKey
|
||||
encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey)
|
||||
if err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 加密手机号失败: %v", err)
|
||||
}
|
||||
// 检查手机号是否在一分钟内已发送过验证码
|
||||
|
||||
// 1. 滑块验证码校验(可选)
|
||||
cfg := l.svcCtx.Config.Captcha
|
||||
captchaResult := captcha.VerifyOptional(captcha.Config{
|
||||
AccessKeyID: cfg.AccessKeyID,
|
||||
AccessKeySecret: cfg.AccessKeySecret,
|
||||
EndpointURL: cfg.EndpointURL,
|
||||
SceneID: cfg.SceneID,
|
||||
}, req.CaptchaVerifyParam)
|
||||
|
||||
if captchaResult.VerifyErr != nil {
|
||||
return captchaResult.VerifyErr
|
||||
}
|
||||
|
||||
// 2. 防刷策略
|
||||
if captchaResult.Skipped {
|
||||
// 没有滑块验证码,使用更严格的限流策略
|
||||
// 2.1 IP 限流:同一 IP 每小时最多发送 10 次
|
||||
ipLimitKey := fmt.Sprintf("ip_limit:%s", clientIP)
|
||||
ipCount, err := l.svcCtx.Redis.Incr(ipLimitKey)
|
||||
if err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 读取IP限流缓存失败: %v", err)
|
||||
}
|
||||
if ipCount == 1 {
|
||||
// 第一次访问,设置 1 小时过期
|
||||
l.svcCtx.Redis.Expire(ipLimitKey, 3600)
|
||||
}
|
||||
if ipCount > 10 {
|
||||
return errors.Wrapf(xerr.NewErrMsg("请求过于频繁,请稍后再试"), "短信发送, IP限流: %s, count: %d", clientIP, ipCount)
|
||||
}
|
||||
|
||||
// 2.2 手机号限流:同一手机号每小时最多发送 5 次(无滑块时更严格)
|
||||
hourLimitKey := fmt.Sprintf("hour_limit:%s:%s", req.ActionType, encryptedMobile)
|
||||
hourCount, err := l.svcCtx.Redis.Incr(hourLimitKey)
|
||||
if err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 读取小时限流缓存失败: %v", err)
|
||||
}
|
||||
if hourCount == 1 {
|
||||
l.svcCtx.Redis.Expire(hourLimitKey, 3600)
|
||||
}
|
||||
if hourCount > 5 {
|
||||
return errors.Wrapf(xerr.NewErrMsg("该手机号请求过于频繁,请稍后再试"), "短信发送, 手机号小时限流: %s, count: %d", encryptedMobile, hourCount)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查手机号是否在一分钟内已发送过验证码(通用)
|
||||
limitCodeKey := fmt.Sprintf("limit:%s:%s", req.ActionType, encryptedMobile)
|
||||
exists, err := l.svcCtx.Redis.Exists(limitCodeKey)
|
||||
if err != nil {
|
||||
@@ -60,7 +94,6 @@ func (l *SendSmsLogic) SendSms(req *types.SendSmsReq) error {
|
||||
}
|
||||
|
||||
if exists {
|
||||
// 如果 Redis 中已经存在标记,说明在 1 分钟内请求过,返回错误
|
||||
return errors.Wrapf(xerr.NewErrMsg("一分钟内不能重复发送验证码"), "短信发送, 手机号1分钟内重复请求发送验证码: %s", encryptedMobile)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,15 +19,24 @@ type Config struct {
|
||||
SceneID string
|
||||
}
|
||||
|
||||
// Verify 验证阿里云验证码
|
||||
func Verify(cfg Config, captchaVerifyParam string) error {
|
||||
// VerifyResult 验证结果
|
||||
type VerifyResult struct {
|
||||
Verified bool // 是否通过滑块验证
|
||||
Skipped bool // 是否跳过验证(无滑块参数)
|
||||
VerifyErr error // 验证错误(如果有)
|
||||
}
|
||||
|
||||
// VerifyOptional 可选验证阿里云验证码
|
||||
// 当 captchaVerifyParam 为空时返回 Skipped=true,由调用方决定后续处理
|
||||
func VerifyOptional(cfg Config, captchaVerifyParam string) VerifyResult {
|
||||
// 开发环境可跳过验证
|
||||
if os.Getenv("ENV") == "development" {
|
||||
return nil
|
||||
return VerifyResult{Verified: true, Skipped: false}
|
||||
}
|
||||
|
||||
// 没有滑块验证码参数,返回跳过状态
|
||||
if captchaVerifyParam == "" {
|
||||
return errors.Wrapf(xerr.NewErrMsg("图形验证码校验失败"), "empty captchaVerifyParam")
|
||||
return VerifyResult{Skipped: true}
|
||||
}
|
||||
|
||||
clientCfg := &openapi.Config{
|
||||
@@ -37,7 +46,7 @@ func Verify(cfg Config, captchaVerifyParam string) error {
|
||||
clientCfg.Endpoint = tea.String(cfg.EndpointURL)
|
||||
client, err := captcha20230305.NewClient(clientCfg)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "create aliyun captcha client error")
|
||||
return VerifyResult{VerifyErr: errors.Wrapf(err, "create aliyun captcha client error")}
|
||||
}
|
||||
|
||||
req := &captcha20230305.VerifyIntelligentCaptchaRequest{
|
||||
@@ -47,13 +56,27 @@ func Verify(cfg Config, captchaVerifyParam string) error {
|
||||
|
||||
resp, err := client.VerifyIntelligentCaptcha(req)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "verify aliyun captcha error")
|
||||
return VerifyResult{VerifyErr: errors.Wrapf(err, "verify aliyun captcha error")}
|
||||
}
|
||||
|
||||
if tea.BoolValue(resp.Body.Result.VerifyResult) {
|
||||
return nil
|
||||
return VerifyResult{Verified: true}
|
||||
}
|
||||
|
||||
return errors.Wrapf(xerr.NewErrMsg("图形验证码校验失败"), "aliyun captcha verify failed: code=%s, msg=%s",
|
||||
tea.StringValue(resp.Body.Code), tea.StringValue(resp.Body.Message))
|
||||
return VerifyResult{
|
||||
VerifyErr: errors.Wrapf(xerr.NewErrMsg("图形验证码校验失败"), "aliyun captcha verify failed: code=%s, msg=%s",
|
||||
tea.StringValue(resp.Body.Code), tea.StringValue(resp.Body.Message)),
|
||||
}
|
||||
}
|
||||
|
||||
// Verify 验证阿里云验证码(必须提供验证码参数)
|
||||
func Verify(cfg Config, captchaVerifyParam string) error {
|
||||
result := VerifyOptional(cfg, captchaVerifyParam)
|
||||
if result.VerifyErr != nil {
|
||||
return result.VerifyErr
|
||||
}
|
||||
if result.Skipped {
|
||||
return errors.Wrapf(xerr.NewErrMsg("图形验证码校验失败"), "empty captchaVerifyParam")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user