diff --git a/api/permission_api/dto/request_dto.go b/api/permission_api/dto/request_dto.go index a499d73..8e81f4c 100644 --- a/api/permission_api/dto/request_dto.go +++ b/api/permission_api/dto/request_dto.go @@ -15,3 +15,6 @@ type AddPermissionToRoleRequestDto struct { Permission string `json:"permission"` Method string `json:"method"` } +type GetPermissionRequest struct { + UserId string `json:"user_id" binding:"required"` +} diff --git a/api/permission_api/permission_api.go b/api/permission_api/permission_api.go index 20974c4..1e1b907 100644 --- a/api/permission_api/permission_api.go +++ b/api/permission_api/permission_api.go @@ -71,12 +71,13 @@ func (PermissionAPI) AssignPermissionsToRole(c *gin.Context) { // GetUserPermissions 获取用户角色权限 func (PermissionAPI) GetUserPermissions(c *gin.Context) { - userId := c.Query("user_id") - if userId == "" { - result.FailWithMessage("user_id is required", c) + getPermissionRequest := dto.GetPermissionRequest{} + err := c.ShouldBindJSON(&getPermissionRequest) + if err != nil { + global.LOG.Error(err) return } - data, err := global.Casbin.GetImplicitRolesForUser(userId) + data, err := global.Casbin.GetImplicitRolesForUser(getPermissionRequest.UserId) if err != nil { result.FailWithMessage("Get user permissions failed", c) return diff --git a/api/sms_api/dto/request_dto.go b/api/sms_api/dto/request_dto.go new file mode 100644 index 0000000..99fb94e --- /dev/null +++ b/api/sms_api/dto/request_dto.go @@ -0,0 +1,7 @@ +package dto + +type SmsRequest struct { + Phone string `json:"phone" binding:"required"` + Angle int64 `json:"angle" binding:"required"` + Key string `json:"key" binding:"required"` +} diff --git a/api/sms_api/sms_api.go b/api/sms_api/sms_api.go index c567a2f..2c62830 100644 --- a/api/sms_api/sms_api.go +++ b/api/sms_api/sms_api.go @@ -7,6 +7,7 @@ import ( "github.com/pkg6/go-sms/gateways" "github.com/pkg6/go-sms/gateways/aliyun" "github.com/pkg6/go-sms/gateways/smsbao" + "schisandra-cloud-album/api/sms_api/dto" "schisandra-cloud-album/common/constant" "schisandra-cloud-album/common/redis" "schisandra-cloud-album/common/result" @@ -23,17 +24,23 @@ import ( // @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) + smsRequest := dto.SmsRequest{} + err := c.ShouldBindJSON(&smsRequest) + if err != nil { + result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendFailed"), c) return } - isPhone := utils.IsPhone(phone) + checkRotateData := utils.CheckRotateData(smsRequest.Angle, smsRequest.Key) + if !checkRotateData { + result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaVerifyError"), c) + return + } + isPhone := utils.IsPhone(smsRequest.Phone) if !isPhone { result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneErrorFormat"), c) return } - val := redis.Get(constant.UserLoginSmsRedisKey + phone).Val() + val := redis.Get(constant.UserLoginSmsRedisKey + smsRequest.Phone).Val() if val != "" { result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaTooOften"), c) return @@ -46,12 +53,12 @@ func (SmsAPI) SendMessageByAli(c *gin.Context) { }, }) code := utils.GenValidateCode(6) - wrong := redis.Set(constant.UserLoginSmsRedisKey+phone, code, time.Minute).Err() + wrong := redis.Set(constant.UserLoginSmsRedisKey+smsRequest.Phone, code, time.Minute).Err() if wrong != nil { global.LOG.Error(wrong) return } - _, err := sms.Send(phone, gosms.MapStringAny{ + _, err = sms.Send(smsRequest.Phone, gosms.MapStringAny{ "content": "您的验证码是:****。请不要把验证码泄露给其他人。", "template": global.CONFIG.SMS.Ali.TemplateID, //"signName": global.CONFIG.SMS.Ali.Signature, @@ -74,19 +81,25 @@ func (SmsAPI) SendMessageByAli(c *gin.Context) { // @Tags 短信验证码 // @Produce json // @Param phone query string true "手机号" -// @Router /api/sms/smsbao/send [get] +// @Router /api/sms/smsbao/send [post] func (SmsAPI) SendMessageBySmsBao(c *gin.Context) { - phone := c.Query("phone") - if phone == "" { - result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneNotEmpty"), c) + smsRequest := dto.SmsRequest{} + err := c.ShouldBindJSON(&smsRequest) + if err != nil { + result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendFailed"), c) return } - isPhone := utils.IsPhone(phone) + checkRotateData := utils.CheckRotateData(smsRequest.Angle, smsRequest.Key) + if !checkRotateData { + result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaVerifyError"), c) + return + } + isPhone := utils.IsPhone(smsRequest.Phone) if !isPhone { result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneErrorFormat"), c) return } - val := redis.Get(constant.UserLoginSmsRedisKey + phone).Val() + val := redis.Get(constant.UserLoginSmsRedisKey + smsRequest.Phone).Val() if val != "" { result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaTooOften"), c) return @@ -98,12 +111,12 @@ func (SmsAPI) SendMessageBySmsBao(c *gin.Context) { }, }) code := utils.GenValidateCode(6) - wrong := redis.Set(constant.UserLoginSmsRedisKey+phone, code, time.Minute).Err() + wrong := redis.Set(constant.UserLoginSmsRedisKey+smsRequest.Phone, code, time.Minute).Err() if wrong != nil { global.LOG.Error(wrong) return } - _, err := sms.Send(phone, gosms.MapStringAny{ + _, err = sms.Send(smsRequest.Phone, gosms.MapStringAny{ "content": "您的验证码是:" + code + "。请不要把验证码泄露给其他人。", }, nil) if err != nil { @@ -120,20 +133,26 @@ func (SmsAPI) SendMessageBySmsBao(c *gin.Context) { // @Tags 短信验证码 // @Produce json // @Param phone query string true "手机号" -// @Router /api/sms/test/send [get] +// @Router /api/sms/test/send [post] func (SmsAPI) SendMessageTest(c *gin.Context) { - phone := c.Query("phone") - if phone == "" { - result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneNotEmpty"), c) + smsRequest := dto.SmsRequest{} + err := c.ShouldBindJSON(&smsRequest) + if err != nil { + result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendFailed"), c) return } - isPhone := utils.IsPhone(phone) + checkRotateData := utils.CheckRotateData(smsRequest.Angle, smsRequest.Key) + if !checkRotateData { + result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaVerifyError"), c) + return + } + isPhone := utils.IsPhone(smsRequest.Phone) if !isPhone { result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneError"), c) return } code := utils.GenValidateCode(6) - err := redis.Set(constant.UserLoginSmsRedisKey+phone, code, time.Minute).Err() + err = redis.Set(constant.UserLoginSmsRedisKey+smsRequest.Phone, code, time.Minute).Err() if err != nil { global.LOG.Error(err) result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendFailed"), c) diff --git a/api/user_api/dto/request_dto.go b/api/user_api/dto/request_dto.go index c22ed14..9ff9802 100644 --- a/api/user_api/dto/request_dto.go +++ b/api/user_api/dto/request_dto.go @@ -19,6 +19,8 @@ type AccountLoginRequest struct { Account string `json:"account" binding:"required"` Password string `json:"password" binding:"required"` AutoLogin bool `json:"auto_login" binding:"required"` + Angle int64 `json:"angle" binding:"required"` + Key string `json:"key" binding:"required"` } // AddUserRequest 新增用户请求 diff --git a/api/user_api/user_api.go b/api/user_api/user_api.go index dd36e05..4c08216 100644 --- a/api/user_api/user_api.go +++ b/api/user_api/user_api.go @@ -117,6 +117,11 @@ func (UserAPI) AccountLogin(c *gin.Context) { result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c) return } + rotateData := utils.CheckRotateData(accountLoginRequest.Angle, accountLoginRequest.Key) + if !rotateData { + result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaVerifyError"), c) + return + } account := accountLoginRequest.Account password := accountLoginRequest.Password diff --git a/common/constant/redis_key.go b/common/constant/redis_key.go index 723c48b..97c995e 100644 --- a/common/constant/redis_key.go +++ b/common/constant/redis_key.go @@ -1,7 +1,7 @@ package constant +// 登录相关的redis key const ( - // 登录相关的redis key UserLoginSmsRedisKey = "user:sms:" UserLoginTokenRedisKey = "user:token:" UserLoginCaptchaRedisKey = "user:captcha:" @@ -10,8 +10,14 @@ const ( UserSessionRedisKey = "user:session:" ) -// 登录之后 +// 评论相关的redis key const ( CommentSubmitCaptchaRedisKey = "comment:submit:captcha:" CommentOfflineMessageRedisKey = "comment:offline:message:" ) + +// 系统相关的redis key + +const ( + SystemApiNonceRedisKey = "system:api:nonce:" +) diff --git a/i18n/language/en.toml b/i18n/language/en.toml index 9e22b73..8bfefdd 100644 --- a/i18n/language/en.toml +++ b/i18n/language/en.toml @@ -73,4 +73,5 @@ CommentLikeSuccess = "comment like success!" CommentLikeFailed = "comment like failed!" CommentDislikeSuccess = "comment dislike success!" CommentDislikeFailed = "comment dislike failed!" -CaptchaVerifyError = "captcha error!" \ No newline at end of file +CaptchaVerifyError = "captcha error!" +RequestVerifyError = "request verify error!" \ No newline at end of file diff --git a/i18n/language/zh.toml b/i18n/language/zh.toml index dc0ec1f..6e34311 100644 --- a/i18n/language/zh.toml +++ b/i18n/language/zh.toml @@ -74,3 +74,4 @@ CommentLikeFailed = "评论点赞失败!" CommentDislikeSuccess = "评论取消点赞成功!" CommentDislikeFailed = "评论取消点赞失败!" CaptchaVerifyError = "验证失败!" +RequestVerifyError = "请求验证失败!" diff --git a/middleware/verify_signature.go b/middleware/verify_signature.go new file mode 100644 index 0000000..c310c2b --- /dev/null +++ b/middleware/verify_signature.go @@ -0,0 +1,93 @@ +package middleware + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + ginI18n "github.com/gin-contrib/i18n" + "github.com/gin-gonic/gin" + "io" + "net/http" + "schisandra-cloud-album/common/constant" + "schisandra-cloud-album/common/redis" + "schisandra-cloud-album/common/result" + "schisandra-cloud-album/global" + "strconv" + "time" +) + +func VerifySignature() gin.HandlerFunc { + return func(c *gin.Context) { + // 仅处理 POST 请求 + if c.Request.Method != http.MethodPost { + c.Next() + return + } + // 从请求头获取签名和时间戳 + signature := c.GetHeader("X-Sign") + timestamp := c.GetHeader("X-Timestamp") + nonce := c.GetHeader("X-Nonce") + secretKey := global.CONFIG.Encrypt.Key + + // 检查时间戳是否过期,这里设置为5分钟过期 + if timestamp == "" || time.Since(parseTimestamp(timestamp)) > 5*time.Minute { + result.FailWithMessage(ginI18n.MustGetMessage(c, "RequestVerifyError"), c) + c.Abort() + return + } + + // 检查 nonce 是否已经被使用 + if data := redis.Get(constant.SystemApiNonceRedisKey + nonce).Val(); data != "" { + result.FailWithMessage(ginI18n.MustGetMessage(c, "RequestVerifyError"), c) + c.Abort() + return + } + + // 记录 nonce 到 Redis 中,并设置过期时间为 5 分钟 + if err := redis.Set(constant.SystemApiNonceRedisKey+nonce, true, 5*time.Minute).Err(); err != nil { + global.LOG.Error(err.Error()) + c.Abort() + return + } + + // 获取请求方法和请求体 + var payload string + if c.Request.Method == http.MethodPost { + body, err := io.ReadAll(c.Request.Body) + if err != nil { + result.FailWithMessage(ginI18n.MustGetMessage(c, "RequestReadError"), c) + c.Abort() + return + } + payload = string(body) + // 重新设置请求体,以便后续处理中可以再次读取 + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + } + + // 创建待签名字符串 + baseString := c.Request.Method + ":" + payload + ":" + timestamp + ":" + nonce + ":" + secretKey + + // 生成 MD5 签名 + h := md5.New() + h.Write([]byte(baseString)) + expectedSignature := hex.EncodeToString(h.Sum(nil)) + + // 验证签名 + if signature != expectedSignature { + result.FailWithMessage(ginI18n.MustGetMessage(c, "RequestVerifyError"), c) + c.Abort() + return + } + // 继续处理请求 + c.Next() + } +} + +// 辅助函数:解析时间戳 +func parseTimestamp(ts string) time.Time { + t, err := strconv.ParseInt(ts, 10, 64) + if err != nil { + return time.Time{} // 解析错误返回零时间 + } + return time.Unix(t/1000, 0) // 假设时间戳是毫秒 +} diff --git a/router/modules/permission_router.go b/router/modules/permission_router.go index 0638b8e..243676a 100644 --- a/router/modules/permission_router.go +++ b/router/modules/permission_router.go @@ -11,6 +11,6 @@ func PermissionRouter(router *gin.RouterGroup) { group := router.Group("/auth/permission") { group.POST("/add", permissionApi.AddPermissions) - group.GET("/get_user_permissions", permissionApi.GetUserPermissions) + group.POST("/get_user_permissions", permissionApi.GetUserPermissions) } } diff --git a/router/modules/sms_router.go b/router/modules/sms_router.go index 0e2f60c..eb47969 100644 --- a/router/modules/sms_router.go +++ b/router/modules/sms_router.go @@ -9,7 +9,7 @@ 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) - group.GET("/test/send", smsApi.SendMessageTest) + group.POST("/ali/send", smsApi.SendMessageByAli) + group.POST("/smsbao/send", smsApi.SendMessageBySmsBao) + group.POST("/test/send", smsApi.SendMessageTest) } diff --git a/router/router.go b/router/router.go index 1e0b03c..69f4fbd 100644 --- a/router/router.go +++ b/router/router.go @@ -50,6 +50,7 @@ func InitRouter() *gin.Engine { middleware.SecurityHeaders(), middleware.JWTAuthMiddleware(), middleware.CasbinMiddleware(), + middleware.VerifySignature(), ) { modules.UserRouterAuth(authGroup) // 注册鉴权路由 diff --git a/utils/check_captcha.go b/utils/check_captcha.go index 8efbbf9..82cb2c1 100644 --- a/utils/check_captcha.go +++ b/utils/check_captcha.go @@ -38,8 +38,8 @@ func CheckSlideData(point []int64, key string) bool { } // CheckRotateData 校验旋转验证码 -func CheckRotateData(angle string, key string) bool { - if angle == "" || key == "" { +func CheckRotateData(angle int64, key string) bool { + if angle == 0 || key == "" { return false } cacheDataByte, err := redis.Get(constant.UserLoginCaptchaRedisKey + key).Bytes()