✨ add security headers
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,8 +8,8 @@
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
.air.toml
|
||||
test
|
||||
tmp
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
@@ -3,7 +3,6 @@ package oauth_api
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/ArtisanCloud/PowerLibs/v3/fmt"
|
||||
"github.com/ArtisanCloud/PowerLibs/v3/http/helper"
|
||||
"github.com/ArtisanCloud/PowerWeChat/v3/src/basicService/qrCode/response"
|
||||
"github.com/ArtisanCloud/PowerWeChat/v3/src/kernel/contract"
|
||||
@@ -60,7 +59,6 @@ func (OAuthAPI) CallbackNotify(c *gin.Context) {
|
||||
println(err.Error())
|
||||
return "error"
|
||||
}
|
||||
fmt.Dump(msg)
|
||||
return messages.NewText("ok")
|
||||
|
||||
case models.CALLBACK_EVENT_SCAN:
|
||||
|
||||
@@ -71,7 +71,7 @@ func (PermissionAPI) AssignPermissionsToRole(c *gin.Context) {
|
||||
|
||||
// GetUserPermissions 获取用户角色权限
|
||||
func (PermissionAPI) GetUserPermissions(c *gin.Context) {
|
||||
userId := c.Query("user_id")
|
||||
userId := c.PostForm("user_id")
|
||||
if userId == "" {
|
||||
result.FailWithMessage(ginI18n.MustGetMessage(c, "GetUserFailed"), c)
|
||||
return
|
||||
|
||||
@@ -9,4 +9,5 @@ const (
|
||||
UserLoginClientRedisKey = "user:login:client:"
|
||||
UserLoginQrcodeRedisKey = "user:login:qrcode:"
|
||||
UserLoginWechatRedisKey = "user:wechat:token:"
|
||||
SystemApiNonceRedisKey = "system:api:nonce:"
|
||||
)
|
||||
|
||||
@@ -16,6 +16,11 @@ func Set(key string, value interface{}, expiration time.Duration) *redis.StatusC
|
||||
return global.REDIS.Set(ctx, key, value, expiration)
|
||||
}
|
||||
|
||||
// Exists 判断名称为key的key是否存在
|
||||
func Exists(key string) *redis.IntCmd {
|
||||
return global.REDIS.Exists(ctx, key)
|
||||
}
|
||||
|
||||
// Get 查询数据库中名称为key的value值
|
||||
func Get(key string) *redis.StringCmd {
|
||||
return global.REDIS.Get(ctx, key)
|
||||
|
||||
@@ -5,5 +5,4 @@ type Wechat struct {
|
||||
AppSecret string `yaml:"app-secret"`
|
||||
Token string `yaml:"token"`
|
||||
AESKey string `yaml:"aes-key"`
|
||||
OpenID string `yaml:"openid"`
|
||||
}
|
||||
|
||||
1
go.mod
1
go.mod
@@ -13,7 +13,6 @@ require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/juju/ratelimit v1.0.2
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20240510055607-89e20ab7b6c6
|
||||
github.com/lxzan/gws v1.8.5
|
||||
|
||||
2
go.sum
2
go.sum
@@ -122,8 +122,6 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
|
||||
@@ -58,6 +58,8 @@ AssignFailed = "assign failed!"
|
||||
AssignSuccess = "assign successfully!"
|
||||
DuplicateLogin = "duplicate login!"
|
||||
PermissionDenied = "permission denied!"
|
||||
LogoutFailed = "logout failed!"
|
||||
LogoutSuccess = "logout successfully!"
|
||||
SystemError = "system error, please contact the administrator!"
|
||||
RequestLimit = "request limit!"
|
||||
404NotFound = "404 not found!"
|
||||
PermissionVerifyFailed = "permission verify failed!"
|
||||
IllegalRequests = "illegal requests!"
|
||||
@@ -58,6 +58,8 @@ AssignFailed = "分配失败!"
|
||||
AssignSuccess = "分配成功!"
|
||||
DuplicateLogin = "重复登录!"
|
||||
PermissionDenied = "权限不足!"
|
||||
LogoutFailed = "登出失败!"
|
||||
LogoutSuccess = "登出成功!"
|
||||
SystemError = "系统错误!,请联系管理员!"
|
||||
RequestLimit = "请求频率过高!"
|
||||
404NotFound = "未找到资源!"
|
||||
PermissionVerifyFailed = "权限验证失败!"
|
||||
IllegalRequests = "非法请求!"
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/ArtisanCloud/PowerWeChat/v3/src/kernel/messages"
|
||||
ginI18n "github.com/gin-contrib/i18n"
|
||||
"github.com/gin-gonic/gin"
|
||||
"schisandra-cloud-album/common/result"
|
||||
"schisandra-cloud-album/global"
|
||||
"schisandra-cloud-album/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExceptionNotification 异常通知中间件
|
||||
func ExceptionNotification() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
openID := global.CONFIG.Wechat.OpenID
|
||||
content := `
|
||||
系统异常通知:
|
||||
请求时间:` + time.Now().Format("2006-01-02 15:04:05") + `
|
||||
请求IP:` + utils.GetClientIP(c) + `
|
||||
请求地址:` + c.Request.URL.String() + `
|
||||
请求方法:` + c.Request.Method + `
|
||||
请求参数:` + c.Request.Form.Encode() + `
|
||||
错误信息:` + err.(error).Error() + `
|
||||
`
|
||||
messages.NewRaw(`
|
||||
{
|
||||
"touser":"` + openID + `",
|
||||
"msgtype":"text",
|
||||
"text":{"content":"` + content + `"}"}
|
||||
}
|
||||
`)
|
||||
result.FailWithMessage(ginI18n.MustGetMessage(c, "SystemError"), c)
|
||||
}
|
||||
}()
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
ginI18n "github.com/gin-contrib/i18n"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/juju/ratelimit"
|
||||
"schisandra-cloud-album/common/result"
|
||||
@@ -12,7 +13,7 @@ func RateLimitMiddleware(fillInterval time.Duration, cap int64) func(c *gin.Cont
|
||||
return func(c *gin.Context) {
|
||||
// 如果取不到令牌就中断本次请求返回 rate limit...
|
||||
if bucket.TakeAvailable(1) < 1 {
|
||||
result.FailWithMessage("rate limit...", c)
|
||||
result.FailWithMessage(ginI18n.MustGetMessage(c, "RequestLimit"), c)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
29
middleware/security_headers.go
Normal file
29
middleware/security_headers.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
ginI18n "github.com/gin-contrib/i18n"
|
||||
"github.com/gin-gonic/gin"
|
||||
"schisandra-cloud-album/common/result"
|
||||
"schisandra-cloud-album/global"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func SecurityHeaders() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
url := strings.TrimPrefix(global.CONFIG.System.Web, "https://")
|
||||
requestHost := c.Request.Host
|
||||
if requestHost != url {
|
||||
result.FailWithMessage(ginI18n.MustGetMessage(c, "IllegalRequests"), c)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Header("X-Frame-Options", "DENY")
|
||||
c.Header("Content-Security-Policy", "default-src 'self'; connect-src *; font-src *; script-src-elem * 'unsafe-inline'; img-src * data:; style-src * 'unsafe-inline';")
|
||||
c.Header("X-XSS-Protection", "1; mode=block")
|
||||
c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
|
||||
c.Header("Referrer-Policy", "strict-origin")
|
||||
c.Header("X-Content-Type-Options", "nosniff")
|
||||
c.Header("Permissions-Policy", "geolocation=(),midi=(),sync-xhr=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=(self),payment=()")
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
12
middleware/validate_sign.go
Normal file
12
middleware/validate_sign.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func ValidateSignMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@ package router
|
||||
|
||||
import (
|
||||
"github.com/gin-contrib/cors"
|
||||
ginI18n "github.com/gin-contrib/i18n"
|
||||
"github.com/gin-gonic/gin"
|
||||
"schisandra-cloud-album/api"
|
||||
"schisandra-cloud-album/common/result"
|
||||
"schisandra-cloud-album/global"
|
||||
"schisandra-cloud-album/middleware"
|
||||
"schisandra-cloud-album/router/modules"
|
||||
@@ -15,6 +17,8 @@ var oauth = api.Api.OAuthApi
|
||||
func InitRouter() *gin.Engine {
|
||||
gin.SetMode(global.CONFIG.System.Env)
|
||||
router := gin.Default()
|
||||
router.NoRoute(HandleNotFound)
|
||||
router.NoMethod(HandleNotFound)
|
||||
err := router.SetTrustedProxies([]string{global.CONFIG.System.Ip})
|
||||
if err != nil {
|
||||
global.LOG.Error(err)
|
||||
@@ -25,12 +29,13 @@ func InitRouter() *gin.Engine {
|
||||
router.Use(cors.New(cors.Config{
|
||||
AllowOrigins: []string{global.CONFIG.System.Web},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"},
|
||||
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization", "Accept-Language"},
|
||||
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization", "Accept-Language", "X-Sign", "X-Timestamp", "X-Nonce"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 12 * time.Hour,
|
||||
}))
|
||||
// 国际化设置
|
||||
router.Use(middleware.I18n(), middleware.ExceptionNotification())
|
||||
router.Use(middleware.I18n(), middleware.ValidateSignMiddleware())
|
||||
router.Use(middleware.SecurityHeaders())
|
||||
|
||||
publicGroup := router.Group("api") // 不需要鉴权的路由组
|
||||
{
|
||||
@@ -55,3 +60,9 @@ func InitRouter() *gin.Engine {
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// HandleNotFound 404处理
|
||||
func HandleNotFound(c *gin.Context) {
|
||||
result.FailWithCodeAndMessage(404, ginI18n.MustGetMessage(c, "404NotFound"), c)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user