✨ add security headers
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
1
go.mod
@@ -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
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/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=
|
||||||
|
|||||||
@@ -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!"
|
||||||
@@ -58,6 +58,8 @@ AssignFailed = "分配失败!"
|
|||||||
AssignSuccess = "分配成功!"
|
AssignSuccess = "分配成功!"
|
||||||
DuplicateLogin = "重复登录!"
|
DuplicateLogin = "重复登录!"
|
||||||
PermissionDenied = "权限不足!"
|
PermissionDenied = "权限不足!"
|
||||||
LogoutFailed = "登出失败!"
|
|
||||||
LogoutSuccess = "登出成功!"
|
|
||||||
SystemError = "系统错误!,请联系管理员!"
|
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
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
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 (
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user