diff --git a/.caches/master.jpg b/.caches/master.jpg deleted file mode 100644 index 891c664..0000000 Binary files a/.caches/master.jpg and /dev/null differ diff --git a/.caches/thumb.png b/.caches/thumb.png deleted file mode 100644 index 25baa74..0000000 Binary files a/.caches/thumb.png and /dev/null differ diff --git a/.gitignore b/.gitignore index 0e8f23f..929c7fa 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ *.dll *.so *.dylib +.air.toml # Test binary, built with `go test -c` *.test diff --git a/api/api.go b/api/api.go index 9b60a6c..4dec60c 100644 --- a/api/api.go +++ b/api/api.go @@ -3,12 +3,14 @@ package api import ( "schisandra-cloud-album/api/auth_api" "schisandra-cloud-album/api/captcha_api" + "schisandra-cloud-album/api/sms_api" ) // Apis 统一导出的api type Apis struct { AuthApi auth_api.AuthAPI - CaptchaAPI captcha_api.CaptchaAPI + CaptchaApi captcha_api.CaptchaAPI + SmsApi sms_api.SmsAPI } // Api new函数实例化,实例化完成后会返回结构体地指针类型 diff --git a/api/captcha_api/captcha_api.go b/api/captcha_api/captcha_api.go index 2bb6271..c8a3add 100644 --- a/api/captcha_api/captcha_api.go +++ b/api/captcha_api/captcha_api.go @@ -3,32 +3,359 @@ package captcha_api import ( "encoding/json" "fmt" - "github.com/wenlng/go-captcha/v2/base/option" + ginI18n "github.com/gin-contrib/i18n" + "github.com/gin-gonic/gin" + "github.com/wenlng/go-captcha-assets/helper" + "github.com/wenlng/go-captcha/v2/click" + "github.com/wenlng/go-captcha/v2/rotate" + "github.com/wenlng/go-captcha/v2/slide" "log" + "schisandra-cloud-album/api/captcha_api/model" + "schisandra-cloud-album/common/redis" + "schisandra-cloud-album/common/result" "schisandra-cloud-album/global" + "strconv" + "strings" + "time" ) -// GenerateTextCaptcha 生成文本验证码 -func GenerateTextCaptcha() { - captData, err := global.TextCaptcha.Generate() +// GenerateRotateCaptcha 生成旋转验证码 +// @Summary 生成旋转验证码 +// @Description 生成旋转验证码 +// @Tags 旋转验证码 +// @Success 200 {string} json +// @Router /api/captcha/rotate/get [get] +func (CaptchaAPI) GenerateRotateCaptcha(c *gin.Context) { + captchaData, err := global.RotateCaptcha.Generate() + if err != nil { + global.LOG.Fatalln(err) + } + blockData := captchaData.GetData() + if blockData == nil { + result.FailWithNull(c) + return + } + var masterImageBase64, thumbImageBase64 string + masterImageBase64 = captchaData.GetMasterImage().ToBase64() + thumbImageBase64 = captchaData.GetThumbImage().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, + "thumb": thumbImageBase64, + } + result.OkWithData(bt, c) +} + +// CheckRotateData 验证旋转验证码 +// @Summary 验证旋转验证码 +// @Description 验证旋转验证码 +// @Tags 旋转验证码 +// @Param angle query string true "验证码角度" +// @Param key query string true "验证码key" +// @Success 200 {string} json +// @Router /api/captcha/rotate/check [post] +func (CaptchaAPI) CheckRotateData(c *gin.Context) { + rotateRequest := model.RotateCaptchaRequest{} + err := c.ShouldBindJSON(&rotateRequest) + angle := rotateRequest.Angle + key := rotateRequest.Key + if err != nil { + result.FailWithNull(c) + return + } + cacheDataByte, err := redis.Get(key).Bytes() + if len(cacheDataByte) == 0 || err != nil { + result.FailWithCodeAndMessage(1011, ginI18n.MustGetMessage(c, "CaptchaExpired"), c) + return + } + var dct *rotate.Block + if err := json.Unmarshal(cacheDataByte, &dct); err != nil { + result.FailWithNull(c) + return + } + sAngle, _ := strconv.ParseFloat(fmt.Sprintf("%v", angle), 64) + chkRet := rotate.CheckAngle(int64(sAngle), int64(dct.Angle), 2) + if chkRet { + result.OkWithMessage("success", c) + return + } + result.FailWithMessage("fail", c) +} + +// GenerateBasicTextCaptcha 生成基础文字验证码 +// @Summary 生成基础文字验证码 +// @Description 生成基础文字验证码 +// @Tags 基础文字验证码 +// @Param type query string true "验证码类型" +// @Success 200 {string} json +// @Router /api/captcha/text/get [get] +func (CaptchaAPI) GenerateBasicTextCaptcha(c *gin.Context) { + var capt click.Captcha + if c.Query("type") == "light" { + capt = global.LightTextCaptcha + } else { + capt = global.TextCaptcha + } + captData, err := capt.Generate() + if err != nil { + global.LOG.Fatalln(err) + } + dotData := captData.GetData() + if dotData == nil { + result.FailWithNull(c) + return + } + var masterImageBase64, thumbImageBase64 string + masterImageBase64 = captData.GetMasterImage().ToBase64() + thumbImageBase64 = captData.GetThumbImage().ToBase64() + + dotsByte, err := json.Marshal(dotData) + 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, + "thumb": thumbImageBase64, + } + result.OkWithData(bt, c) +} + +// CheckClickData 验证基础文字验证码 +// @Summary 验证基础文字验证码 +// @Description 验证基础文字验证码 +// @Tags 基础文字验证码 +// @Param captcha query string true "验证码" +// @Param key query string true "验证码key" +// @Success 200 {string} json +// @Router /api/captcha/text/check [get] +func (CaptchaAPI) CheckClickData(c *gin.Context) { + dots := c.Query("dots") + key := c.Query("key") + if dots == "" || key == "" { + result.FailWithNull(c) + return + } + cacheDataByte, err := redis.Get(key).Bytes() + if len(cacheDataByte) == 0 || err != nil { + result.FailWithNull(c) + return + } + src := strings.Split(dots, ",") + + var dct map[int]*click.Dot + if err := json.Unmarshal(cacheDataByte, &dct); err != nil { + result.FailWithNull(c) + return + } + chkRet := false + if (len(dct) * 2) == len(src) { + for i := 0; i < len(dct); i++ { + dot := dct[i] + j := i * 2 + k := i*2 + 1 + sx, _ := strconv.ParseFloat(fmt.Sprintf("%v", src[j]), 64) + sy, _ := strconv.ParseFloat(fmt.Sprintf("%v", src[k]), 64) + + chkRet = click.CheckPoint(int64(sx), int64(sy), int64(dot.X), int64(dot.Y), int64(dot.Width), int64(dot.Height), 0) + if !chkRet { + break + } + } + } + if chkRet { + result.OkWithMessage("success", c) + return + } + result.FailWithMessage("fail", c) +} + +// GenerateClickShapeCaptcha 生成点击形状验证码 +// @Summary 生成点击形状验证码 +// @Description 生成点击形状验证码 +// @Tags 点击形状验证码 +// @Success 200 {string} json +// @Router /api/captcha/shape/get [get] +func (CaptchaAPI) GenerateClickShapeCaptcha(c *gin.Context) { + captData, err := global.ClickShapeCaptcha.Generate() if err != nil { log.Fatalln(err) } - dotData := captData.GetData() if dotData == nil { - log.Fatalln(">>>>> 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 +}