add api validation signature

This commit is contained in:
landaiqing
2024-09-28 00:20:13 +08:00
parent 03c5b92515
commit bd2e8b8f6c
14 changed files with 173 additions and 34 deletions

View File

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

View File

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

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

View File

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

View File

@@ -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 新增用户请求

View File

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

View File

@@ -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:"
)

View File

@@ -73,4 +73,5 @@ CommentLikeSuccess = "comment like success!"
CommentLikeFailed = "comment like failed!"
CommentDislikeSuccess = "comment dislike success!"
CommentDislikeFailed = "comment dislike failed!"
CaptchaVerifyError = "captcha error!"
CaptchaVerifyError = "captcha error!"
RequestVerifyError = "request verify error!"

View File

@@ -74,3 +74,4 @@ CommentLikeFailed = "评论点赞失败!"
CommentDislikeSuccess = "评论取消点赞成功!"
CommentDislikeFailed = "评论取消点赞失败!"
CaptchaVerifyError = "验证失败!"
RequestVerifyError = "请求验证失败!"

View 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) // 假设时间戳是毫秒
}

View File

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

View File

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

View File

@@ -50,6 +50,7 @@ func InitRouter() *gin.Engine {
middleware.SecurityHeaders(),
middleware.JWTAuthMiddleware(),
middleware.CasbinMiddleware(),
middleware.VerifySignature(),
)
{
modules.UserRouterAuth(authGroup) // 注册鉴权路由

View File

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