add jwt / complete the rotation verification

This commit is contained in:
landaiqing
2024-08-12 22:05:59 +08:00
parent 54f6256c00
commit 48c5aeb0f4
31 changed files with 1702 additions and 44 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

1
.gitignore vendored
View File

@@ -8,6 +8,7 @@
*.dll *.dll
*.so *.so
*.dylib *.dylib
.air.toml
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test

View File

@@ -3,12 +3,14 @@ package api
import ( import (
"schisandra-cloud-album/api/auth_api" "schisandra-cloud-album/api/auth_api"
"schisandra-cloud-album/api/captcha_api" "schisandra-cloud-album/api/captcha_api"
"schisandra-cloud-album/api/sms_api"
) )
// Apis 统一导出的api // Apis 统一导出的api
type Apis struct { type Apis struct {
AuthApi auth_api.AuthAPI AuthApi auth_api.AuthAPI
CaptchaAPI captcha_api.CaptchaAPI CaptchaApi captcha_api.CaptchaAPI
SmsApi sms_api.SmsAPI
} }
// Api new函数实例化实例化完成后会返回结构体地指针类型 // Api new函数实例化实例化完成后会返回结构体地指针类型

View File

@@ -3,32 +3,359 @@ package captcha_api
import ( import (
"encoding/json" "encoding/json"
"fmt" "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" "log"
"schisandra-cloud-album/api/captcha_api/model"
"schisandra-cloud-album/common/redis"
"schisandra-cloud-album/common/result"
"schisandra-cloud-album/global" "schisandra-cloud-album/global"
"strconv"
"strings"
"time"
) )
// GenerateTextCaptcha 生成文本验证码 // GenerateRotateCaptcha 生成旋转验证码
func GenerateTextCaptcha() { // @Summary 生成旋转验证码
captData, err := global.TextCaptcha.Generate() // @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 { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
dotData := captData.GetData() dotData := captData.GetData()
if dotData == nil { 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) dotsByte, err := json.Marshal(dotData)
fmt.Println(">>>>> ", string(dots))
err = captData.GetMasterImage().SaveToFile("./.caches/master.jpg", option.QualityNone)
if err != nil { 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 { 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)
} }

View File

@@ -0,0 +1,6 @@
package model
type RotateCaptchaRequest struct {
Angle int `json:"angle"`
Key string `json:"key"`
}

3
api/sms_api/sms.go Normal file
View File

@@ -0,0 +1,3 @@
package sms_api
type SmsAPI struct{}

80
api/sms_api/sms_api.go Normal file
View File

@@ -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)
}

View File

@@ -54,7 +54,7 @@ func initInfo() (db *gorm.DB, g *gen.Generator, fieldOpts []gen.ModelOpt) {
// WithDefaultQuery 生成默认查询结构体(作为全局变量使用), 即`Q`结构体和其字段(各表模型) // WithDefaultQuery 生成默认查询结构体(作为全局变量使用), 即`Q`结构体和其字段(各表模型)
// WithoutContext 生成没有context调用限制的代码供查询 // WithoutContext 生成没有context调用限制的代码供查询
// WithQueryInterface 生成interface形式的查询代码(可导出), 如`Where()`方法返回的就是一个可导出的接口类型 // WithQueryInterface 生成interface形式的查询代码(可导出), 如`Where()`方法返回的就是一个可导出的接口类型
Mode: gen.WithDefaultQuery | gen.WithQueryInterface, Mode: gen.WithDefaultQuery | gen.WithoutContext,
// 表字段可为 null 值时, 对应结体字段使用指针类型 // 表字段可为 null 值时, 对应结体字段使用指针类型
FieldNullable: true, // generate pointer when field is nullable FieldNullable: true, // generate pointer when field is nullable

View File

@@ -13,6 +13,7 @@ const (
ParamsMatchError ErrorCode = 1008 ParamsMatchError ErrorCode = 1008
ParamsNotUniqueError ErrorCode = 1009 ParamsNotUniqueError ErrorCode = 1009
FileSizeExceeded ErrorCode = 1010 FileSizeExceeded ErrorCode = 1010
CaptchaExpireError ErrorCode = 1011
) )
type ErrorMap map[ErrorCode]string type ErrorMap map[ErrorCode]string
@@ -28,4 +29,5 @@ var ErrMap = ErrorMap{
1008: "参数值不匹配", 1008: "参数值不匹配",
1009: "参数值不唯一", 1009: "参数值不唯一",
1010: "超出文件上传大小限制", 1010: "超出文件上传大小限制",
1011: "验证码已过期",
} }

