✨ add api validation signature
This commit is contained in:
@@ -15,3 +15,6 @@ type AddPermissionToRoleRequestDto struct {
|
|||||||
Permission string `json:"permission"`
|
Permission string `json:"permission"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
}
|
}
|
||||||
|
type GetPermissionRequest struct {
|
||||||
|
UserId string `json:"user_id" binding:"required"`
|
||||||
|
}
|
||||||
|
@@ -71,12 +71,13 @@ func (PermissionAPI) AssignPermissionsToRole(c *gin.Context) {
|
|||||||
|
|
||||||
// GetUserPermissions 获取用户角色权限
|
// GetUserPermissions 获取用户角色权限
|
||||||
func (PermissionAPI) GetUserPermissions(c *gin.Context) {
|
func (PermissionAPI) GetUserPermissions(c *gin.Context) {
|
||||||
userId := c.Query("user_id")
|
getPermissionRequest := dto.GetPermissionRequest{}
|
||||||
if userId == "" {
|
err := c.ShouldBindJSON(&getPermissionRequest)
|
||||||
result.FailWithMessage("user_id is required", c)
|
if err != nil {
|
||||||
|
global.LOG.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, err := global.Casbin.GetImplicitRolesForUser(userId)
|
data, err := global.Casbin.GetImplicitRolesForUser(getPermissionRequest.UserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.FailWithMessage("Get user permissions failed", c)
|
result.FailWithMessage("Get user permissions failed", c)
|
||||||
return
|
return
|
||||||
|
7
api/sms_api/dto/request_dto.go
Normal file
7
api/sms_api/dto/request_dto.go
Normal file
@@ -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"`
|
||||||
|
}
|
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/pkg6/go-sms/gateways"
|
"github.com/pkg6/go-sms/gateways"
|
||||||
"github.com/pkg6/go-sms/gateways/aliyun"
|
"github.com/pkg6/go-sms/gateways/aliyun"
|
||||||
"github.com/pkg6/go-sms/gateways/smsbao"
|
"github.com/pkg6/go-sms/gateways/smsbao"
|
||||||
|
"schisandra-cloud-album/api/sms_api/dto"
|
||||||
"schisandra-cloud-album/common/constant"
|
"schisandra-cloud-album/common/constant"
|
||||||
"schisandra-cloud-album/common/redis"
|
"schisandra-cloud-album/common/redis"
|
||||||
"schisandra-cloud-album/common/result"
|
"schisandra-cloud-album/common/result"
|
||||||
@@ -23,17 +24,23 @@ import (
|
|||||||
// @Param phone query string true "手机号"
|
// @Param phone query string true "手机号"
|
||||||
// @Router /api/sms/ali/send [get]
|
// @Router /api/sms/ali/send [get]
|
||||||
func (SmsAPI) SendMessageByAli(c *gin.Context) {
|
func (SmsAPI) SendMessageByAli(c *gin.Context) {
|
||||||
phone := c.Query("phone")
|
smsRequest := dto.SmsRequest{}
|
||||||
if phone == "" {
|
err := c.ShouldBindJSON(&smsRequest)
|
||||||
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneNotEmpty"), c)
|
if err != nil {
|
||||||
|
result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendFailed"), c)
|
||||||
return
|
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 {
|
if !isPhone {
|
||||||
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneErrorFormat"), c)
|
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneErrorFormat"), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val := redis.Get(constant.UserLoginSmsRedisKey + phone).Val()
|
val := redis.Get(constant.UserLoginSmsRedisKey + smsRequest.Phone).Val()
|
||||||
if val != "" {
|
if val != "" {
|
||||||
result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaTooOften"), c)
|
result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaTooOften"), c)
|
||||||
return
|
return
|
||||||
@@ -46,12 +53,12 @@ func (SmsAPI) SendMessageByAli(c *gin.Context) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
code := utils.GenValidateCode(6)
|
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 {
|
if wrong != nil {
|
||||||
global.LOG.Error(wrong)
|
global.LOG.Error(wrong)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err := sms.Send(phone, gosms.MapStringAny{
|
_, err = sms.Send(smsRequest.Phone, gosms.MapStringAny{
|
||||||
"content": "您的验证码是:****。请不要把验证码泄露给其他人。",
|
"content": "您的验证码是:****。请不要把验证码泄露给其他人。",
|
||||||
"template": global.CONFIG.SMS.Ali.TemplateID,
|
"template": global.CONFIG.SMS.Ali.TemplateID,
|
||||||
//"signName": global.CONFIG.SMS.Ali.Signature,
|
//"signName": global.CONFIG.SMS.Ali.Signature,
|
||||||
@@ -74,19 +81,25 @@ func (SmsAPI) SendMessageByAli(c *gin.Context) {
|
|||||||
// @Tags 短信验证码
|
// @Tags 短信验证码
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param phone query string true "手机号"
|
// @Param phone query string true "手机号"
|
||||||
// @Router /api/sms/smsbao/send [get]
|
// @Router /api/sms/smsbao/send [post]
|
||||||
func (SmsAPI) SendMessageBySmsBao(c *gin.Context) {
|
func (SmsAPI) SendMessageBySmsBao(c *gin.Context) {
|
||||||
phone := c.Query("phone")
|
smsRequest := dto.SmsRequest{}
|
||||||
if phone == "" {
|
err := c.ShouldBindJSON(&smsRequest)
|
||||||
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneNotEmpty"), c)
|
if err != nil {
|
||||||
|
result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendFailed"), c)
|
||||||
return
|
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 {
|
if !isPhone {
|
||||||
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneErrorFormat"), c)
|
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneErrorFormat"), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val := redis.Get(constant.UserLoginSmsRedisKey + phone).Val()
|
val := redis.Get(constant.UserLoginSmsRedisKey + smsRequest.Phone).Val()
|
||||||
if val != "" {
|
if val != "" {
|
||||||
result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaTooOften"), c)
|
result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaTooOften"), c)
|
||||||
return
|
return
|
||||||
@@ -98,12 +111,12 @@ func (SmsAPI) SendMessageBySmsBao(c *gin.Context) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
code := utils.GenValidateCode(6)
|
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 {
|
if wrong != nil {
|
||||||
global.LOG.Error(wrong)
|
global.LOG.Error(wrong)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err := sms.Send(phone, gosms.MapStringAny{
|
_, err = sms.Send(smsRequest.Phone, gosms.MapStringAny{
|
||||||
"content": "您的验证码是:" + code + "。请不要把验证码泄露给其他人。",
|
"content": "您的验证码是:" + code + "。请不要把验证码泄露给其他人。",
|
||||||
}, nil)
|
}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -120,20 +133,26 @@ func (SmsAPI) SendMessageBySmsBao(c *gin.Context) {
|
|||||||
// @Tags 短信验证码
|
// @Tags 短信验证码
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param phone query string true "手机号"
|
// @Param phone query string true "手机号"
|
||||||
// @Router /api/sms/test/send [get]
|
// @Router /api/sms/test/send [post]
|
||||||
func (SmsAPI) SendMessageTest(c *gin.Context) {
|
func (SmsAPI) SendMessageTest(c *gin.Context) {
|
||||||
phone := c.Query("phone")
|
smsRequest := dto.SmsRequest{}
|
||||||
if phone == "" {
|
err := c.ShouldBindJSON(&smsRequest)
|
||||||
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneNotEmpty"), c)
|
if err != nil {
|
||||||
|
result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendFailed"), c)
|
||||||
return
|
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 {
|
if !isPhone {
|
||||||
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneError"), c)
|
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneError"), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
code := utils.GenValidateCode(6)
|
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 {
|
if err != nil {
|
||||||
global.LOG.Error(err)
|
global.LOG.Error(err)
|
||||||
result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendFailed"), c)
|
result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaSendFailed"), c)
|
||||||
|
@@ -19,6 +19,8 @@ type AccountLoginRequest struct {
|
|||||||
Account string `json:"account" binding:"required"`
|
Account string `json:"account" binding:"required"`
|
||||||
Password string `json:"password" binding:"required"`
|
Password string `json:"password" binding:"required"`
|
||||||
AutoLogin bool `json:"auto_login" binding:"required"`
|
AutoLogin bool `json:"auto_login" binding:"required"`
|
||||||
|
Angle int64 `json:"angle" binding:"required"`
|
||||||
|
Key string `json:"key" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddUserRequest 新增用户请求
|
// AddUserRequest 新增用户请求
|
||||||
|
@@ -117,6 +117,11 @@ func (UserAPI) AccountLogin(c *gin.Context) {
|
|||||||
result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c)
|
result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
rotateData := utils.CheckRotateData(accountLoginRequest.Angle, accountLoginRequest.Key)
|
||||||
|
if !rotateData {
|
||||||
|
result.FailWithMessage(ginI18n.MustGetMessage(c, "CaptchaVerifyError"), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
account := accountLoginRequest.Account
|
account := accountLoginRequest.Account
|
||||||
password := accountLoginRequest.Password
|
password := accountLoginRequest.Password
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
package constant
|
package constant
|
||||||
|
|
||||||
|
// 登录相关的redis key
|
||||||
const (
|
const (
|
||||||
// 登录相关的redis key
|
|
||||||
UserLoginSmsRedisKey = "user:sms:"
|
UserLoginSmsRedisKey = "user:sms:"
|
||||||
UserLoginTokenRedisKey = "user:token:"
|
UserLoginTokenRedisKey = "user:token:"
|
||||||
UserLoginCaptchaRedisKey = "user:captcha:"
|
UserLoginCaptchaRedisKey = "user:captcha:"
|
||||||
@@ -10,8 +10,14 @@ const (
|
|||||||
UserSessionRedisKey = "user:session:"
|
UserSessionRedisKey = "user:session:"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 登录之后
|
// 评论相关的redis key
|
||||||
const (
|
const (
|
||||||
CommentSubmitCaptchaRedisKey = "comment:submit:captcha:"
|
CommentSubmitCaptchaRedisKey = "comment:submit:captcha:"
|
||||||
CommentOfflineMessageRedisKey = "comment:offline:message:"
|
CommentOfflineMessageRedisKey = "comment:offline:message:"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 系统相关的redis key
|
||||||
|
|
||||||
|
const (
|
||||||
|
SystemApiNonceRedisKey = "system:api:nonce:"
|
||||||
|
)
|
||||||
|
@@ -73,4 +73,5 @@ CommentLikeSuccess = "comment like success!"
|
|||||||
CommentLikeFailed = "comment like failed!"
|
CommentLikeFailed = "comment like failed!"
|
||||||
CommentDislikeSuccess = "comment dislike success!"
|
CommentDislikeSuccess = "comment dislike success!"
|
||||||
CommentDislikeFailed = "comment dislike failed!"
|
CommentDislikeFailed = "comment dislike failed!"
|
||||||
CaptchaVerifyError = "captcha error!"
|
CaptchaVerifyError = "captcha error!"
|
||||||
|
RequestVerifyError = "request verify error!"
|
@@ -74,3 +74,4 @@ CommentLikeFailed = "评论点赞失败!"
|
|||||||
CommentDislikeSuccess = "评论取消点赞成功!"
|
CommentDislikeSuccess = "评论取消点赞成功!"
|
||||||
CommentDislikeFailed = "评论取消点赞失败!"
|
CommentDislikeFailed = "评论取消点赞失败!"
|
||||||
CaptchaVerifyError = "验证失败!"
|
CaptchaVerifyError = "验证失败!"
|
||||||
|
RequestVerifyError = "请求验证失败!"
|
||||||
|
93
middleware/verify_signature.go
Normal file
93
middleware/verify_signature.go
Normal file
@@ -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) // 假设时间戳是毫秒
|
||||||
|
}
|
@@ -11,6 +11,6 @@ func PermissionRouter(router *gin.RouterGroup) {
|
|||||||
group := router.Group("/auth/permission")
|
group := router.Group("/auth/permission")
|
||||||
{
|
{
|
||||||
group.POST("/add", permissionApi.AddPermissions)
|
group.POST("/add", permissionApi.AddPermissions)
|
||||||
group.GET("/get_user_permissions", permissionApi.GetUserPermissions)
|
group.POST("/get_user_permissions", permissionApi.GetUserPermissions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,7 @@ var smsApi = api.Api.SmsApi
|
|||||||
|
|
||||||
func SmsRouter(router *gin.RouterGroup) {
|
func SmsRouter(router *gin.RouterGroup) {
|
||||||
group := router.Group("/sms")
|
group := router.Group("/sms")
|
||||||
group.GET("/ali/send", smsApi.SendMessageByAli)
|
group.POST("/ali/send", smsApi.SendMessageByAli)
|
||||||
group.GET("/smsbao/send", smsApi.SendMessageBySmsBao)
|
group.POST("/smsbao/send", smsApi.SendMessageBySmsBao)
|
||||||
group.GET("/test/send", smsApi.SendMessageTest)
|
group.POST("/test/send", smsApi.SendMessageTest)
|
||||||
}
|
}
|
||||||
|
@@ -50,6 +50,7 @@ func InitRouter() *gin.Engine {
|
|||||||
middleware.SecurityHeaders(),
|
middleware.SecurityHeaders(),
|
||||||
middleware.JWTAuthMiddleware(),
|
middleware.JWTAuthMiddleware(),
|
||||||
middleware.CasbinMiddleware(),
|
middleware.CasbinMiddleware(),
|
||||||
|
middleware.VerifySignature(),
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
modules.UserRouterAuth(authGroup) // 注册鉴权路由
|
modules.UserRouterAuth(authGroup) // 注册鉴权路由
|
||||||
|
@@ -38,8 +38,8 @@ func CheckSlideData(point []int64, key string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CheckRotateData 校验旋转验证码
|
// CheckRotateData 校验旋转验证码
|
||||||
func CheckRotateData(angle string, key string) bool {
|
func CheckRotateData(angle int64, key string) bool {
|
||||||
if angle == "" || key == "" {
|
if angle == 0 || key == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
cacheDataByte, err := redis.Get(constant.UserLoginCaptchaRedisKey + key).Bytes()
|
cacheDataByte, err := redis.Get(constant.UserLoginCaptchaRedisKey + key).Bytes()
|
||||||
|
Reference in New Issue
Block a user