From c5be114db2cca34bcc8360ff022de4cda4d0bbd4 Mon Sep 17 00:00:00 2001 From: RockYang Date: Tue, 25 Jul 2023 17:00:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E4=BA=BA=E6=9C=BA?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E9=AA=8C=E8=AF=81=20API=20=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E7=9F=AD=E4=BF=A1=E9=98=B2=E5=88=B7?= =?UTF-8?q?=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/config.sample.toml | 13 +- api/core/config.go | 2 +- api/core/types/config.go | 4 +- api/handler/captcha_handler.go | 47 ++++ api/handler/sms_handler.go | 65 +++++ api/handler/user_handler.go | 20 +- api/handler/verify_handler.go | 150 ------------ api/main.go | 25 +- api/service/captcha_service.go | 62 +++++ api/service/function/tou_tiao.go | 4 +- api/service/function/weibo_hot.go | 4 +- api/service/function/zao_bao.go | 4 +- docker/conf/config.toml | 13 +- web/package-lock.json | 1 + web/package.json | 1 + web/src/components/CaptchaPlus.vue | 369 +++++++++++++++++++++++++++++ web/src/components/SendMsg.vue | 121 +++++++--- web/src/views/Register.vue | 13 +- 18 files changed, 705 insertions(+), 213 deletions(-) create mode 100644 api/handler/captcha_handler.go create mode 100644 api/handler/sms_handler.go delete mode 100644 api/handler/verify_handler.go create mode 100644 api/service/captcha_service.go create mode 100644 web/src/components/CaptchaPlus.vue diff --git a/api/config.sample.toml b/api/config.sample.toml index b1337cb..80d99d1 100644 --- a/api/config.sample.toml +++ b/api/config.sample.toml @@ -3,9 +3,9 @@ ProxyURL = "http://172.22.11.200:7777" MysqlDns = "root:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local" StaticDir = "./static" StaticUrl = "http://localhost:5678/static" -AesEncryptKey = "YOUR_AES_KEY" -FunApiToken = "YOUR_FUN_API_TOKEN" +AesEncryptKey = "{YOUR_AES_KEY}" StartWechatBot = false +EnabledMsgService = false [Session] Driver = "cookie" @@ -27,9 +27,14 @@ StartWechatBot = false Port = 6379 Password = "" +[ApiConfig] + ApiURL = "{URL}" + AppId = "{APP_ID}" + Token = "{TOKEN}" + [SmsConfig] - AccessKey = "YOUR_ACCESS_KEY" - AccessSecret = "YOUR_SECRET_KEY" + AccessKey = "{YOUR_ACCESS_KEY}" + AccessSecret = "{YOUR_SECRET_KEY}" Product = "Dysmsapi" Domain = "dysmsapi.aliyuncs.com" diff --git a/api/core/config.go b/api/core/config.go index 83f00f6..2c8a51d 100644 --- a/api/core/config.go +++ b/api/core/config.go @@ -33,7 +33,7 @@ func NewDefaultConfig() *types.AppConfig { HttpOnly: false, SameSite: http.SameSiteLaxMode, }, - Func: types.FunctionApiConfig{}, + ApiConfig: types.ChatPlusApiConfig{}, StartWechatBot: false, } } diff --git a/api/core/types/config.go b/api/core/types/config.go index f3c01c2..045ea75 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -15,7 +15,7 @@ type AppConfig struct { StaticDir string // 静态资源目录 StaticUrl string // 静态资源 URL Redis RedisConfig // redis 连接信息 - Func FunctionApiConfig // function api configs + ApiConfig ChatPlusApiConfig // chatplus api configs AesEncryptKey string SmsConfig AliYunSmsConfig // 短信发送配置 StartWechatBot bool // 是否启动微信机器人 @@ -86,7 +86,7 @@ type SystemConfig struct { UserInitCalls int `json:"user_init_calls"` // 新用户注册默认总送多少次调用 } -type FunctionApiConfig struct { +type ChatPlusApiConfig struct { ApiURL string AppId string Token string diff --git a/api/handler/captcha_handler.go b/api/handler/captcha_handler.go new file mode 100644 index 0000000..e0ffbfe --- /dev/null +++ b/api/handler/captcha_handler.go @@ -0,0 +1,47 @@ +package handler + +import ( + "chatplus/core/types" + "chatplus/service" + "chatplus/utils/resp" + "github.com/gin-gonic/gin" +) + +// 今日头条函数实现 + +type CaptchaHandler struct { + service *service.CaptchaService +} + +func NewCaptchaHandler(s *service.CaptchaService) *CaptchaHandler { + return &CaptchaHandler{service: s} +} + +func (h *CaptchaHandler) Get(c *gin.Context) { + data, err := h.service.Get() + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + resp.SUCCESS(c, data) +} + +// Check verify the captcha data +func (h *CaptchaHandler) Check(c *gin.Context) { + var data struct { + Key string `json:"key"` + Dots string `json:"dots"` + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + if h.service.Check(data) { + resp.SUCCESS(c) + } else { + resp.ERROR(c) + } + +} diff --git a/api/handler/sms_handler.go b/api/handler/sms_handler.go new file mode 100644 index 0000000..c5fe92b --- /dev/null +++ b/api/handler/sms_handler.go @@ -0,0 +1,65 @@ +package handler + +import ( + "chatplus/core" + "chatplus/core/types" + "chatplus/service" + "chatplus/store" + "chatplus/utils" + "chatplus/utils/resp" + "github.com/gin-gonic/gin" +) + +const CodeStorePrefix = "/verify/codes/" + +type SmsHandler struct { + BaseHandler + db *store.LevelDB + sms *service.AliYunSmsService + captcha *service.CaptchaService +} + +func NewSmsHandler(app *core.AppServer, db *store.LevelDB, sms *service.AliYunSmsService, captcha *service.CaptchaService) *SmsHandler { + handler := &SmsHandler{db: db, sms: sms, captcha: captcha} + handler.App = app + return handler +} + +// VerifyCode 发送验证码短信 +func (h *SmsHandler) VerifyCode(c *gin.Context) { + var data struct { + Mobile string `json:"mobile"` + Key string `json:"key"` + Dots string `json:"dots"` + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + if !h.captcha.Check(data) { + resp.ERROR(c, "验证码错误,请先完人机验证") + return + } + + code := utils.RandomNumber(6) + err := h.sms.SendVerifyCode(data.Mobile, code) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + // 存储验证码,等待后面注册验证 + err = h.db.Put(CodeStorePrefix+data.Mobile, code) + if err != nil { + resp.ERROR(c, "验证码保存失败") + return + } + + resp.SUCCESS(c) +} + +// Status check if the message service is enabled +func (h *SmsHandler) Status(c *gin.Context) { + resp.SUCCESS(c, h.App.Config.EnabledMsgService) +} diff --git a/api/handler/user_handler.go b/api/handler/user_handler.go index afc3532..1bdb4cf 100644 --- a/api/handler/user_handler.go +++ b/api/handler/user_handler.go @@ -58,12 +58,14 @@ func (h *UserHandler) Register(c *gin.Context) { // 检查验证码 key := CodeStorePrefix + data.Mobile - var code int - err := h.levelDB.Get(key, &code) - if err != nil || code != data.Code { - logger.Info(code) - resp.ERROR(c, "短信验证码错误") - return + if h.App.Config.EnabledMsgService { + var code int + err := h.levelDB.Get(key, &code) + if err != nil || code != data.Code { + logger.Info(code) + resp.ERROR(c, "短信验证码错误") + return + } } // check if the username is exists @@ -111,7 +113,7 @@ func (h *UserHandler) Register(c *gin.Context) { var cfg model.Config h.db.Where("marker = ?", "system").First(&cfg) var config types.SystemConfig - err = utils.JsonDecode(cfg.Config, &config) + err := utils.JsonDecode(cfg.Config, &config) if err != nil || config.UserInitCalls <= 0 { user.Calls = types.UserInitCalls } else { @@ -124,7 +126,9 @@ func (h *UserHandler) Register(c *gin.Context) { return } - _ = h.levelDB.Delete(key) // 注册成功,删除短信验证码 + if h.App.Config.EnabledMsgService { + _ = h.levelDB.Delete(key) // 注册成功,删除短信验证码 + } resp.SUCCESS(c, user) } diff --git a/api/handler/verify_handler.go b/api/handler/verify_handler.go deleted file mode 100644 index 1154169..0000000 --- a/api/handler/verify_handler.go +++ /dev/null @@ -1,150 +0,0 @@ -package handler - -import ( - "chatplus/core" - "chatplus/core/types" - "chatplus/service" - "chatplus/store" - "chatplus/utils" - "chatplus/utils/resp" - "fmt" - "time" - - "github.com/gin-gonic/gin" -) - -// 短信验证码控制器 - -type VerifyHandler struct { - BaseHandler - sms *service.AliYunSmsService - db *store.LevelDB -} - -const TokenStorePrefix = "/verify/tokens/" -const CodeStorePrefix = "/verify/codes/" -const MobileStatPrefix = "/verify/stats/" - -func NewVerifyHandler(app *core.AppServer, sms *service.AliYunSmsService, db *store.LevelDB) *VerifyHandler { - handler := &VerifyHandler{sms: sms, db: db} - handler.App = app - return handler -} - -type VerifyToken struct { - Token string - Timestamp int64 -} - -// CodeStats 验证码发送统计 -type CodeStats struct { - Mobile string - Count uint - Time int64 -} - -// Token 生成自验证 token -func (h *VerifyHandler) Token(c *gin.Context) { - // 如果不是通过浏览器访问,则返回错误的 token - // TODO: 引入验证码机制防刷机制 - if c.GetHeader("Sec-Fetch-Mode") != "cors" { - token := fmt.Sprintf("%s:%d", utils.RandString(32), time.Now().Unix()) - encrypt, err := utils.AesEncrypt(h.App.Config.AesEncryptKey, []byte(token)) - if err != nil { - resp.ERROR(c, "Token 加密出错") - return - } - resp.SUCCESS(c, encrypt) - return - } - - token := VerifyToken{ - Token: utils.RandString(32), - Timestamp: time.Now().Unix(), - } - json := utils.JsonEncode(token) - encrypt, err := utils.AesEncrypt(h.App.Config.AesEncryptKey, []byte(json)) - if err != nil { - resp.ERROR(c, "Token 加密出错") - return - } - err = h.db.Put(TokenStorePrefix+token.Token, token) - if err != nil { - resp.ERROR(c, "Token 存储失败") - return - } - - resp.SUCCESS(c, encrypt) -} - -// SendMsg 发送验证码短信 -func (h *VerifyHandler) SendMsg(c *gin.Context) { - var data struct { - Mobile string `json:"mobile"` - Token string `json:"token"` - } - if err := c.ShouldBindJSON(&data); err != nil { - resp.ERROR(c, types.InvalidArgs) - return - } - - decrypt, err := utils.AesDecrypt(h.App.Config.AesEncryptKey, data.Token) - if err != nil { - resp.ERROR(c, "Token 解密失败") - return - } - - var token VerifyToken - err = utils.JsonDecode(string(decrypt), &token) - if err != nil { - resp.ERROR(c, "Token 解码失败") - return - } - - if time.Now().Unix()-token.Timestamp > 30 { - resp.ERROR(c, "Token 已过期,请刷新页面重试") - return - } - - // 验证当前手机号发送次数,24 小时内相同手机号只允许发送 2 次 - var stat CodeStats - err = h.db.Get(MobileStatPrefix+data.Mobile, &stat) - if err != nil { - stat = CodeStats{ - Mobile: data.Mobile, - Count: 0, - Time: time.Now().Unix(), - } - } else if stat.Count == 2 { - if time.Now().Unix()-stat.Time > 86400 { - stat.Count = 0 - stat.Time = time.Now().Unix() - } else { - resp.ERROR(c, "触发流量预警,请 24 小时后再操作!") - return - } - } - - code := utils.RandomNumber(6) - err = h.sms.SendVerifyCode(data.Mobile, code) - if err != nil { - resp.ERROR(c, err.Error()) - return - } - - // 每个 token 用完一次立即失效 - _ = h.db.Delete(TokenStorePrefix + token.Token) - // 存储验证码,等待后面注册验证 - err = h.db.Put(CodeStorePrefix+data.Mobile, code) - if err != nil { - resp.ERROR(c, "验证码保存失败") - return - } - - // 更新发送次数 - stat.Count = stat.Count + 1 - _ = h.db.Put(MobileStatPrefix+data.Mobile, stat) - logger.Infof("%+v", stat) - - resp.SUCCESS(c) -} diff --git a/api/main.go b/api/main.go index 8a23e57..595f853 100644 --- a/api/main.go +++ b/api/main.go @@ -117,13 +117,13 @@ func main() { // 创建函数 fx.Provide(func(config *types.AppConfig) (function.FuncZaoBao, error) { - return function.NewZaoBao(config.Func), nil + return function.NewZaoBao(config.ApiConfig), nil }), fx.Provide(func(config *types.AppConfig) (function.FuncWeiboHot, error) { - return function.NewWeiboHot(config.Func), nil + return function.NewWeiboHot(config.ApiConfig), nil }), fx.Provide(func(config *types.AppConfig) (function.FuncHeadlines, error) { - return function.NewHeadLines(config.Func), nil + return function.NewHeadLines(config.ApiConfig), nil }), // 创建控制器 @@ -131,8 +131,9 @@ func main() { fx.Provide(handler.NewUserHandler), fx.Provide(handler.NewChatHandler), fx.Provide(handler.NewUploadHandler), - fx.Provide(handler.NewVerifyHandler), + fx.Provide(handler.NewSmsHandler), fx.Provide(handler.NewRewardHandler), + fx.Provide(handler.NewCaptchaHandler), fx.Provide(admin.NewConfigHandler), fx.Provide(admin.NewAdminHandler), @@ -143,6 +144,9 @@ func main() { // 创建服务 fx.Provide(service.NewAliYunSmsService), + fx.Provide(func(config *types.AppConfig) *service.CaptchaService { + return service.NewCaptchaService(config.ApiConfig) + }), // 注册路由 fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) { @@ -174,10 +178,15 @@ func main() { fx.Invoke(func(s *core.AppServer, h *handler.UploadHandler) { s.Engine.POST("/api/upload", h.Upload) }), - fx.Invoke(func(s *core.AppServer, h *handler.VerifyHandler) { - group := s.Engine.Group("/api/verify/") - group.GET("token", h.Token) - group.POST("sms", h.SendMsg) + fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) { + group := s.Engine.Group("/api/sms/") + group.GET("status", h.Status) + group.POST("code", h.VerifyCode) + }), + fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) { + group := s.Engine.Group("/api/captcha/") + group.GET("get", h.Get) + group.POST("check", h.Check) }), fx.Invoke(func(s *core.AppServer, h *handler.RewardHandler) { group := s.Engine.Group("/api/reward/") diff --git a/api/service/captcha_service.go b/api/service/captcha_service.go new file mode 100644 index 0000000..c4b784a --- /dev/null +++ b/api/service/captcha_service.go @@ -0,0 +1,62 @@ +package service + +import ( + "chatplus/core/types" + "errors" + "fmt" + "github.com/imroc/req/v3" + "time" +) + +type CaptchaService struct { + config types.ChatPlusApiConfig + client *req.Client +} + +func NewCaptchaService(config types.ChatPlusApiConfig) *CaptchaService { + return &CaptchaService{ + config: config, + client: req.C().SetTimeout(10 * time.Second), + } +} + +func (s *CaptchaService) Get() (interface{}, error) { + if s.config.Token == "" { + return nil, errors.New("无效的 API Token") + } + + url := fmt.Sprintf("%s/api/captcha/get", s.config.ApiURL) + var res types.BizVo + r, err := s.client.R(). + SetHeader("AppId", s.config.AppId). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)). + SetSuccessResult(&res).Get(url) + if err != nil || r.IsErrorState() { + return nil, fmt.Errorf("请求 API 失败:%v", err) + } + + if res.Code != types.Success { + return nil, fmt.Errorf("请求 API 失败:%s", res.Message) + } + + return res.Data, nil +} + +func (s *CaptchaService) Check(data interface{}) bool { + url := fmt.Sprintf("%s/api/captcha/check", s.config.ApiURL) + var res types.BizVo + r, err := s.client.R(). + SetHeader("AppId", s.config.AppId). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)). + SetBodyJsonMarshal(data). + SetSuccessResult(&res).Post(url) + if err != nil || r.IsErrorState() { + return false + } + + if res.Code != types.Success { + return false + } + + return true +} diff --git a/api/service/function/tou_tiao.go b/api/service/function/tou_tiao.go index 54638da..c0092d0 100644 --- a/api/service/function/tou_tiao.go +++ b/api/service/function/tou_tiao.go @@ -13,11 +13,11 @@ import ( type FuncHeadlines struct { name string - config types.FunctionApiConfig + config types.ChatPlusApiConfig client *req.Client } -func NewHeadLines(config types.FunctionApiConfig) FuncHeadlines { +func NewHeadLines(config types.ChatPlusApiConfig) FuncHeadlines { return FuncHeadlines{ name: "今日头条", config: config, diff --git a/api/service/function/weibo_hot.go b/api/service/function/weibo_hot.go index a7870be..f8d830a 100644 --- a/api/service/function/weibo_hot.go +++ b/api/service/function/weibo_hot.go @@ -13,11 +13,11 @@ import ( type FuncWeiboHot struct { name string - config types.FunctionApiConfig + config types.ChatPlusApiConfig client *req.Client } -func NewWeiboHot(config types.FunctionApiConfig) FuncWeiboHot { +func NewWeiboHot(config types.ChatPlusApiConfig) FuncWeiboHot { return FuncWeiboHot{ name: "微博热搜", config: config, diff --git a/api/service/function/zao_bao.go b/api/service/function/zao_bao.go index c04e57f..7218dd7 100644 --- a/api/service/function/zao_bao.go +++ b/api/service/function/zao_bao.go @@ -13,11 +13,11 @@ import ( type FuncZaoBao struct { name string - config types.FunctionApiConfig + config types.ChatPlusApiConfig client *req.Client } -func NewZaoBao(config types.FunctionApiConfig) FuncZaoBao { +func NewZaoBao(config types.ChatPlusApiConfig) FuncZaoBao { return FuncZaoBao{ name: "每日早报", config: config, diff --git a/docker/conf/config.toml b/docker/conf/config.toml index b1337cb..80d99d1 100644 --- a/docker/conf/config.toml +++ b/docker/conf/config.toml @@ -3,9 +3,9 @@ ProxyURL = "http://172.22.11.200:7777" MysqlDns = "root:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local" StaticDir = "./static" StaticUrl = "http://localhost:5678/static" -AesEncryptKey = "YOUR_AES_KEY" -FunApiToken = "YOUR_FUN_API_TOKEN" +AesEncryptKey = "{YOUR_AES_KEY}" StartWechatBot = false +EnabledMsgService = false [Session] Driver = "cookie" @@ -27,9 +27,14 @@ StartWechatBot = false Port = 6379 Password = "" +[ApiConfig] + ApiURL = "{URL}" + AppId = "{APP_ID}" + Token = "{TOKEN}" + [SmsConfig] - AccessKey = "YOUR_ACCESS_KEY" - AccessSecret = "YOUR_SECRET_KEY" + AccessKey = "{YOUR_ACCESS_KEY}" + AccessSecret = "{YOUR_SECRET_KEY}" Product = "Dysmsapi" Domain = "dysmsapi.aliyuncs.com" diff --git a/web/package-lock.json b/web/package-lock.json index 7ddbdee..e03388d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -17,6 +17,7 @@ "good-storage": "^1.1.1", "highlight.js": "^11.7.0", "json-bigint": "^1.0.0", + "lodash": "^4.17.21", "markdown-it": "^13.0.1", "md-editor-v3": "^2.2.1", "pinia": "^2.1.4", diff --git a/web/package.json b/web/package.json index 898238e..7974766 100644 --- a/web/package.json +++ b/web/package.json @@ -24,6 +24,7 @@ "sortablejs": "^1.15.0", "vant": "^4.5.0", "vue": "^3.2.13", + "lodash": "^4.17.21", "vue-router": "^4.0.15" }, "devDependencies": { diff --git a/web/src/components/CaptchaPlus.vue b/web/src/components/CaptchaPlus.vue new file mode 100644 index 0000000..f908479 --- /dev/null +++ b/web/src/components/CaptchaPlus.vue @@ -0,0 +1,369 @@ + + + + + diff --git a/web/src/components/SendMsg.vue b/web/src/components/SendMsg.vue index 7abb5b8..092ccc6 100644 --- a/web/src/components/SendMsg.vue +++ b/web/src/components/SendMsg.vue @@ -1,16 +1,40 @@