View File

@@ -38,12 +38,17 @@ func OkWithMessage(msg string, c *gin.Context) {
func Fail(msg string, data any, c *gin.Context) { func Fail(msg string, data any, c *gin.Context) {
Result(FAIL, msg, data, false, c) 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) { func FailWithMessage(msg string, c *gin.Context) {
Result(FAIL, msg, nil, false, c) Result(FAIL, msg, nil, false, c)
} }
func FailWithData(data any, c *gin.Context) { func FailWithData(data any, c *gin.Context) {
Result(FAIL, "fail", data, false, c) Result(FAIL, "fail", data, false, c)
}
func FailWithNull(c *gin.Context) {
Result(FAIL, "fail", nil, false, c)
} }
func FailWithCode(code ErrorCode, c *gin.Context) { func FailWithCode(code ErrorCode, c *gin.Context) {
msg, ok := ErrMap[code] msg, ok := ErrMap[code]

10
config/conf_jwt.go Normal file
View File

@@ -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"`
}

18
config/conf_sms.go Normal file
View File

@@ -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"` //短信宝密码
}

View File

@@ -5,4 +5,6 @@ type Config struct {
Logger Logger `yaml:"logger"` Logger Logger `yaml:"logger"`
System System `yaml:"system"` System System `yaml:"system"`
Redis Redis `yaml:"redis"` Redis Redis `yaml:"redis"`
SMS SMS `yaml:"sms"`
JWT JWT `yaml:"jwt"`
} }

View File

@@ -5,6 +5,7 @@ import (
"github.com/wenlng/go-captcha-assets/bindata/chars" "github.com/wenlng/go-captcha-assets/bindata/chars"
"github.com/wenlng/go-captcha-assets/resources/fonts/fzshengsksjw" "github.com/wenlng/go-captcha-assets/resources/fonts/fzshengsksjw"
"github.com/wenlng/go-captcha-assets/resources/images" "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-assets/resources/tiles"
"github.com/wenlng/go-captcha/v2/base/option" "github.com/wenlng/go-captcha/v2/base/option"
"github.com/wenlng/go-captcha/v2/click" "github.com/wenlng/go-captcha/v2/click"
@@ -14,28 +15,125 @@ import (
"schisandra-cloud-album/global" "schisandra-cloud-album/global"
) )
func InitCaptcha() {
initRotateCaptcha()
}
// initTextCaptcha 初始化点选验证码 // initTextCaptcha 初始化点选验证码
func 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
fonts, err := fzshengsksjw.GetFont() fonts, err := fzshengsksjw.GetFont()
if err != nil { if err != nil {
global.LOG.Fatalln(err) log.Fatalln(err)
} }
// background images // background images
imgs, err := images.GetImages() imgs, err := images.GetImages()
if err != nil { 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( builder.SetResources(
click.WithChars(chars.GetChineseChars()), click.WithChars(chars.GetChineseChars()),
click.WithFonts([]*truetype.Font{fonts}), click.WithFonts([]*truetype.Font{fonts}),
click.WithBackgrounds(imgs), 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 初始化滑动验证码 // initSlideCaptcha 初始化滑动验证码
@@ -95,6 +193,38 @@ func initRotateCaptcha() {
global.RotateCaptcha = builder.Make() global.RotateCaptcha = builder.Make()
} }
func InitCaptcha() { // initSlideRegionCaptcha 初始化滑动区域验证码
initTextCaptcha() 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()
} }

View File

@@ -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"
}
}
} }
} }
}` }`

View File

@@ -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"
}
}
} }
} }
} }

View File

@@ -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: info:
contact: {} contact: {}
paths: paths:
@@ -96,4 +148,178 @@ paths:
summary: 根据uuid查询用户 summary: 根据uuid查询用户
tags: 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" swagger: "2.0"

View File

