add ali captcha

This commit is contained in:
2026-02-24 16:47:46 +08:00
parent 86bda66271
commit 2d7e241b76
16 changed files with 354 additions and 17 deletions

View File

@@ -51,10 +51,11 @@ type (
type (
QueryServiceReq {
Product string `path:"product"`
Data string `json:"data" validate:"required"`
AgentIdentifier string `json:"agent_identifier,optional"`
App bool `json:"app,optional"`
Product string `path:"product"`
Data string `json:"data" validate:"required"`
AgentIdentifier string `json:"agent_identifier,optional"`
App bool `json:"app,optional"`
CaptchaVerifyParam string `json:"captchaVerifyParam,optional"`
}
QueryServiceResp {
Id string `json:"id"`

View File

@@ -141,8 +141,26 @@ service main {
type (
sendSmsReq {
Mobile string `json:"mobile" validate:"required,mobile"`
ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply realName bindMobile"`
Mobile string `json:"mobile" validate:"required,mobile"`
ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply realName bindMobile"`
CaptchaVerifyParam string `json:"captchaVerifyParam"`
}
)
//============================> captcha v1 <============================
@server (
prefix: api/v1
group: captcha
)
service main {
@doc "get encrypted scene id for aliyun captcha"
@handler getEncryptedSceneId
post /captcha/encryptedSceneId returns (GetEncryptedSceneIdResp)
}
type (
GetEncryptedSceneIdResp {
EncryptedSceneId string `json:"encryptedSceneId"`
}
)

View File

@@ -17,6 +17,12 @@ VerifyCode:
SignName: "天远查"
TemplateCode: "SMS_302641455"
ValidTime: 300
Captcha:
AccessKeyID: "LTAI5tKGB3TVJbMHSoZN3yr9"
AccessKeySecret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
EndpointURL: "captcha.cn-shanghai.aliyuncs.com"
SceneID: "wynt39to"
EKey: "xdhf5JbWVmFXx+2K+6kBk2aH++GtQBEI8Gmzdeen90o="
Encrypt:
SecretKey: "ff83609b2b24fc73196aac3d3dfb874f"
Alipay:

View File

@@ -19,6 +19,16 @@ VerifyCode:
SignName: "天远查"
TemplateCode: "SMS_302641455"
ValidTime: 300
Captcha:
# 建议与短信相同的 AccessKey或单独为验证码创建子账号
AccessKeyID: "LTAI5tKGB3TVJbMHSoZN3yr9"
AccessKeySecret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
# 验证码服务 Endpoint国内一般为 captcha.cn-shanghai.aliyuncs.com
EndpointURL: "captcha.cn-shanghai.aliyuncs.com"
# 阿里云控制台中该场景的 SceneId请替换为真实值
SceneID: "wynt39to"
# 验证码控制台中的 ekey通常为 Base64 字符串),用于生成 EncryptedSceneId
EKey: "xdhf5JbWVmFXx+2K+6kBk2aH++GtQBEI8Gmzdeen90o="
Encrypt:
SecretKey: "ff83609b2b24fc73196aac3d3dfb874f"
WestConfig:

View File

@@ -11,6 +11,7 @@ type Config struct {
CacheRedis cache.CacheConf
JwtAuth JwtAuth // JWT 鉴权相关配置
VerifyCode VerifyCode
Captcha CaptchaConfig
Encrypt Encrypt
Alipay AlipayConfig
Wxpay WxpayConfig
@@ -45,6 +46,13 @@ type VerifyCode struct {
TemplateCode string
ValidTime int
}
type CaptchaConfig struct {
AccessKeyID string
AccessKeySecret string
EndpointURL string
SceneID string
EKey string
}
type Encrypt struct {
SecretKey string
}

View File

@@ -0,0 +1,17 @@
package captcha
import (
"net/http"
"tyc-server/app/main/api/internal/logic/captcha"
"tyc-server/app/main/api/internal/svc"
"tyc-server/common/result"
)
func GetEncryptedSceneIdHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := captcha.NewGetEncryptedSceneIdLogic(r.Context(), svcCtx)
resp, err := l.GetEncryptedSceneId()
result.HttpResult(r, w, resp, err)
}
}

View File

@@ -22,6 +22,7 @@ import (
app "tyc-server/app/main/api/internal/handler/app"
auth "tyc-server/app/main/api/internal/handler/auth"
authorization "tyc-server/app/main/api/internal/handler/authorization"
captcha "tyc-server/app/main/api/internal/handler/captcha"
notification "tyc-server/app/main/api/internal/handler/notification"
pay "tyc-server/app/main/api/internal/handler/pay"
product "tyc-server/app/main/api/internal/handler/product"
@@ -954,6 +955,18 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
rest.WithPrefix("/api/v1"),
)
server.AddRoutes(
[]rest.Route{
{
// get encrypted scene id for aliyun captcha
Method: http.MethodPost,
Path: "/captcha/encryptedSceneId",
Handler: captcha.GetEncryptedSceneIdHandler(serverCtx),
},
},
rest.WithPrefix("/api/v1"),
)
server.AddRoutes(
[]rest.Route{
{

View File

@@ -6,6 +6,7 @@ import (
"math/rand"
"time"
"tyc-server/common/xerr"
"tyc-server/pkg/captcha"
"tyc-server/pkg/lzkit/crypto"
"github.com/pkg/errors"
@@ -35,6 +36,17 @@ func NewSendSmsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendSmsLo
}
func (l *SendSmsLogic) SendSms(req *types.SendSmsReq) error {
// 图形验证码校验(开发环境可跳过)
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
}
secretKey := l.svcCtx.Config.Encrypt.SecretKey
encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey)
if err != nil {

View File

@@ -0,0 +1,38 @@
package captcha
import (
"context"
"tyc-server/app/main/api/internal/svc"
"tyc-server/app/main/api/internal/types"
"tyc-server/pkg/captcha"
"github.com/zeromicro/go-zero/core/logx"
)
type GetEncryptedSceneIdLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetEncryptedSceneIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetEncryptedSceneIdLogic {
return &GetEncryptedSceneIdLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetEncryptedSceneIdLogic) GetEncryptedSceneId() (resp *types.GetEncryptedSceneIdResp, err error) {
cfg := l.svcCtx.Config.Captcha
encrypted, genErr := captcha.GenerateEncryptedSceneID(cfg.SceneID, cfg.EKey, 3600)
if genErr != nil {
// 记录日志,返回通用错误
l.Errorf("generate encrypted scene id error: %+v", genErr)
return nil, genErr
}
return &types.GetEncryptedSceneIdResp{
EncryptedSceneId: encrypted,
}, nil
}

View File

@@ -13,6 +13,7 @@ import (
"tyc-server/common/ctxdata"
"tyc-server/common/globalkey"
"tyc-server/common/xerr"
"tyc-server/pkg/captcha"
"tyc-server/pkg/lzkit/crypto"
"tyc-server/pkg/lzkit/validator"
@@ -109,7 +110,52 @@ var productHandlers = map[string]queryHandlerFunc{
"toc_BankcardBlacklist": runVerifyBankBlackReq,
}
// productHasSmsCode 表示该 product 解密后的请求结构体中是否包含必填短信验证码 Code。
// 有 Code 的产品在「获取验证码」时已经做了滑块,这里不再强制要求 CaptchaVerifyParam。
// 其他产品(无 Code在查询时必须传并校验 CaptchaVerifyParam防止跳过图形验证。
func productHasSmsCode(product string) bool {
switch product {
case "marriage",
"homeservice",
"riskassessment",
"companyinfo",
"rentalinfo",
"preloanbackgroundcheck",
"backgroundcheck",
"personalData",
"toc_PersonalLawsuit",
"toc_EnterpriseLawsuit",
"toc_Marriage",
"toc_PersonalMarriageStatus",
"toc_MarriageStatusRegisterTime",
"toc_MarriageStatusSupplement",
"toc_MarriageStatusVerify",
"toc_DualMarriageStatusRegisterTime",
"toc_VehiclesUnderName",
"toc_VehiclesUnderNamePlate":
return true
default:
return false
}
}
func (l *QueryServiceLogic) PreprocessLogic(req *types.QueryServiceReq, product string) (*types.QueryServiceResp, error) {
// 无短信验证码 Code 的 product查询前必须传并校验滑块否则不允许跳过
if !productHasSmsCode(product) {
if req.CaptchaVerifyParam == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请完成图形验证"), "product %s requires captcha", product)
}
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 nil, err
}
}
decryptData, err := l.DecryptData(req.Data)
if err != nil {
return nil, err

View File

@@ -38,7 +38,7 @@ func (l *MobileCodeLoginLogic) MobileCodeLogin(req *types.MobileCodeLoginReq) (r
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机登录, 加密手机号失败: %+v", err)
}
// 检查验证码(开发环境可跳过)
// 短信验证码校验(开发环境可跳过)
if os.Getenv("ENV") != "development" {
redisKey := fmt.Sprintf("%s:%s", "login", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey)

View File

@@ -1547,6 +1547,10 @@ type GetCommissionResp struct {
List []Commission `json:"list"` // 查询列表
}
type GetEncryptedSceneIdResp struct {
EncryptedSceneId string `json:"encryptedSceneId"`
}
type GetLinkDataReq struct {
LinkIdentifier string `form:"link_identifier"`
}
@@ -2050,10 +2054,11 @@ type QueryRetryResp struct {
}
type QueryServiceReq struct {
Product string `path:"product"`
Data string `json:"data" validate:"required"`
AgentIdentifier string `json:"agent_identifier,optional"`
App bool `json:"app,optional"`
Product string `path:"product"`
Data string `json:"data" validate:"required"`
AgentIdentifier string `json:"agent_identifier,optional"`
App bool `json:"app,optional"`
CaptchaVerifyParam string `json:"captchaVerifyParam,optional"`
}
type QueryServiceResp struct {
@@ -2245,6 +2250,7 @@ type GetAppVersionResp struct {
}
type SendSmsReq struct {
Mobile string `json:"mobile" validate:"required,mobile"`
ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply realName bindMobile"`
Mobile string `json:"mobile" validate:"required,mobile"`
ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply realName bindMobile"`
CaptchaVerifyParam string `json:"captchaVerifyParam"`
}