add security headers

This commit is contained in:
landaiqing
2024-09-08 15:29:08 +08:00
parent c61c33880c
commit a60739d56d
15 changed files with 72 additions and 55 deletions

2
.gitignore vendored
View File

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

View File

@@ -3,7 +3,6 @@ package oauth_api
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/ArtisanCloud/PowerLibs/v3/fmt"
"github.com/ArtisanCloud/PowerLibs/v3/http/helper" "github.com/ArtisanCloud/PowerLibs/v3/http/helper"
"github.com/ArtisanCloud/PowerWeChat/v3/src/basicService/qrCode/response" "github.com/ArtisanCloud/PowerWeChat/v3/src/basicService/qrCode/response"
"github.com/ArtisanCloud/PowerWeChat/v3/src/kernel/contract" "github.com/ArtisanCloud/PowerWeChat/v3/src/kernel/contract"
@@ -60,7 +59,6 @@ func (OAuthAPI) CallbackNotify(c *gin.Context) {
println(err.Error()) println(err.Error())
return "error" return "error"
} }
fmt.Dump(msg)
return messages.NewText("ok") return messages.NewText("ok")
case models.CALLBACK_EVENT_SCAN: case models.CALLBACK_EVENT_SCAN:

View File

@@ -71,7 +71,7 @@ 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") userId := c.PostForm("user_id")
if userId == "" { if userId == "" {
result.FailWithMessage(ginI18n.MustGetMessage(c, "GetUserFailed"), c) result.FailWithMessage(ginI18n.MustGetMessage(c, "GetUserFailed"), c)
return return

View File

@@ -9,4 +9,5 @@ const (
UserLoginClientRedisKey = "user:login:client:" UserLoginClientRedisKey = "user:login:client:"
UserLoginQrcodeRedisKey = "user:login:qrcode:" UserLoginQrcodeRedisKey = "user:login:qrcode:"
UserLoginWechatRedisKey = "user:wechat:token:" UserLoginWechatRedisKey = "user:wechat:token:"
SystemApiNonceRedisKey = "system:api:nonce:"
) )

View File

@@ -16,6 +16,11 @@ func Set(key string, value interface{}, expiration time.Duration) *redis.StatusC
return global.REDIS.Set(ctx, key, value, expiration) 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值 // Get 查询数据库中名称为key的value值
func Get(key string) *redis.StringCmd { func Get(key string) *redis.StringCmd {
return global.REDIS.Get(ctx, key) return global.REDIS.Get(ctx, key)

View File

@@ -5,5 +5,4 @@ type Wechat struct {
AppSecret string `yaml:"app-secret"` AppSecret string `yaml:"app-secret"`
Token string `yaml:"token"` Token string `yaml:"token"`
AESKey string `yaml:"aes-key"` AESKey string `yaml:"aes-key"`
OpenID string `yaml:"openid"`
} }

1
go.mod
View File

@@ -13,7 +13,6 @@ require (
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 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/juju/ratelimit v1.0.2
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20240510055607-89e20ab7b6c6 github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20240510055607-89e20ab7b6c6
github.com/lxzan/gws v1.8.5 github.com/lxzan/gws v1.8.5

2
go.sum
View File

@@ -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/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/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/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.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/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=

View File

@@ -58,6 +58,8 @@ AssignFailed = "assign failed!"
AssignSuccess = "assign successfully!" AssignSuccess = "assign successfully!"
DuplicateLogin = "duplicate login!" DuplicateLogin = "duplicate login!"
PermissionDenied = "permission denied!" PermissionDenied = "permission denied!"
LogoutFailed = "logout failed!"
LogoutSuccess = "logout successfully!"
SystemError = "system error, please contact the administrator!" SystemError = "system error, please contact the administrator!"
RequestLimit = "request limit!"
404NotFound = "404 not found!"
PermissionVerifyFailed = "permission verify failed!"
IllegalRequests = "illegal requests!"

View File

@@ -58,6 +58,8 @@ AssignFailed = "分配失败!"
AssignSuccess = "分配成功!" AssignSuccess = "分配成功!"
DuplicateLogin = "重复登录!" DuplicateLogin = "重复登录!"
PermissionDenied = "权限不足!" PermissionDenied = "权限不足!"
LogoutFailed = "登出失败!"
LogoutSuccess = "登出成功!"
SystemError = "系统错误!,请联系管理员!" SystemError = "系统错误!,请联系管理员!"
RequestLimit = "请求频率过高!"
404NotFound = "未找到资源!"
PermissionVerifyFailed = "权限验证失败!"
IllegalRequests = "非法请求!"

View File

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

View File

@@ -1,6 +1,7 @@
package middleware package middleware
import ( import (
ginI18n "github.com/gin-contrib/i18n"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/juju/ratelimit" "github.com/juju/ratelimit"
"schisandra-cloud-album/common/result" "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) { return func(c *gin.Context) {
// 如果取不到令牌就中断本次请求返回 rate limit... // 如果取不到令牌就中断本次请求返回 rate limit...
if bucket.TakeAvailable(1) < 1 { if bucket.TakeAvailable(1) < 1 {
result.FailWithMessage("rate limit...", c) result.FailWithMessage(ginI18n.MustGetMessage(c, "RequestLimit"), c)
c.Abort() c.Abort()
return return
} }

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

View File

@@ -0,0 +1,12 @@
package middleware
import (
"github.com/gin-gonic/gin"
)
func ValidateSignMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
}
}

View File

@@ -2,8 +2,10 @@ package router
import ( import (
"github.com/gin-contrib/cors" "github.com/gin-contrib/cors"
ginI18n "github.com/gin-contrib/i18n"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"schisandra-cloud-album/api" "schisandra-cloud-album/api"
"schisandra-cloud-album/common/result"
"schisandra-cloud-album/global" "schisandra-cloud-album/global"
"schisandra-cloud-album/middleware" "schisandra-cloud-album/middleware"
"schisandra-cloud-album/router/modules" "schisandra-cloud-album/router/modules"
@@ -15,6 +17,8 @@ var oauth = api.Api.OAuthApi
func InitRouter() *gin.Engine { func InitRouter() *gin.Engine {
gin.SetMode(global.CONFIG.System.Env) gin.SetMode(global.CONFIG.System.Env)
router := gin.Default() router := gin.Default()
router.NoRoute(HandleNotFound)
router.NoMethod(HandleNotFound)
err := router.SetTrustedProxies([]string{global.CONFIG.System.Ip}) err := router.SetTrustedProxies([]string{global.CONFIG.System.Ip})
if err != nil { if err != nil {
global.LOG.Error(err) global.LOG.Error(err)
@@ -25,12 +29,13 @@ func InitRouter() *gin.Engine {
router.Use(cors.New(cors.Config{ router.Use(cors.New(cors.Config{
AllowOrigins: []string{global.CONFIG.System.Web}, AllowOrigins: []string{global.CONFIG.System.Web},
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"}, 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, AllowCredentials: true,
MaxAge: 12 * time.Hour, MaxAge: 12 * time.Hour,
})) }))
// 国际化设置 // 国际化设置
router.Use(middleware.I18n(), middleware.ExceptionNotification()) router.Use(middleware.I18n(), middleware.ValidateSignMiddleware())
router.Use(middleware.SecurityHeaders())
publicGroup := router.Group("api") // 不需要鉴权的路由组 publicGroup := router.Group("api") // 不需要鉴权的路由组
{ {
@@ -55,3 +60,9 @@ func InitRouter() *gin.Engine {
return router return router
} }
// HandleNotFound 404处理
func HandleNotFound(c *gin.Context) {
result.FailWithCodeAndMessage(404, ginI18n.MustGetMessage(c, "404NotFound"), c)
return
}