@@ -12,11 +12,14 @@ import (
// Config 全局配置文件 // Config 全局配置文件
var ( var (
CONFIG *config.Config CONFIG *config.Config
DB *gorm.DB DB *gorm.DB
LOG *logrus.Logger LOG *logrus.Logger
TextCaptcha click.Captcha TextCaptcha click.Captcha
SlideCaptcha slide.Captcha LightTextCaptcha click.Captcha
RotateCaptcha rotate.Captcha ClickShapeCaptcha click.Captcha
REDIS *redis.Client SlideCaptcha slide.Captcha
RotateCaptcha rotate.Captcha
SlideRegionCaptcha slide.Captcha
REDIS *redis.Client
) )

3
go.mod
View File

@@ -43,6 +43,7 @@ require (
github.com/go-playground/validator/v10 v10.22.0 // indirect github.com/go-playground/validator/v10 v10.22.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.3 // 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/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // 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/modern-go/reflect2 v1.0.2 // indirect
github.com/nicksnyder/go-i18n/v2 v2.4.0 // indirect github.com/nicksnyder/go-i18n/v2 v2.4.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // 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/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect golang.org/x/arch v0.8.0 // indirect

6
go.sum
View File

@@ -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/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 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 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 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 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= 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/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 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=

View File

@@ -31,6 +31,5 @@ CaptchaNotMatch = "captcha not match!"
CaptchaSendFailed = "captcha send failed!" CaptchaSendFailed = "captcha send failed!"
CaptchaSendSuccess = "captcha send successfully!" CaptchaSendSuccess = "captcha send successfully!"
CaptchaTooOften = "captcha too often!" CaptchaTooOften = "captcha too often!"
CaptchaTooShort = "captcha length must be greater than 6!" PhoneNotEmpty = "phone number can not be empty!"
CaptchaTooLong = "captcha length must be less than 20!" EmailNotEmpty = "email can not be empty!"
CaptchaSendLimit = "captcha send limit!"

View File

@@ -31,6 +31,5 @@ CaptchaNotMatch = "验证码不匹配!"
CaptchaSendFailed = "验证码发送失败!" CaptchaSendFailed = "验证码发送失败!"
CaptchaSendSuccess = "验证码发送成功!" CaptchaSendSuccess = "验证码发送成功!"
CaptchaTooOften = "验证码发送过于频繁!" CaptchaTooOften = "验证码发送过于频繁!"
CaptchaTooShort = "验证码长度不能少于6位!" PhoneNotEmpty = "手机号不能为空!"
CaptchaTooLong = "验证码长度不能多于20位!" EmailNotEmpty = "邮箱不能为空!"
CaptchaSendLimit = "验证码发送次数已达上限!"

12
main.go
View File

@@ -9,18 +9,18 @@ import (
func main() { func main() {
// 初始化配置 // 初始化配置
core.InitConfig() core.InitConfig() // 读取配置文件
core.InitLogger() core.InitLogger() // 初始化日志
core.InitGorm() core.InitGorm() // 初始化数据库
core.InitCaptcha() core.InitRedis() // 初始化redis
core.InitRedis() core.InitCaptcha() // 初始化验证码
// 命令行参数绑定 // 命令行参数绑定
option := cmd.Parse() option := cmd.Parse()
if cmd.IsStopWeb(&option) { if cmd.IsStopWeb(&option) {
cmd.SwitchOption(&option) cmd.SwitchOption(&option)
return return
} }
r := router.InitRouter() r := router.InitRouter() // 初始化路由
addr := global.CONFIG.System.Addr() addr := global.CONFIG.System.Addr()
global.LOG.Info("Server run on ", addr) global.LOG.Info("Server run on ", addr)
err := r.Run(addr) err := r.Run(addr)

View File

@@ -18,7 +18,7 @@ func I18n() gin.HandlerFunc {
}), }),
ginI18n.WithGetLngHandle( ginI18n.WithGetLngHandle(
func(context *gin.Context, defaultLng string) string { func(context *gin.Context, defaultLng string) string {
lang := context.Query("lang") lang := context.GetHeader("Accept-Language")
if lang == "" { if lang == "" {
return defaultLng return defaultLng
} }

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -22,7 +22,9 @@ func InitRouter() *gin.Engine {
// 国际化设置 // 国际化设置
publicGroup.Use(middleware.I18n()) publicGroup.Use(middleware.I18n())
modules.SwaggerRouter(router) // 注册swagger路由 modules.SwaggerRouter(router) // 注册swagger路由
modules.AuthRouter(publicGroup) // 注册鉴权路由 modules.AuthRouter(publicGroup) // 注册鉴权路由
modules.CaptchaRouter(publicGroup) // 注册验证码路由
modules.SmsRouter(publicGroup) // 注册短信验证码路由
return router return router
} }

67
utils/cache.go Normal file
View File

@@ -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)
}
}

20
utils/genValidateCode.go Normal file
View File

@@ -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()
}

49
utils/jwt.go Normal file
View File

@@ -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
}