feat: 合并Gin_Vue
This commit is contained in:
182
src/framework/middleware/collectlogs/operate_log.go
Normal file
182
src/framework/middleware/collectlogs/operate_log.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package collectlogs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/constants/common"
|
||||
"ems.agt/src/framework/utils/ctx"
|
||||
"ems.agt/src/framework/utils/parse"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
"ems.agt/src/modules/system/model"
|
||||
"ems.agt/src/modules/system/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
// 业务操作类型-其它
|
||||
BUSINESS_TYPE_OTHER = "0"
|
||||
|
||||
// 业务操作类型-新增
|
||||
BUSINESS_TYPE_INSERT = "1"
|
||||
|
||||
// 业务操作类型-修改
|
||||
BUSINESS_TYPE_UPDATE = "2"
|
||||
|
||||
// 业务操作类型-删除
|
||||
BUSINESS_TYPE_DELETE = "3"
|
||||
|
||||
// 业务操作类型-授权
|
||||
BUSINESS_TYPE_GRANT = "4"
|
||||
|
||||
// 业务操作类型-导出
|
||||
BUSINESS_TYPE_EXPORT = "5"
|
||||
|
||||
// 业务操作类型-导入
|
||||
BUSINESS_TYPE_IMPORT = "6"
|
||||
|
||||
// 业务操作类型-强退
|
||||
BUSINESS_TYPE_FORCE = "7"
|
||||
|
||||
// 业务操作类型-清空数据
|
||||
BUSINESS_TYPE_CLEAN = "8"
|
||||
)
|
||||
|
||||
const (
|
||||
// 操作人类别-其它
|
||||
OPERATOR_TYPE_OTHER = "0"
|
||||
|
||||
// 操作人类别-后台用户
|
||||
OPERATOR_TYPE_MANAGE = "1"
|
||||
|
||||
// 操作人类别-手机端用户
|
||||
OPERATOR_TYPE_MOBILE = "2"
|
||||
)
|
||||
|
||||
// Option 操作日志参数
|
||||
type Options struct {
|
||||
Title string `json:"title"` // 标题
|
||||
BusinessType string `json:"businessType"` // 类型,默认常量 BUSINESS_TYPE_OTHER
|
||||
OperatorType string `json:"operatorType"` // 操作人类别,默认常量 OPERATOR_TYPE_OTHER
|
||||
IsSaveRequestData bool `json:"isSaveRequestData"` // 是否保存请求的参数
|
||||
IsSaveResponseData bool `json:"isSaveResponseData"` // 是否保存响应的参数
|
||||
}
|
||||
|
||||
// OptionNew 操作日志参数默认值
|
||||
//
|
||||
// 标题 "title":"--"
|
||||
//
|
||||
// 类型 "businessType": BUSINESS_TYPE_OTHER
|
||||
//
|
||||
// 注意之后JSON反序列使用:c.ShouldBindBodyWith(¶ms, binding.JSON)
|
||||
func OptionNew(title, businessType string) Options {
|
||||
return Options{
|
||||
Title: title,
|
||||
BusinessType: businessType,
|
||||
OperatorType: OPERATOR_TYPE_OTHER,
|
||||
IsSaveRequestData: true,
|
||||
IsSaveResponseData: true,
|
||||
}
|
||||
}
|
||||
|
||||
// 敏感属性字段进行掩码
|
||||
var maskProperties []string = []string{
|
||||
"password",
|
||||
"oldPassword",
|
||||
"newPassword",
|
||||
"confirmPassword",
|
||||
}
|
||||
|
||||
// OperateLog 访问操作日志记录
|
||||
//
|
||||
// 请在用户身份授权认证校验后使用以便获取登录用户信息
|
||||
func OperateLog(options Options) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Set("startTime", time.Now())
|
||||
|
||||
// 函数名
|
||||
funcName := c.HandlerName()
|
||||
lastDotIndex := strings.LastIndex(funcName, "/")
|
||||
funcName = funcName[lastDotIndex+1:]
|
||||
|
||||
// 解析ip地址
|
||||
ipaddr, location := ctx.IPAddrLocation(c)
|
||||
|
||||
// 获取登录用户信息
|
||||
loginUser, err := ctx.LoginUser(c)
|
||||
if err != nil {
|
||||
c.JSON(401, result.CodeMsg(401, "无效身份授权"))
|
||||
c.Abort() // 停止执行后续的处理函数
|
||||
return
|
||||
}
|
||||
|
||||
// 操作日志记录
|
||||
operLog := model.SysLogOperate{
|
||||
Title: options.Title,
|
||||
BusinessType: options.BusinessType,
|
||||
OperatorType: options.OperatorType,
|
||||
Method: funcName,
|
||||
OperURL: c.Request.RequestURI,
|
||||
RequestMethod: c.Request.Method,
|
||||
OperIP: ipaddr,
|
||||
OperLocation: location,
|
||||
OperName: loginUser.User.UserName,
|
||||
DeptName: loginUser.User.Dept.DeptName,
|
||||
}
|
||||
|
||||
if loginUser.User.UserType == "sys" {
|
||||
operLog.OperatorType = OPERATOR_TYPE_MANAGE
|
||||
}
|
||||
|
||||
// 是否需要保存request,参数和值
|
||||
if options.IsSaveRequestData {
|
||||
params := ctx.RequestParamsMap(c)
|
||||
for k, v := range params {
|
||||
// 敏感属性字段进行掩码
|
||||
for _, s := range maskProperties {
|
||||
if s == k {
|
||||
params[k] = parse.SafeContent(v.(string))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
jsonStr, _ := json.Marshal(params)
|
||||
paramsStr := string(jsonStr)
|
||||
if len(paramsStr) > 2000 {
|
||||
paramsStr = paramsStr[:2000]
|
||||
}
|
||||
operLog.OperParam = paramsStr
|
||||
}
|
||||
|
||||
// 调用下一个处理程序
|
||||
c.Next()
|
||||
|
||||
// 响应状态
|
||||
status := c.Writer.Status()
|
||||
if status == 200 {
|
||||
operLog.Status = common.STATUS_YES
|
||||
} else {
|
||||
operLog.Status = common.STATUS_NO
|
||||
}
|
||||
|
||||
// 是否需要保存response,参数和值
|
||||
if options.IsSaveResponseData {
|
||||
contentDisposition := c.Writer.Header().Get("Content-Disposition")
|
||||
contentType := c.Writer.Header().Get("Content-Type")
|
||||
content := contentType + contentDisposition
|
||||
msg := fmt.Sprintf(`{"status":"%d","size":"%d","content-type":"%s"}`, status, c.Writer.Size(), content)
|
||||
operLog.OperMsg = msg
|
||||
}
|
||||
|
||||
// 日志记录时间
|
||||
duration := time.Since(c.GetTime("startTime"))
|
||||
operLog.CostTime = duration.Milliseconds()
|
||||
operLog.OperTime = time.Now().UnixMilli()
|
||||
|
||||
// 保存操作记录到数据库
|
||||
service.NewSysLogOperateImpl.InsertSysLogOperate(operLog)
|
||||
}
|
||||
}
|
||||
83
src/framework/middleware/cors.go
Normal file
83
src/framework/middleware/cors.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Cors 跨域
|
||||
func Cors() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 设置Vary头部
|
||||
c.Header("Vary", "Origin")
|
||||
c.Header("Keep-Alive", "timeout=5")
|
||||
|
||||
requestOrigin := c.GetHeader("Origin")
|
||||
if requestOrigin == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
origin := requestOrigin
|
||||
if v := config.Get("cors.origin"); v != nil {
|
||||
origin = v.(string)
|
||||
}
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
|
||||
if v := config.Get("cors.credentials"); v != nil && v.(bool) {
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
// OPTIONS
|
||||
if method := c.Request.Method; method == "OPTIONS" {
|
||||
requestMethod := c.GetHeader("Access-Control-Request-Method")
|
||||
if requestMethod == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 响应最大时间值
|
||||
if v := config.Get("cors.maxAge"); v != nil && v.(int) > 10000 {
|
||||
c.Header("Access-Control-Max-Age", fmt.Sprint(v))
|
||||
}
|
||||
|
||||
// 允许方法
|
||||
if v := config.Get("cors.allowMethods"); v != nil {
|
||||
var allowMethods = make([]string, 0)
|
||||
for _, s := range v.([]any) {
|
||||
allowMethods = append(allowMethods, s.(string))
|
||||
}
|
||||
c.Header("Access-Control-Allow-Methods", strings.Join(allowMethods, ","))
|
||||
} else {
|
||||
c.Header("Access-Control-Allow-Methods", "GET,HEAD,PUT,POST,DELETE,PATCH")
|
||||
}
|
||||
|
||||
// 允许请求头
|
||||
if v := config.Get("cors.allowHeaders"); v != nil {
|
||||
var allowHeaders = make([]string, 0)
|
||||
for _, s := range v.([]any) {
|
||||
allowHeaders = append(allowHeaders, s.(string))
|
||||
}
|
||||
c.Header("Access-Control-Allow-Headers", strings.Join(allowHeaders, ","))
|
||||
}
|
||||
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
// 暴露请求头
|
||||
if v := config.Get("cors.exposeHeaders"); v != nil {
|
||||
var exposeHeaders = make([]string, 0)
|
||||
for _, s := range v.([]any) {
|
||||
exposeHeaders = append(exposeHeaders, s.(string))
|
||||
}
|
||||
c.Header("Access-Control-Expose-Headers", strings.Join(exposeHeaders, ","))
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
180
src/framework/middleware/pre_authorize.go
Normal file
180
src/framework/middleware/pre_authorize.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
AdminConstants "ems.agt/src/framework/constants/admin"
|
||||
commonConstants "ems.agt/src/framework/constants/common"
|
||||
ctxUtils "ems.agt/src/framework/utils/ctx"
|
||||
tokenUtils "ems.agt/src/framework/utils/token"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PreAuthorize 用户身份授权认证校验
|
||||
//
|
||||
// 只需含有其中角色 "hasRoles": {"xxx"},
|
||||
//
|
||||
// 只需含有其中权限 "hasPerms": {"xxx"},
|
||||
//
|
||||
// 同时匹配其中角色 "matchRoles": {"xxx"},
|
||||
//
|
||||
// 同时匹配其中权限 "matchPerms": {"xxx"},
|
||||
func PreAuthorize(options map[string][]string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 获取请求头标识信息
|
||||
tokenStr := ctxUtils.Authorization(c)
|
||||
if tokenStr == "" {
|
||||
c.JSON(401, result.CodeMsg(401, "无效身份授权"))
|
||||
c.Abort() // 停止执行后续的处理函数
|
||||
return
|
||||
}
|
||||
|
||||
// 验证令牌
|
||||
claims, err := tokenUtils.Verify(tokenStr)
|
||||
if err != nil {
|
||||
c.JSON(401, result.CodeMsg(401, err.Error()))
|
||||
c.Abort() // 停止执行后续的处理函数
|
||||
return
|
||||
}
|
||||
|
||||
// 获取缓存的用户信息
|
||||
loginUser := tokenUtils.LoginUser(claims)
|
||||
if loginUser.UserID == "" {
|
||||
c.JSON(401, result.CodeMsg(401, "无效身份授权"))
|
||||
c.Abort() // 停止执行后续的处理函数
|
||||
return
|
||||
}
|
||||
|
||||
// 检查刷新有效期后存入上下文
|
||||
tokenUtils.RefreshIn(&loginUser)
|
||||
c.Set(commonConstants.CTX_LOGIN_USER, loginUser)
|
||||
|
||||
// 登录用户角色权限校验
|
||||
if options != nil {
|
||||
var roles []string
|
||||
for _, item := range loginUser.User.Roles {
|
||||
roles = append(roles, item.RoleKey)
|
||||
}
|
||||
perms := loginUser.Permissions
|
||||
verifyOk := verifyRolePermission(roles, perms, options)
|
||||
if !verifyOk {
|
||||
msg := fmt.Sprintf("无权访问 %s %s", c.Request.Method, c.Request.RequestURI)
|
||||
c.JSON(403, result.CodeMsg(403, msg))
|
||||
c.Abort() // 停止执行后续的处理函数
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 调用下一个处理程序
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// verifyRolePermission 校验角色权限是否满足
|
||||
//
|
||||
// roles 角色字符数组
|
||||
//
|
||||
// perms 权限字符数组
|
||||
//
|
||||
// options 参数
|
||||
func verifyRolePermission(roles, perms []string, options map[string][]string) bool {
|
||||
// 直接放行 管理员角色或任意权限
|
||||
if contains(roles, AdminConstants.ROLE_KEY) || contains(perms, AdminConstants.PERMISSION) {
|
||||
return true
|
||||
}
|
||||
opts := make([]bool, 4)
|
||||
|
||||
// 只需含有其中角色
|
||||
hasRole := false
|
||||
if arr, ok := options["hasRoles"]; ok && len(arr) > 0 {
|
||||
hasRole = some(roles, arr)
|
||||
opts[0] = true
|
||||
}
|
||||
|
||||
// 只需含有其中权限
|
||||
hasPerms := false
|
||||
if arr, ok := options["hasPerms"]; ok && len(arr) > 0 {
|
||||
hasPerms = some(perms, arr)
|
||||
opts[1] = true
|
||||
}
|
||||
|
||||
// 同时匹配其中角色
|
||||
matchRoles := false
|
||||
if arr, ok := options["matchRoles"]; ok && len(arr) > 0 {
|
||||
matchRoles = every(roles, arr)
|
||||
opts[2] = true
|
||||
}
|
||||
|
||||
// 同时匹配其中权限
|
||||
matchPerms := false
|
||||
if arr, ok := options["matchPerms"]; ok && len(arr) > 0 {
|
||||
matchPerms = every(perms, arr)
|
||||
opts[3] = true
|
||||
}
|
||||
|
||||
// 同时判断 含有其中
|
||||
if opts[0] && opts[1] {
|
||||
return hasRole || hasPerms
|
||||
}
|
||||
// 同时判断 匹配其中
|
||||
if opts[2] && opts[3] {
|
||||
return matchRoles && matchPerms
|
||||
}
|
||||
// 同时判断 含有其中且匹配其中
|
||||
if opts[0] && opts[3] {
|
||||
return hasRole && matchPerms
|
||||
}
|
||||
if opts[1] && opts[2] {
|
||||
return hasPerms && matchRoles
|
||||
}
|
||||
|
||||
return hasRole || hasPerms || matchRoles || matchPerms
|
||||
}
|
||||
|
||||
// contains 检查字符串数组中是否包含指定的字符串
|
||||
func contains(arr []string, target string) bool {
|
||||
for _, str := range arr {
|
||||
if str == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// some 检查字符串数组中含有其中一项
|
||||
func some(origin []string, target []string) bool {
|
||||
has := false
|
||||
for _, t := range target {
|
||||
for _, o := range origin {
|
||||
if t == o {
|
||||
has = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if has {
|
||||
break
|
||||
}
|
||||
}
|
||||
return has
|
||||
}
|
||||
|
||||
// every 检查字符串数组中同时包含所有项
|
||||
func every(origin []string, target []string) bool {
|
||||
match := true
|
||||
for _, t := range target {
|
||||
found := false
|
||||
for _, o := range origin {
|
||||
if t == o {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return match
|
||||
}
|
||||
101
src/framework/middleware/rate_limit.go
Normal file
101
src/framework/middleware/rate_limit.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/constants/cachekey"
|
||||
"ems.agt/src/framework/redis"
|
||||
"ems.agt/src/framework/utils/ctx"
|
||||
"ems.agt/src/framework/utils/ip2region"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
// 默认策略全局限流
|
||||
LIMIT_GLOBAL = 1
|
||||
|
||||
// 根据请求者IP进行限流
|
||||
LIMIT_IP = 2
|
||||
|
||||
// 根据用户ID进行限流
|
||||
LIMIT_USER = 3
|
||||
)
|
||||
|
||||
// LimitOption 请求限流参数
|
||||
type LimitOption struct {
|
||||
Time int64 `json:"time"` // 限流时间,单位秒
|
||||
Count int64 `json:"count"` // 限流次数
|
||||
Type int64 `json:"type"` // 限流条件类型,默认LIMIT_GLOBAL
|
||||
}
|
||||
|
||||
// RateLimit 请求限流
|
||||
//
|
||||
// 示例参数:middleware.LimitOption{ Time: 5, Count: 10, Type: middleware.LIMIT_IP }
|
||||
//
|
||||
// 参数表示:5秒内,最多请求10次,限制类型为 IP
|
||||
//
|
||||
// 使用 USER 时,请在用户身份授权认证校验后使用
|
||||
// 以便获取登录用户信息,无用户信息时默认为 GLOBAL
|
||||
func RateLimit(option LimitOption) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 初始可选参数数据
|
||||
if option.Time < 5 {
|
||||
option.Time = 5
|
||||
}
|
||||
if option.Count < 10 {
|
||||
option.Count = 10
|
||||
}
|
||||
if option.Type == 0 {
|
||||
option.Type = LIMIT_GLOBAL
|
||||
}
|
||||
|
||||
// 获取执行函数名称
|
||||
funcName := c.HandlerName()
|
||||
lastDotIndex := strings.LastIndex(funcName, "/")
|
||||
funcName = funcName[lastDotIndex+1:]
|
||||
// 生成限流key
|
||||
var limitKey string = cachekey.RATE_LIMIT_KEY + funcName
|
||||
|
||||
// 用户
|
||||
if option.Type == LIMIT_USER {
|
||||
loginUser, err := ctx.LoginUser(c)
|
||||
if err != nil {
|
||||
c.JSON(401, result.Err(map[string]any{
|
||||
"code": 401,
|
||||
"msg": err.Error(),
|
||||
}))
|
||||
c.Abort() // 停止执行后续的处理函数
|
||||
return
|
||||
}
|
||||
limitKey = cachekey.RATE_LIMIT_KEY + loginUser.UserID + ":" + funcName
|
||||
}
|
||||
|
||||
// IP
|
||||
if option.Type == LIMIT_IP {
|
||||
clientIP := ip2region.ClientIP(c.ClientIP())
|
||||
limitKey = cachekey.RATE_LIMIT_KEY + clientIP + ":" + funcName
|
||||
}
|
||||
|
||||
// 在Redis查询并记录请求次数
|
||||
rateCount, _ := redis.RateLimit("", limitKey, option.Time, option.Count)
|
||||
rateTime, _ := redis.GetExpire("", limitKey)
|
||||
|
||||
// 设置响应头中的限流声明字段
|
||||
c.Header("X-RateLimit-Limit", fmt.Sprintf("%d", option.Count)) // 总请求数限制
|
||||
c.Header("X-RateLimit-Remaining", fmt.Sprintf("%d", option.Count-rateCount)) // 剩余可用请求数
|
||||
c.Header("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Unix()+int64(rateTime))) // 重置时间戳
|
||||
|
||||
if rateCount >= option.Count {
|
||||
c.JSON(200, result.ErrMsg("访问过于频繁,请稍候再试"))
|
||||
c.Abort() // 停止执行后续的处理函数
|
||||
return
|
||||
}
|
||||
|
||||
// 调用下一个处理程序
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
84
src/framework/middleware/repeat/repeat.go
Normal file
84
src/framework/middleware/repeat/repeat.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package repeat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/constants/cachekey"
|
||||
"ems.agt/src/framework/logger"
|
||||
"ems.agt/src/framework/redis"
|
||||
"ems.agt/src/framework/utils/ctx"
|
||||
"ems.agt/src/framework/utils/ip2region"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// repeatParam 重复提交参数的类型定义
|
||||
type repeatParam struct {
|
||||
Time int64 `json:"time"`
|
||||
Params string `json:"params"`
|
||||
}
|
||||
|
||||
// RepeatSubmit 防止表单重复提交,小于间隔时间视为重复提交
|
||||
//
|
||||
// 间隔时间(单位秒) 默认:5
|
||||
//
|
||||
// 注意之后JSON反序列使用:c.ShouldBindBodyWith(¶ms, binding.JSON)
|
||||
func RepeatSubmit(interval int64) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if interval < 5 {
|
||||
interval = 5
|
||||
}
|
||||
|
||||
// 提交参数
|
||||
params := ctx.RequestParamsMap(c)
|
||||
paramsJSONByte, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
logger.Errorf("RepeatSubmit params json marshal err: %v", err)
|
||||
}
|
||||
paramsJSONStr := string(paramsJSONByte)
|
||||
|
||||
// 唯一标识(指定key + 客户端IP + 请求地址)
|
||||
clientIP := ip2region.ClientIP(c.ClientIP())
|
||||
repeatKey := cachekey.REPEAT_SUBMIT_KEY + clientIP + ":" + c.Request.RequestURI
|
||||
|
||||
// 在Redis查询并记录请求次数
|
||||
repeatStr, _ := redis.Get("", repeatKey)
|
||||
if repeatStr != "" {
|
||||
var rp repeatParam
|
||||
err := json.Unmarshal([]byte(repeatStr), &rp)
|
||||
if err != nil {
|
||||
logger.Errorf("RepeatSubmit repeatStr json unmarshal err: %v", err)
|
||||
}
|
||||
compareTime := time.Now().Unix() - rp.Time
|
||||
compareParams := rp.Params == paramsJSONStr
|
||||
|
||||
// 设置重复提交声明响应头(毫秒)
|
||||
c.Header("X-RepeatSubmit-Rest", strconv.FormatInt(time.Now().Add(time.Duration(compareTime)*time.Second).UnixNano()/int64(time.Millisecond), 10))
|
||||
|
||||
// 小于间隔时间且参数内容一致
|
||||
if compareTime < interval && compareParams {
|
||||
c.JSON(200, result.ErrMsg("不允许重复提交,请稍候再试"))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 当前请求参数
|
||||
rp := repeatParam{
|
||||
Time: time.Now().Unix(),
|
||||
Params: paramsJSONStr,
|
||||
}
|
||||
rpJSON, err := json.Marshal(rp)
|
||||
if err != nil {
|
||||
logger.Errorf("RepeatSubmit rp json marshal err: %v", err)
|
||||
}
|
||||
// 保存请求时间和参数
|
||||
redis.SetByExpire("", repeatKey, string(rpJSON), time.Duration(interval)*time.Second)
|
||||
|
||||
// 调用下一个处理程序
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
23
src/framework/middleware/report.go
Normal file
23
src/framework/middleware/report.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"ems.agt/src/framework/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Report 请求响应日志
|
||||
func Report() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
|
||||
// 调用下一个处理程序
|
||||
c.Next()
|
||||
|
||||
// 计算请求处理时间,并打印日志
|
||||
duration := time.Since(start)
|
||||
logger.Infof("%s %s report end=> %v", c.Request.Method, c.Request.RequestURI, duration)
|
||||
}
|
||||
}
|
||||
22
src/framework/middleware/security/csp.go
Normal file
22
src/framework/middleware/security/csp.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/config"
|
||||
"ems.agt/src/framework/utils/generate"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// TODO
|
||||
// csp 这将帮助防止跨站脚本攻击(XSS)。
|
||||
// HTTP 响应头 Content-Security-Policy 允许站点管理者控制指定的页面加载哪些资源。
|
||||
func csp(c *gin.Context) {
|
||||
enable := false
|
||||
if v := config.Get("security.csp.enable"); v != nil {
|
||||
enable = v.(bool)
|
||||
}
|
||||
|
||||
if enable {
|
||||
c.Header("x-csp-nonce", generate.Code(8))
|
||||
}
|
||||
}
|
||||
37
src/framework/middleware/security/hsts.go
Normal file
37
src/framework/middleware/security/hsts.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// hsts 是一个安全功能 HTTP Strict Transport Security(通常简称为 HSTS )
|
||||
// 它告诉浏览器只能通过 HTTPS 访问当前资源,而不是 HTTP。
|
||||
func hsts(c *gin.Context) {
|
||||
enable := false
|
||||
if v := config.Get("security.hsts.enable"); v != nil {
|
||||
enable = v.(bool)
|
||||
}
|
||||
|
||||
maxAge := 365 * 24 * 3600
|
||||
if v := config.Get("security.hsts.maxAge"); v != nil {
|
||||
maxAge = v.(int)
|
||||
}
|
||||
|
||||
includeSubdomains := false
|
||||
if v := config.Get("security.hsts.includeSubdomains"); v != nil {
|
||||
includeSubdomains = v.(bool)
|
||||
}
|
||||
|
||||
str := fmt.Sprintf("max-age=%d", maxAge)
|
||||
if includeSubdomains {
|
||||
str += "; includeSubdomains"
|
||||
}
|
||||
|
||||
if enable {
|
||||
c.Header("strict-transport-security", str)
|
||||
}
|
||||
}
|
||||
20
src/framework/middleware/security/noopen.go
Normal file
20
src/framework/middleware/security/noopen.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// noopen 用于指定 IE 8 以上版本的用户不打开文件而直接保存文件。
|
||||
// 在下载对话框中不显式“打开”选项。
|
||||
func noopen(c *gin.Context) {
|
||||
enable := false
|
||||
if v := config.Get("security.noopen.enable"); v != nil {
|
||||
enable = v.(bool)
|
||||
}
|
||||
|
||||
if enable {
|
||||
c.Header("x-download-options", "noopen")
|
||||
}
|
||||
}
|
||||
26
src/framework/middleware/security/nosniff.go
Normal file
26
src/framework/middleware/security/nosniff.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// nosniff 用于防止 XSS 等跨站脚本攻击
|
||||
// 如果从 script 或 stylesheet 读入的文件的 MIME 类型与指定 MIME 类型不匹配,不允许读取该文件。
|
||||
func nosniff(c *gin.Context) {
|
||||
// 排除状态码范围
|
||||
status := c.Writer.Status()
|
||||
if status >= 300 && status <= 308 {
|
||||
return
|
||||
}
|
||||
|
||||
enable := false
|
||||
if v := config.Get("security.nosniff.enable"); v != nil {
|
||||
enable = v.(bool)
|
||||
}
|
||||
|
||||
if enable {
|
||||
c.Header("x-content-type-options", "nosniff")
|
||||
}
|
||||
}
|
||||
74
src/framework/middleware/security/referer.go
Normal file
74
src/framework/middleware/security/referer.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"ems.agt/src/framework/config"
|
||||
"ems.agt/src/framework/vo/result"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// referer 配置 referer 的 host 部分
|
||||
func referer(c *gin.Context) {
|
||||
enable := false
|
||||
if v := config.Get("security.csrf.enable"); v != nil {
|
||||
enable = v.(bool)
|
||||
}
|
||||
|
||||
// csrf 校验类型
|
||||
okType := false
|
||||
if v := config.Get("security.csrf.type"); v != nil {
|
||||
vType := v.(string)
|
||||
if vType == "all" || vType == "any" || vType == "referer" {
|
||||
okType = true
|
||||
}
|
||||
}
|
||||
if !okType {
|
||||
return
|
||||
}
|
||||
|
||||
// 忽略请求方法
|
||||
method := c.Request.Method
|
||||
ignoreMethods := []string{"GET", "HEAD", "OPTIONS", "TRACE"}
|
||||
for _, ignore := range ignoreMethods {
|
||||
if ignore == method {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
referer := c.GetHeader("Referer")
|
||||
if referer == "" {
|
||||
c.AbortWithStatusJSON(200, result.ErrMsg("无效 Referer 未知"))
|
||||
return
|
||||
}
|
||||
|
||||
// 获取host
|
||||
u, err := url.Parse(referer)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(200, result.ErrMsg("无效 Referer 未知"))
|
||||
return
|
||||
}
|
||||
host := u.Host
|
||||
|
||||
// 允许的来源白名单
|
||||
refererWhiteList := make([]string, 0)
|
||||
if v := config.Get("security.csrf.refererWhiteList"); v != nil {
|
||||
for _, s := range v.([]any) {
|
||||
refererWhiteList = append(refererWhiteList, s.(string))
|
||||
}
|
||||
}
|
||||
|
||||
if enable && okType {
|
||||
ok := false
|
||||
for _, domain := range refererWhiteList {
|
||||
if domain == host {
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(200, result.ErrMsg("无效 Referer "+host))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/framework/middleware/security/security.go
Normal file
23
src/framework/middleware/security/security.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Security 安全
|
||||
func Security() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 拦截,判断是否有效Referer
|
||||
referer(c)
|
||||
|
||||
// 无拦截,仅仅设置响应头
|
||||
xframe(c)
|
||||
csp(c)
|
||||
hsts(c)
|
||||
noopen(c)
|
||||
nosniff(c)
|
||||
xssProtection(c)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
26
src/framework/middleware/security/xframe.go
Normal file
26
src/framework/middleware/security/xframe.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// xframe 用来配置 X-Frame-Options 响应头
|
||||
// 用来给浏览器指示允许一个页面可否在 frame, iframe, embed 或者 object 中展现的标记。
|
||||
// 站点可以通过确保网站没有被嵌入到别人的站点里面,从而避免 clickjacking 攻击。
|
||||
func xframe(c *gin.Context) {
|
||||
enable := false
|
||||
if v := config.Get("security.xframe.enable"); v != nil {
|
||||
enable = v.(bool)
|
||||
}
|
||||
|
||||
value := "sameorigin"
|
||||
if v := config.Get("security.xframe.value"); v != nil {
|
||||
value = v.(string)
|
||||
}
|
||||
|
||||
if enable {
|
||||
c.Header("x-frame-options", value)
|
||||
}
|
||||
}
|
||||
24
src/framework/middleware/security/xss_protection.go
Normal file
24
src/framework/middleware/security/xss_protection.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"ems.agt/src/framework/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// xssProtection 用于启用浏览器的XSS过滤功能,以防止 XSS 跨站脚本攻击。
|
||||
func xssProtection(c *gin.Context) {
|
||||
enable := false
|
||||
if v := config.Get("security.xssProtection.enable"); v != nil {
|
||||
enable = v.(bool)
|
||||
}
|
||||
|
||||
value := "1; mode=block"
|
||||
if v := config.Get("security.xssProtection.value"); v != nil {
|
||||
value = v.(string)
|
||||
}
|
||||
|
||||
if enable {
|
||||
c.Header("x-xss-protection", value)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user