From 48c5aeb0f409f72b6f7c4eba2a08684007d6fb54 Mon Sep 17 00:00:00 2001 From: landaiqing <3517283258@qq.com> Date: Mon, 12 Aug 2024 22:05:59 +0800 Subject: [PATCH] :sparkles: add jwt / complete the rotation verification --- .caches/master.jpg | Bin 58719 -> 0 bytes .caches/thumb.png | Bin 3108 -> 0 bytes .gitignore | 1 + api/api.go | 4 +- api/captcha_api/captcha_api.go | 353 ++++++++++++++++++++++++- api/captcha_api/model/request_model.go | 6 + api/sms_api/sms.go | 3 + api/sms_api/sms_api.go | 80 ++++++ cmd/gen/gen.go | 2 +- common/result/error_code.go | 2 + common/result/result.go | 7 +- config/conf_jwt.go | 10 + config/conf_sms.go | 18 ++ config/config.go | 2 + core/captcha.go | 142 +++++++++- docs/docs.go | 335 +++++++++++++++++++++++ docs/swagger.json | 335 +++++++++++++++++++++++ docs/swagger.yaml | 226 ++++++++++++++++ global/global.go | 17 +- go.mod | 3 + go.sum | 6 + i18n/language/en.toml | 5 +- i18n/language/zh.toml | 5 +- main.go | 12 +- middleware/i18n.go | 2 +- router/modules/captcha_router.go | 14 + router/modules/sms_router.go | 14 + router/router.go | 6 +- utils/cache.go | 67 +++++ utils/genValidateCode.go | 20 ++ utils/jwt.go | 49 ++++ 31 files changed, 1702 insertions(+), 44 deletions(-) delete mode 100644 .caches/master.jpg delete mode 100644 .caches/thumb.png create mode 100644 api/captcha_api/model/request_model.go create mode 100644 api/sms_api/sms.go create mode 100644 api/sms_api/sms_api.go create mode 100644 config/conf_jwt.go create mode 100644 config/conf_sms.go create mode 100644 router/modules/captcha_router.go create mode 100644 router/modules/sms_router.go create mode 100644 utils/cache.go create mode 100644 utils/genValidateCode.go create mode 100644 utils/jwt.go diff --git a/.caches/master.jpg b/.caches/master.jpg deleted file mode 100644 index 891c664beaa76fae4c5309ec5078a6decb55e1cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58719 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 generate err") + result.FailWithNull(c) + return } + var masterImageBase64, thumbImageBase64 string + masterImageBase64 = captData.GetMasterImage().ToBase64() + thumbImageBase64 = captData.GetThumbImage().ToBase64() - dots, _ := json.Marshal(dotData) - fmt.Println(">>>>> ", string(dots)) - - err = captData.GetMasterImage().SaveToFile("./.caches/master.jpg", option.QualityNone) + dotsByte, err := json.Marshal(dotData) if err != nil { - fmt.Println(err) + result.FailWithNull(c) + return } - err = captData.GetThumbImage().SaveToFile("./.caches/thumb.png") + key := helper.StringToMD5(string(dotsByte)) + err = redis.Set(key, dotsByte, time.Minute).Err() if err != nil { - fmt.Println(err) + result.FailWithNull(c) + return } + bt := map[string]interface{}{ + "key": key, + "image": masterImageBase64, + "thumb": thumbImageBase64, + } + result.OkWithData(bt, c) +} + +// GenerateSlideBasicCaptData 验证点击形状验证码 +// @Summary 验证点击形状验证码 +// @Description 验证点击形状验证码 +// @Tags 点击形状验证码 +// @Success 200 {string} json +// @Router /api/captcha/shape/check [get] +func (CaptchaAPI) GenerateSlideBasicCaptData(c *gin.Context) { + captData, err := global.SlideCaptcha.Generate() + if err != nil { + global.LOG.Fatalln(err) + } + blockData := captData.GetData() + if blockData == nil { + result.FailWithNull(c) + return + } + var masterImageBase64, tileImageBase64 string + masterImageBase64 = captData.GetMasterImage().ToBase64() + + tileImageBase64 = captData.GetTileImage().ToBase64() + + dotsByte, err := json.Marshal(blockData) + if err != nil { + result.FailWithNull(c) + return + } + key := helper.StringToMD5(string(dotsByte)) + err = redis.Set(key, dotsByte, time.Minute).Err() + if err != nil { + result.FailWithNull(c) + return + } + bt := map[string]interface{}{ + "key": key, + "image": masterImageBase64, + "tile": tileImageBase64, + "tile_width": blockData.Width, + "tile_height": blockData.Height, + "tile_x": blockData.TileX, + "tile_y": blockData.TileY, + } + result.OkWithData(bt, c) +} + +// CheckSlideData 验证点击形状验证码 +// @Summary 验证点击形状验证码 +// @Description 验证点击形状验证码 +// @Tags 点击形状验证码 +// @Param point query string true "点击坐标" +// @Param key query string true "验证码key" +// @Success 200 {string} json +// @Router /api/captcha/shape/slide/check [get] +func (CaptchaAPI) CheckSlideData(c *gin.Context) { + point := c.Query("point") + key := c.Query("key") + if point == "" || key == "" { + result.FailWithNull(c) + return + } + + cacheDataByte, err := redis.Get(key).Bytes() + if len(cacheDataByte) == 0 || err != nil { + result.FailWithNull(c) + return + } + src := strings.Split(point, ",") + + var dct *slide.Block + if err := json.Unmarshal(cacheDataByte, &dct); err != nil { + result.FailWithNull(c) + return + } + + chkRet := false + if 2 == len(src) { + sx, _ := strconv.ParseFloat(fmt.Sprintf("%v", src[0]), 64) + sy, _ := strconv.ParseFloat(fmt.Sprintf("%v", src[1]), 64) + chkRet = slide.CheckPoint(int64(sx), int64(sy), int64(dct.X), int64(dct.Y), 4) + } + + if chkRet { + result.OkWithMessage("success", c) + return + } + result.FailWithMessage("fail", c) +} + +// GenerateSlideRegionCaptData 验证点击形状验证码 +// @Summary 验证点击形状验证码 +// @Description 验证点击形状验证码 +// @Tags 点击形状验证码 +// @Success 200 {string} json +// @Router /api/captcha/shape/slide/region/get [get] +func (CaptchaAPI) GenerateSlideRegionCaptData(c *gin.Context) { + captData, err := global.SlideRegionCaptcha.Generate() + if err != nil { + global.LOG.Fatalln(err) + } + + blockData := captData.GetData() + if blockData == nil { + result.FailWithNull(c) + return + } + + var masterImageBase64, tileImageBase64 string + masterImageBase64 = captData.GetMasterImage().ToBase64() + tileImageBase64 = captData.GetTileImage().ToBase64() + + blockByte, err := json.Marshal(blockData) + if err != nil { + result.FailWithNull(c) + return + } + key := helper.StringToMD5(string(blockByte)) + err = redis.Set(key, blockByte, time.Minute).Err() + if err != nil { + result.FailWithNull(c) + return + } + bt := map[string]interface{}{ + "code": 0, + "key": key, + "image": masterImageBase64, + "tile": tileImageBase64, + "tile_width": blockData.Width, + "tile_height": blockData.Height, + "tile_x": blockData.TileX, + "tile_y": blockData.TileY, + } + result.OkWithData(bt, c) } diff --git a/api/captcha_api/model/request_model.go b/api/captcha_api/model/request_model.go new file mode 100644 index 0000000..423351f --- /dev/null +++ b/api/captcha_api/model/request_model.go @@ -0,0 +1,6 @@ +package model + +type RotateCaptchaRequest struct { + Angle int `json:"angle"` + Key string `json:"key"` +} diff --git a/api/sms_api/sms.go b/api/sms_api/sms.go new file mode 100644 index 0000000..56a8f1e --- /dev/null +++ b/api/sms_api/sms.go @@ -0,0 +1,3 @@ +package sms_api + +type SmsAPI struct{} diff --git a/api/sms_api/sms_api.go b/api/sms_api/sms_api.go new file mode 100644 index 0000000..a4c2c8b --- /dev/null +++ b/api/sms_api/sms_api.go @@ -0,0 +1,80 @@ +package sms_api + +import ( + ginI18n "github.com/gin-contrib/i18n" + "github.com/gin-gonic/gin" + gosms "github.com/pkg6/go-sms" + "github.com/pkg6/go-sms/gateways" + "github.com/pkg6/go-sms/gateways/aliyun" + "github.com/pkg6/go-sms/gateways/smsbao" + "schisandra-cloud-album/common/result" + "schisandra-cloud-album/global" + "schisandra-cloud-album/utils" +) + +// SendMessageByAli 发送短信验证码 +// @Summary 发送短信验证码 +// @Description 发送短信验证码 +// @Tags 短信验证码 +// @Produce json +// @Param phone query string true "手机号" +// @Router /api/sms/ali/send [get] +func (SmsAPI) SendMessageByAli(c *gin.Context) { + phone := c.Query("phone") + if phone == "" { + result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneNotEmpty"), c) + return + } + sms := gosms.NewParser(gateways.Gateways{ + ALiYun: aliyun.ALiYun{ + Host: global.CONFIG.SMS.Ali.Host, + AccessKeyId: global.CONFIG.SMS.Ali.AccessKeyID, + AccessKeySecret: global.CONFIG.SMS.Ali.AccessKeySecret, + }, + }) + code := utils.GenValidateCode(6) + _, err := sms.Send(phone, gosms.MapStringAny{ + "content": "您的验证码是:****。请不要把验证码泄露给其他人。", + "template": global.CONFIG.SMS.Ali.TemplateID, + //"signName": global.CONFIG.SMS.Ali.Signature, + "data": gosms.MapStrings{ + "code": code, + }, + }, nil) + if err != nil { + global.LOG.Error(err) + result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendFailed"), c) + return + } +} + +// SendMessageBySmsBao 短信宝发送短信验证码 +// @Summary 短信宝发送短信验证码 +// @Description 发送短信验证码 +// @Tags 短信验证码 +// @Produce json +// @Param phone query string true "手机号" +// @Router /api/sms/smsbao/send [get] +func (SmsAPI) SendMessageBySmsBao(c *gin.Context) { + phone := c.Query("phone") + if phone == "" { + result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneNotEmpty"), c) + return + } + sms := gosms.NewParser(gateways.Gateways{ + SmsBao: smsbao.SmsBao{ + User: global.CONFIG.SMS.SmsBao.User, + Password: global.CONFIG.SMS.SmsBao.Password, + }, + }) + code := utils.GenValidateCode(6) + _, err := sms.Send(phone, gosms.MapStringAny{ + "content": "您的验证码是:" + code + "。请不要把验证码泄露给其他人。", + }, nil) + if err != nil { + global.LOG.Error(err) + result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendFailed"), c) + return + } + result.OkWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendSuccess"), c) +} diff --git a/cmd/gen/gen.go b/cmd/gen/gen.go index ee48771..62699ea 100644 --- a/cmd/gen/gen.go +++ b/cmd/gen/gen.go @@ -54,7 +54,7 @@ func initInfo() (db *gorm.DB, g *gen.Generator, fieldOpts []gen.ModelOpt) { // WithDefaultQuery 生成默认查询结构体(作为全局变量使用), 即`Q`结构体和其字段(各表模型) // WithoutContext 生成没有context调用限制的代码供查询 // WithQueryInterface 生成interface形式的查询代码(可导出), 如`Where()`方法返回的就是一个可导出的接口类型 - Mode: gen.WithDefaultQuery | gen.WithQueryInterface, + Mode: gen.WithDefaultQuery | gen.WithoutContext, // 表字段可为 null 值时, 对应结体字段使用指针类型 FieldNullable: true, // generate pointer when field is nullable diff --git a/common/result/error_code.go b/common/result/error_code.go index c0cead4..ef5a210 100644 --- a/common/result/error_code.go +++ b/common/result/error_code.go @@ -13,6 +13,7 @@ const ( ParamsMatchError ErrorCode = 1008 ParamsNotUniqueError ErrorCode = 1009 FileSizeExceeded ErrorCode = 1010 + CaptchaExpireError ErrorCode = 1011 ) type ErrorMap map[ErrorCode]string @@ -28,4 +29,5 @@ var ErrMap = ErrorMap{ 1008: "参数值不匹配", 1009: "参数值不唯一", 1010: "超出文件上传大小限制", + 1011: "验证码已过期", } diff --git a/common/result/result.go b/common/result/result.go index e4d1a3b..191f512 100644 --- a/common/result/result.go +++ b/common/result/result.go @@ -38,12 +38,17 @@ func OkWithMessage(msg string, c *gin.Context) { func Fail(msg string, data any, c *gin.Context) { Result(FAIL, msg, data, false, c) } +func FailWithCodeAndMessage(code int, msg string, c *gin.Context) { + Result(code, msg, nil, false, c) +} func FailWithMessage(msg string, c *gin.Context) { Result(FAIL, msg, nil, false, c) } func FailWithData(data any, c *gin.Context) { Result(FAIL, "fail", data, false, c) - +} +func FailWithNull(c *gin.Context) { + Result(FAIL, "fail", nil, false, c) } func FailWithCode(code ErrorCode, c *gin.Context) { msg, ok := ErrMap[code] diff --git a/config/conf_jwt.go b/config/conf_jwt.go new file mode 100644 index 0000000..d381e3e --- /dev/null +++ b/config/conf_jwt.go @@ -0,0 +1,10 @@ +package config + +type JWT struct { + Secret string `yaml:"secret"` + Expiration string `yaml:"expiration"` + RefreshExpiration string `yaml:"refresh-expiration"` + RefreshTokenKey string `yaml:"refresh-token-key"` + HeaderKey string `yaml:"header-key"` + HeaderPrefix string `yaml:"header-prefix"` +} diff --git a/config/conf_sms.go b/config/conf_sms.go new file mode 100644 index 0000000..494c2b4 --- /dev/null +++ b/config/conf_sms.go @@ -0,0 +1,18 @@ +package config + +type SMS struct { + Ali Ali `yaml:"ali"` //阿里云短信配置 + SmsBao SmsBao `yaml:"sms-bao"` //短信宝配置 +} + +type Ali struct { + Host string `yaml:"host"` //主机地址 + AccessKeyID string `yaml:"access-key-id"` //阿里云AccessKeyId + AccessKeySecret string `yaml:"access-key-secret"` //阿里云AccessKeySecret + TemplateID string `yaml:"template-id"` //短信模板ID + Signature string `yaml:"signature"` //短信签名 +} +type SmsBao struct { + User string `yaml:"user"` //短信宝用户名 + Password string `yaml:"password"` //短信宝密码 +} diff --git a/config/config.go b/config/config.go index 838b8f0..46728e3 100644 --- a/config/config.go +++ b/config/config.go @@ -5,4 +5,6 @@ type Config struct { Logger Logger `yaml:"logger"` System System `yaml:"system"` Redis Redis `yaml:"redis"` + SMS SMS `yaml:"sms"` + JWT JWT `yaml:"jwt"` } diff --git a/core/captcha.go b/core/captcha.go index 291e7a9..969d236 100644 --- a/core/captcha.go +++ b/core/captcha.go @@ -5,6 +5,7 @@ import ( "github.com/wenlng/go-captcha-assets/bindata/chars" "github.com/wenlng/go-captcha-assets/resources/fonts/fzshengsksjw" "github.com/wenlng/go-captcha-assets/resources/images" + "github.com/wenlng/go-captcha-assets/resources/shapes" "github.com/wenlng/go-captcha-assets/resources/tiles" "github.com/wenlng/go-captcha/v2/base/option" "github.com/wenlng/go-captcha/v2/click" @@ -14,28 +15,125 @@ import ( "schisandra-cloud-album/global" ) +func InitCaptcha() { + initRotateCaptcha() +} + // initTextCaptcha 初始化点选验证码 func initTextCaptcha() { - builder := click.NewBuilder() + builder := click.NewBuilder( + click.WithRangeLen(option.RangeVal{Min: 4, Max: 6}), + click.WithRangeVerifyLen(option.RangeVal{Min: 2, Max: 4}), + click.WithRangeThumbColors([]string{ + "#1f55c4", + "#780592", + "#2f6b00", + "#910000", + "#864401", + "#675901", + "#016e5c", + }), + click.WithRangeColors([]string{ + "#fde98e", + "#60c1ff", + "#fcb08e", + "#fb88ff", + "#b4fed4", + "#cbfaa9", + "#78d6f8", + }), + ) // fonts fonts, err := fzshengsksjw.GetFont() if err != nil { - global.LOG.Fatalln(err) + log.Fatalln(err) } // background images imgs, err := images.GetImages() if err != nil { - global.LOG.Fatalln(err) + log.Fatalln(err) } + // thumb images + //thumbImages, err := thumbs.GetThumbs() + //if err != nil { + // log.Fatalln(err) + //} + + // set resources + builder.SetResources( + click.WithChars(chars.GetChineseChars()), + //click.WithChars([]string{ + // "1A", + // "5E", + // "3d", + // "0p", + // "78", + // "DL", + // "CB", + // "9M", + //}), + //click.WithChars(chars.GetAlphaChars()), + click.WithFonts([]*truetype.Font{fonts}), + click.WithBackgrounds(imgs), + //click.WithThumbBackgrounds(thumbImages), + ) + global.TextCaptcha = builder.Make() + + // ============================ + + builder.Clear() + builder.SetOptions( + click.WithRangeLen(option.RangeVal{Min: 4, Max: 6}), + click.WithRangeVerifyLen(option.RangeVal{Min: 2, Max: 4}), + click.WithRangeThumbColors([]string{ + "#4a85fb", + "#d93ffb", + "#56be01", + "#ee2b2b", + "#cd6904", + "#b49b03", + "#01ad90", + }), + ) builder.SetResources( click.WithChars(chars.GetChineseChars()), click.WithFonts([]*truetype.Font{fonts}), click.WithBackgrounds(imgs), ) - global.TextCaptcha = builder.Make() + global.LightTextCaptcha = builder.Make() +} + +// initClickShapeCaptcha 初始化点击形状验证码 +func initClickShapeCaptcha() { + builder := click.NewBuilder( + click.WithRangeLen(option.RangeVal{Min: 3, Max: 6}), + click.WithRangeVerifyLen(option.RangeVal{Min: 2, Max: 3}), + click.WithRangeThumbBgDistort(1), + click.WithIsThumbNonDeformAbility(true), + ) + + // shape + // click.WithUseShapeOriginalColor(false) -> Random rewriting of graphic colors + shapeMaps, err := shapes.GetShapes() + if err != nil { + log.Fatalln(err) + } + + // background images + imgs, err := images.GetImages() + if err != nil { + log.Fatalln(err) + } + + // set resources + builder.SetResources( + click.WithShapes(shapeMaps), + click.WithBackgrounds(imgs), + ) + global.ClickShapeCaptcha = builder.MakeWithShape() } // initSlideCaptcha 初始化滑动验证码 @@ -95,6 +193,38 @@ func initRotateCaptcha() { global.RotateCaptcha = builder.Make() } -func InitCaptcha() { - initTextCaptcha() +// initSlideRegionCaptcha 初始化滑动区域验证码 +func initSlideRegionCaptcha() { + builder := slide.NewBuilder( + slide.WithGenGraphNumber(2), + slide.WithEnableGraphVerticalRandom(true), + ) + + // background image + imgs, err := images.GetImages() + if err != nil { + log.Fatalln(err) + } + + graphs, err := tiles.GetTiles() + if err != nil { + log.Fatalln(err) + } + var newGraphs = make([]*slide.GraphImage, 0, len(graphs)) + for i := 0; i < len(graphs); i++ { + graph := graphs[i] + newGraphs = append(newGraphs, &slide.GraphImage{ + OverlayImage: graph.OverlayImage, + MaskImage: graph.MaskImage, + ShadowImage: graph.ShadowImage, + }) + } + + // set resources + builder.SetResources( + slide.WithGraphImages(newGraphs), + slide.WithBackgrounds(imgs), + ) + + global.SlideRegionCaptcha = builder.MakeWithRegion() } diff --git a/docs/docs.go b/docs/docs.go index 1e59c81..ca36bb1 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -162,6 +162,341 @@ const docTemplate = `{ } } } + }, + "/api/auth/user/register": { + "post": { + "tags": [ + "鉴权模块" + ], + "summary": "用户注册", + "parameters": [ + { + "description": "用户信息", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.ScaAuthUser" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/captcha/rotate/check": { + "post": { + "description": "验证旋转验证码", + "tags": [ + "旋转验证码" + ], + "summary": "验证旋转验证码", + "parameters": [ + { + "type": "string", + "description": "验证码角度", + "name": "angle", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "验证码key", + "name": "key", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/captcha/rotate/get": { + "get": { + "description": "生成旋转验证码", + "tags": [ + "旋转验证码" + ], + "summary": "生成旋转验证码", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/captcha/shape/check": { + "get": { + "description": "验证点击形状验证码", + "tags": [ + "点击形状验证码" + ], + "summary": "验证点击形状验证码", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/captcha/shape/get": { + "get": { + "description": "生成点击形状验证码", + "tags": [ + "点击形状验证码" + ], + "summary": "生成点击形状验证码", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/captcha/shape/slide/check": { + "get": { + "description": "验证点击形状验证码", + "tags": [ + "点击形状验证码" + ], + "summary": "验证点击形状验证码", + "parameters": [ + { + "type": "string", + "description": "点击坐标", + "name": "point", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "验证码key", + "name": "key", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/captcha/shape/slide/region/get": { + "get": { + "description": "验证点击形状验证码", + "tags": [ + "点击形状验证码" + ], + "summary": "验证点击形状验证码", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/captcha/text/check": { + "get": { + "description": "验证基础文字验证码", + "tags": [ + "基础文字验证码" + ], + "summary": "验证基础文字验证码", + "parameters": [ + { + "type": "string", + "description": "验证码", + "name": "captcha", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "验证码key", + "name": "key", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/captcha/text/get": { + "get": { + "description": "生成基础文字验证码", + "tags": [ + "基础文字验证码" + ], + "summary": "生成基础文字验证码", + "parameters": [ + { + "type": "string", + "description": "验证码类型", + "name": "type", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/sms/ali/send": { + "get": { + "description": "发送短信验证码", + "produces": [ + "application/json" + ], + "tags": [ + "短信验证码" + ], + "summary": "发送短信验证码", + "parameters": [ + { + "type": "string", + "description": "手机号", + "name": "phone", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/api/sms/smsbao/send": { + "get": { + "description": "发送短信验证码", + "produces": [ + "application/json" + ], + "tags": [ + "短信验证码" + ], + "summary": "发送短信验证码", + "parameters": [ + { + "type": "string", + "description": "手机号", + "name": "phone", + "in": "query", + "required": true + } + ], + "responses": {} + } + } + }, + "definitions": { + "model.ScaAuthUser": { + "type": "object", + "properties": { + "avatar": { + "description": "头像", + "type": "string" + }, + "blog": { + "description": "博客", + "type": "string" + }, + "company": { + "description": "公司", + "type": "string" + }, + "created_by": { + "description": "创建人", + "type": "string" + }, + "created_time": { + "description": "创建时间", + "type": "string" + }, + "email": { + "description": "邮箱", + "type": "string" + }, + "gender": { + "description": "性别", + "type": "string" + }, + "introduce": { + "description": "介绍", + "type": "string" + }, + "location": { + "description": "地址", + "type": "string" + }, + "nickname": { + "description": "昵称", + "type": "string" + }, + "phone": { + "description": "电话", + "type": "string" + }, + "status": { + "description": "状态 0 正常 1 封禁", + "type": "integer" + }, + "update_by": { + "description": "更新人", + "type": "string" + }, + "update_time": { + "description": "更新时间", + "type": "string" + }, + "username": { + "description": "用户名", + "type": "string" + }, + "uuid": { + "description": "唯一ID", + "type": "string" + } + } } } }` diff --git a/docs/swagger.json b/docs/swagger.json index ddec889..c5caaa5 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -151,6 +151,341 @@ } } } + }, + "/api/auth/user/register": { + "post": { + "tags": [ + "鉴权模块" + ], + "summary": "用户注册", + "parameters": [ + { + "description": "用户信息", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.ScaAuthUser" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/captcha/rotate/check": { + "post": { + "description": "验证旋转验证码", + "tags": [ + "旋转验证码" + ], + "summary": "验证旋转验证码", + "parameters": [ + { + "type": "string", + "description": "验证码角度", + "name": "angle", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "验证码key", + "name": "key", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/captcha/rotate/get": { + "get": { + "description": "生成旋转验证码", + "tags": [ + "旋转验证码" + ], + "summary": "生成旋转验证码", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/captcha/shape/check": { + "get": { + "description": "验证点击形状验证码", + "tags": [ + "点击形状验证码" + ], + "summary": "验证点击形状验证码", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/captcha/shape/get": { + "get": { + "description": "生成点击形状验证码", + "tags": [ + "点击形状验证码" + ], + "summary": "生成点击形状验证码", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/captcha/shape/slide/check": { + "get": { + "description": "验证点击形状验证码", + "tags": [ + "点击形状验证码" + ], + "summary": "验证点击形状验证码", + "parameters": [ + { + "type": "string", + "description": "点击坐标", + "name": "point", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "验证码key", + "name": "key", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/captcha/shape/slide/region/get": { + "get": { + "description": "验证点击形状验证码", + "tags": [ + "点击形状验证码" + ], + "summary": "验证点击形状验证码", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/captcha/text/check": { + "get": { + "description": "验证基础文字验证码", + "tags": [ + "基础文字验证码" + ], + "summary": "验证基础文字验证码", + "parameters": [ + { + "type": "string", + "description": "验证码", + "name": "captcha", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "验证码key", + "name": "key", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/captcha/text/get": { + "get": { + "description": "生成基础文字验证码", + "tags": [ + "基础文字验证码" + ], + "summary": "生成基础文字验证码", + "parameters": [ + { + "type": "string", + "description": "验证码类型", + "name": "type", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/sms/ali/send": { + "get": { + "description": "发送短信验证码", + "produces": [ + "application/json" + ], + "tags": [ + "短信验证码" + ], + "summary": "发送短信验证码", + "parameters": [ + { + "type": "string", + "description": "手机号", + "name": "phone", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/api/sms/smsbao/send": { + "get": { + "description": "发送短信验证码", + "produces": [ + "application/json" + ], + "tags": [ + "短信验证码" + ], + "summary": "发送短信验证码", + "parameters": [ + { + "type": "string", + "description": "手机号", + "name": "phone", + "in": "query", + "required": true + } + ], + "responses": {} + } + } + }, + "definitions": { + "model.ScaAuthUser": { + "type": "object", + "properties": { + "avatar": { + "description": "头像", + "type": "string" + }, + "blog": { + "description": "博客", + "type": "string" + }, + "company": { + "description": "公司", + "type": "string" + }, + "created_by": { + "description": "创建人", + "type": "string" + }, + "created_time": { + "description": "创建时间", + "type": "string" + }, + "email": { + "description": "邮箱", + "type": "string" + }, + "gender": { + "description": "性别", + "type": "string" + }, + "introduce": { + "description": "介绍", + "type": "string" + }, + "location": { + "description": "地址", + "type": "string" + }, + "nickname": { + "description": "昵称", + "type": "string" + }, + "phone": { + "description": "电话", + "type": "string" + }, + "status": { + "description": "状态 0 正常 1 封禁", + "type": "integer" + }, + "update_by": { + "description": "更新人", + "type": "string" + }, + "update_time": { + "description": "更新时间", + "type": "string" + }, + "username": { + "description": "用户名", + "type": "string" + }, + "uuid": { + "description": "唯一ID", + "type": "string" + } + } } } } \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 76848ab..c710b55 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,3 +1,55 @@ +definitions: + model.ScaAuthUser: + properties: + avatar: + description: 头像 + type: string + blog: + description: 博客 + type: string + company: + description: 公司 + type: string + created_by: + description: 创建人 + type: string + created_time: + description: 创建时间 + type: string + email: + description: 邮箱 + type: string + gender: + description: 性别 + type: string + introduce: + description: 介绍 + type: string + location: + description: 地址 + type: string + nickname: + description: 昵称 + type: string + phone: + description: 电话 + type: string + status: + description: 状态 0 正常 1 封禁 + type: integer + update_by: + description: 更新人 + type: string + update_time: + description: 更新时间 + type: string + username: + description: 用户名 + type: string + uuid: + description: 唯一ID + type: string + type: object info: contact: {} paths: @@ -96,4 +148,178 @@ paths: summary: 根据uuid查询用户 tags: - 鉴权模块 + /api/auth/user/register: + post: + parameters: + - description: 用户信息 + in: body + name: user + required: true + schema: + $ref: '#/definitions/model.ScaAuthUser' + responses: + "200": + description: OK + schema: + type: string + summary: 用户注册 + tags: + - 鉴权模块 + /api/captcha/rotate/check: + post: + description: 验证旋转验证码 + parameters: + - description: 验证码角度 + in: query + name: angle + required: true + type: string + - description: 验证码key + in: query + name: key + required: true + type: string + responses: + "200": + description: OK + schema: + type: string + summary: 验证旋转验证码 + tags: + - 旋转验证码 + /api/captcha/rotate/get: + get: + description: 生成旋转验证码 + responses: + "200": + description: OK + schema: + type: string + summary: 生成旋转验证码 + tags: + - 旋转验证码 + /api/captcha/shape/check: + get: + description: 验证点击形状验证码 + responses: + "200": + description: OK + schema: + type: string + summary: 验证点击形状验证码 + tags: + - 点击形状验证码 + /api/captcha/shape/get: + get: + description: 生成点击形状验证码 + responses: + "200": + description: OK + schema: + type: string + summary: 生成点击形状验证码 + tags: + - 点击形状验证码 + /api/captcha/shape/slide/check: + get: + description: 验证点击形状验证码 + parameters: + - description: 点击坐标 + in: query + name: point + required: true + type: string + - description: 验证码key + in: query + name: key + required: true + type: string + responses: + "200": + description: OK + schema: + type: string + summary: 验证点击形状验证码 + tags: + - 点击形状验证码 + /api/captcha/shape/slide/region/get: + get: + description: 验证点击形状验证码 + responses: + "200": + description: OK + schema: + type: string + summary: 验证点击形状验证码 + tags: + - 点击形状验证码 + /api/captcha/text/check: + get: + description: 验证基础文字验证码 + parameters: + - description: 验证码 + in: query + name: captcha + required: true + type: string + - description: 验证码key + in: query + name: key + required: true + type: string + responses: + "200": + description: OK + schema: + type: string + summary: 验证基础文字验证码 + tags: + - 基础文字验证码 + /api/captcha/text/get: + get: + description: 生成基础文字验证码 + parameters: + - description: 验证码类型 + in: query + name: type + required: true + type: string + responses: + "200": + description: OK + schema: + type: string + summary: 生成基础文字验证码 + tags: + - 基础文字验证码 + /api/sms/ali/send: + get: + description: 发送短信验证码 + parameters: + - description: 手机号 + in: query + name: phone + required: true + type: string + produces: + - application/json + responses: {} + summary: 发送短信验证码 + tags: + - 短信验证码 + /api/sms/smsbao/send: + get: + description: 发送短信验证码 + parameters: + - description: 手机号 + in: query + name: phone + required: true + type: string + produces: + - application/json + responses: {} + summary: 发送短信验证码 + tags: + - 短信验证码 swagger: "2.0" diff --git a/global/global.go b/global/global.go index b84b1aa..1688795 100644 --- a/global/global.go +++ b/global/global.go @@ -12,11 +12,14 @@ import ( // Config 全局配置文件 var ( - CONFIG *config.Config - DB *gorm.DB - LOG *logrus.Logger - TextCaptcha click.Captcha - SlideCaptcha slide.Captcha - RotateCaptcha rotate.Captcha - REDIS *redis.Client + CONFIG *config.Config + DB *gorm.DB + LOG *logrus.Logger + TextCaptcha click.Captcha + LightTextCaptcha click.Captcha + ClickShapeCaptcha click.Captcha + SlideCaptcha slide.Captcha + RotateCaptcha rotate.Captcha + SlideRegionCaptcha slide.Captcha + REDIS *redis.Client ) diff --git a/go.mod b/go.mod index 83bf59f..5e77ef1 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,7 @@ require ( github.com/go-playground/validator/v10 v10.22.0 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/goccy/go-json v0.10.3 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -55,6 +56,8 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nicksnyder/go-i18n/v2 v2.4.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pkg6/go-requests v0.2.2 // indirect + github.com/pkg6/go-sms v0.1.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.8.0 // indirect diff --git a/go.sum b/go.sum index 5f051ba..fafdaa2 100644 --- a/go.sum +++ b/go.sum @@ -58,6 +58,8 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= @@ -111,6 +113,10 @@ github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg6/go-requests v0.2.2 h1:wL0aFmyybM/Wuqj8xQa3sNL5ioAL97hQZ78TJovltbM= +github.com/pkg6/go-requests v0.2.2/go.mod h1:/rcVm8Itd2djtxDVxjRnHURChV86TB4ooZnP+IBZBmg= +github.com/pkg6/go-sms v0.1.2 h1:HZQlBkRVF9xQHhyCMB3kXY/kltfvuNgMTKuN/DoSg7w= +github.com/pkg6/go-sms v0.1.2/go.mod h1:PwFBEssnkYXw+mfSmQ+6fwgXgrcUB9NK5dLUglx+ZW4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= diff --git a/i18n/language/en.toml b/i18n/language/en.toml index 9f3153e..2122a5c 100644 --- a/i18n/language/en.toml +++ b/i18n/language/en.toml @@ -31,6 +31,5 @@ CaptchaNotMatch = "captcha not match!" CaptchaSendFailed = "captcha send failed!" CaptchaSendSuccess = "captcha send successfully!" CaptchaTooOften = "captcha too often!" -CaptchaTooShort = "captcha length must be greater than 6!" -CaptchaTooLong = "captcha length must be less than 20!" -CaptchaSendLimit = "captcha send limit!" +PhoneNotEmpty = "phone number can not be empty!" +EmailNotEmpty = "email can not be empty!" diff --git a/i18n/language/zh.toml b/i18n/language/zh.toml index 44e9a7c..b7631b3 100644 --- a/i18n/language/zh.toml +++ b/i18n/language/zh.toml @@ -31,6 +31,5 @@ CaptchaNotMatch = "验证码不匹配!" CaptchaSendFailed = "验证码发送失败!" CaptchaSendSuccess = "验证码发送成功!" CaptchaTooOften = "验证码发送过于频繁!" -CaptchaTooShort = "验证码长度不能少于6位!" -CaptchaTooLong = "验证码长度不能多于20位!" -CaptchaSendLimit = "验证码发送次数已达上限!" +PhoneNotEmpty = "手机号不能为空!" +EmailNotEmpty = "邮箱不能为空!" diff --git a/main.go b/main.go index 8e745db..1b302d4 100644 --- a/main.go +++ b/main.go @@ -9,18 +9,18 @@ import ( func main() { // 初始化配置 - core.InitConfig() - core.InitLogger() - core.InitGorm() - core.InitCaptcha() - core.InitRedis() + core.InitConfig() // 读取配置文件 + core.InitLogger() // 初始化日志 + core.InitGorm() // 初始化数据库 + core.InitRedis() // 初始化redis + core.InitCaptcha() // 初始化验证码 // 命令行参数绑定 option := cmd.Parse() if cmd.IsStopWeb(&option) { cmd.SwitchOption(&option) return } - r := router.InitRouter() + r := router.InitRouter() // 初始化路由 addr := global.CONFIG.System.Addr() global.LOG.Info("Server run on ", addr) err := r.Run(addr) diff --git a/middleware/i18n.go b/middleware/i18n.go index e662c15..a6de233 100644 --- a/middleware/i18n.go +++ b/middleware/i18n.go @@ -18,7 +18,7 @@ func I18n() gin.HandlerFunc { }), ginI18n.WithGetLngHandle( func(context *gin.Context, defaultLng string) string { - lang := context.Query("lang") + lang := context.GetHeader("Accept-Language") if lang == "" { return defaultLng } diff --git a/router/modules/captcha_router.go b/router/modules/captcha_router.go new file mode 100644 index 0000000..18754dd --- /dev/null +++ b/router/modules/captcha_router.go @@ -0,0 +1,14 @@ +package modules + +import ( + "github.com/gin-gonic/gin" + "schisandra-cloud-album/api" +) + +var captchaApi = api.Api.CaptchaApi + +func CaptchaRouter(router *gin.RouterGroup) { + group := router.Group("/captcha") + group.GET("/rotate/get", captchaApi.GenerateRotateCaptcha) + group.POST("/rotate/check", captchaApi.CheckRotateData) +} diff --git a/router/modules/sms_router.go b/router/modules/sms_router.go new file mode 100644 index 0000000..87cb5cf --- /dev/null +++ b/router/modules/sms_router.go @@ -0,0 +1,14 @@ +package modules + +import ( + "github.com/gin-gonic/gin" + "schisandra-cloud-album/api" +) + +var smsApi = api.Api.SmsApi + +func SmsRouter(router *gin.RouterGroup) { + group := router.Group("/sms") + group.GET("/ali/send", smsApi.SendMessageByAli) + group.GET("/smsbao/send", smsApi.SendMessageBySmsBao) +} diff --git a/router/router.go b/router/router.go index 5d8a9fa..1d72831 100644 --- a/router/router.go +++ b/router/router.go @@ -22,7 +22,9 @@ func InitRouter() *gin.Engine { // 国际化设置 publicGroup.Use(middleware.I18n()) - modules.SwaggerRouter(router) // 注册swagger路由 - modules.AuthRouter(publicGroup) // 注册鉴权路由 + modules.SwaggerRouter(router) // 注册swagger路由 + modules.AuthRouter(publicGroup) // 注册鉴权路由 + modules.CaptchaRouter(publicGroup) // 注册验证码路由 + modules.SmsRouter(publicGroup) // 注册短信验证码路由 return router } diff --git a/utils/cache.go b/utils/cache.go new file mode 100644 index 0000000..0040f82 --- /dev/null +++ b/utils/cache.go @@ -0,0 +1,67 @@ +package utils + +import ( + "sync" + "time" +) + +type cachedata = struct { + data []byte + createAt time.Time +} + +var mux sync.Mutex + +var cachemaps = make(map[string]*cachedata) + +// WriteCache . +func WriteCache(key string, data []byte) { + mux.Lock() + defer mux.Unlock() + cachemaps[key] = &cachedata{ + createAt: time.Now(), + data: data, + } +} + +// ReadCache . +func ReadCache(key string) []byte { + mux.Lock() + defer mux.Unlock() + if cd, ok := cachemaps[key]; ok { + return cd.data + } + + return []byte{} +} + +// ClearCache . +func ClearCache(key string) { + mux.Lock() + defer mux.Unlock() + delete(cachemaps, key) +} + +// RunTimedTask . +func RunTimedTask() { + ticker := time.NewTicker(time.Minute * 5) + go func() { + for range ticker.C { + checkCacheOvertimeFile() + } + }() +} + +func checkCacheOvertimeFile() { + var keys = make([]string, 0) + for key, data := range cachemaps { + ex := time.Now().Unix() - data.createAt.Unix() + if ex > (60 * 30) { + keys = append(keys, key) + } + } + + for _, key := range keys { + ClearCache(key) + } +} diff --git a/utils/genValidateCode.go b/utils/genValidateCode.go new file mode 100644 index 0000000..e9aa05d --- /dev/null +++ b/utils/genValidateCode.go @@ -0,0 +1,20 @@ +package utils + +import ( + "fmt" + "math/rand" + "strings" + "time" +) + +func GenValidateCode(width int) string { + numeric := [10]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + r := len(numeric) + rand.New(rand.NewSource(time.Now().UnixNano())) + + var sb strings.Builder + for i := 0; i < width; i++ { + fmt.Fprintf(&sb, "%d", numeric[rand.Intn(r)]) + } + return sb.String() +} diff --git a/utils/jwt.go b/utils/jwt.go new file mode 100644 index 0000000..376256a --- /dev/null +++ b/utils/jwt.go @@ -0,0 +1,49 @@ +package utils + +import ( + "github.com/golang-jwt/jwt/v5" + "schisandra-cloud-album/global" + "time" +) + +type JWTPayload struct { + UserID int `json:"user_id"` + Role string `json:"role"` + Username string `json:"username"` +} + +type JWTClaims struct { + JWTPayload + jwt.RegisteredClaims +} + +var MySecret = []byte(global.CONFIG.JWT.Secret) + +// GenerateToken generates a JWT token with the given payload +func GenerateToken(payload JWTPayload) (string, error) { + claims := JWTClaims{ + JWTPayload: payload, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(MySecret) +} + +// ParseToken parses a JWT token and returns the payload +func ParseToken(tokenString string) (*JWTPayload, error) { + token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) { + return MySecret, nil + }) + if err != nil { + global.LOG.Error(err) + return nil, err + } + if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid { + return &claims.JWTPayload, nil + } + return nil, err +}