feat: 合并Gin_Vue

This commit is contained in:
TsMask
2023-10-16 17:10:38 +08:00
parent 5289818fd4
commit 40a32cb67f
203 changed files with 19719 additions and 178 deletions

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

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

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

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

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

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

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

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

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

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

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

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

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

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