feat: 重建 Sense 认证权限与审计 (#64)

This commit is contained in:
QiuSW
2026-08-14 12:03:10 +08:00
parent c515f00319
commit 0411aa37c5
32 changed files with 509 additions and 123 deletions
+4 -4
View File
@@ -3,9 +3,9 @@ package middleware
import (
"time"
"git.ilapage.cn/ila/yovision/Sense/server/common/middleware/handler"
"github.com/go-admin-team/go-admin-core/sdk/config"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
"git.ilapage.cn/ila/yovision/Sense/server/common/middleware/handler"
)
// AuthInit jwt验证new
@@ -19,7 +19,7 @@ func AuthInit() (*jwt.GinJWTMiddleware, error) {
}
}
return jwt.New(&jwt.GinJWTMiddleware{
Realm: "test zone",
Realm: "Sense",
Key: []byte(config.JwtConfig.Secret),
Timeout: timeout,
MaxRefresh: time.Hour,
@@ -28,9 +28,9 @@ func AuthInit() (*jwt.GinJWTMiddleware, error) {
Authenticator: handler.Authenticator,
Authorizator: handler.Authorizator,
Unauthorized: handler.Unauthorized,
TokenLookup: "header: Authorization, query: token, cookie: jwt",
TokenLookup: "header: Authorization, cookie: sense_session",
TokenHeadName: "Bearer",
TimeFunc: time.Now,
})
}
}
+37 -28
View File
@@ -6,7 +6,6 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk"
"github.com/go-admin-team/go-admin-core/sdk/api"
"github.com/go-admin-team/go-admin-core/sdk/config"
"github.com/go-admin-team/go-admin-core/sdk/pkg"
@@ -15,7 +14,6 @@ import (
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
"github.com/go-admin-team/go-admin-core/sdk/pkg/response"
"github.com/mssola/user_agent"
"git.ilapage.cn/ila/yovision/Sense/server/common/global"
)
func PayloadFunc(data interface{}) jwt.MapClaims {
@@ -28,7 +26,7 @@ func PayloadFunc(data interface{}) jwt.MapClaims {
jwt.RoleKey: r.RoleKey,
jwt.NiceKey: u.Username,
jwt.DataScopeKey: r.DataScope,
jwt.RoleNameKey: r.RoleName,
jwt.RoleNameKey: r.RoleKey,
}
}
return jwt.MapClaims{}
@@ -108,36 +106,44 @@ func Authenticator(c *gin.Context) (interface{}, error) {
// LoginLogToDB Write log to database
func LoginLogToDB(c *gin.Context, status string, msg string, username string) {
if !config.LoggerConfig.EnabledDB {
WriteIdentityAudit(c, status, msg, username)
}
// WriteIdentityAudit persists identity/security events synchronously. It never
// records request bodies, credentials, tokens, captcha answers, or cookies.
func WriteIdentityAudit(c *gin.Context, status string, msg string, username string) {
log := api.GetRequestLogger(c)
db, err := pkg.GetOrm(c)
if err != nil {
log.Errorf("identity audit database unavailable: %s", err.Error())
return
}
log := api.GetRequestLogger(c)
l := make(map[string]interface{})
ua := user_agent.New(c.Request.UserAgent())
l["ipaddr"] = common.GetClientIP(c)
l["loginLocation"] = "" // pkg.GetLocation(common.GetClientIP(c),gaConfig.ExtConfig.AMap.Key)
l["loginTime"] = pkg.GetCurrentTime()
l["status"] = status
l["remark"] = c.Request.UserAgent()
browserName, browserVersion := ua.Browser()
l["browser"] = browserName + " " + browserVersion
l["os"] = ua.OS()
l["platform"] = ua.Platform()
l["username"] = username
l["msg"] = msg
q := sdk.Runtime.GetMemoryQueue(c.Request.Host)
message, err := sdk.Runtime.GetStreamMessage("", global.LoginLog, l)
if err != nil {
log.Errorf("GetStreamMessage error, %s", err.Error())
//日志报错错误,不中断请求
} else {
err = q.Append(message)
if err != nil {
log.Errorf("Append message error, %s", err.Error())
}
l := models.SysLoginLog{
Username: truncateAuditText(username, 128),
Status: status,
Ipaddr: truncateAuditText(common.GetClientIP(c), 255),
LoginLocation: "",
Browser: truncateAuditText(browserName+" "+browserVersion, 255),
Os: truncateAuditText(ua.OS(), 255),
Platform: truncateAuditText(ua.Platform(), 255),
LoginTime: pkg.GetCurrentTime(),
Remark: truncateAuditText(c.Request.Method+" "+c.FullPath(), 255),
Msg: truncateAuditText(msg, 255),
}
if err = db.Create(&l).Error; err != nil {
log.Errorf("identity audit write failed: %s", err.Error())
}
}
func truncateAuditText(value string, limit int) string {
runes := []rune(value)
if len(runes) <= limit {
return value
}
return string(runes[:limit])
}
// LogOut
@@ -164,7 +170,7 @@ func Authorizator(data interface{}, c *gin.Context) bool {
if v, ok := data.(map[string]interface{}); ok {
u, _ := v["user"].(models.SysUser)
r, _ := v["role"].(models.SysRole)
c.Set("role", r.RoleName)
c.Set("role", r.RoleKey)
c.Set("roleIds", r.RoleId)
c.Set("userId", u.UserId)
c.Set("userName", u.Username)
@@ -175,6 +181,9 @@ func Authorizator(data interface{}, c *gin.Context) bool {
}
func Unauthorized(c *gin.Context, code int, message string) {
if c.FullPath() != "/api/v1/login" {
WriteIdentityAudit(c, "1", "未认证访问被拒绝", "")
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": message,
@@ -0,0 +1,12 @@
package handler
import "testing"
func TestTruncateAuditText(t *testing.T) {
if got := truncateAuditText("中文审计记录", 3); got != "中文审" {
t.Fatalf("unexpected truncation: %q", got)
}
if got := truncateAuditText("short", 10); got != "short" {
t.Fatalf("unexpected unchanged value: %q", got)
}
}
+44 -1
View File
@@ -95,11 +95,54 @@ func LoggerToFile() gin.HandlerFunc {
log.Fields(map[string]interface{}{})
}()
if c.Request.Method != "OPTIONS" && config.LoggerConfig.EnabledDB && statusCode != 404 {
SetDBOperLog(c, clientIP, statusCode, reqUri, reqMethod, latencyTime, body, result, statusBus)
SetDBOperLog(c, clientIP, statusCode, reqUri, reqMethod, latencyTime, sanitizeAuditJSON(body), sanitizeAuditJSON(result), statusBus)
}
}
}
func sanitizeAuditJSON(value string) string {
if strings.TrimSpace(value) == "" {
return ""
}
var decoded interface{}
if err := json.Unmarshal([]byte(value), &decoded); err != nil {
return "[内容已隐藏]"
}
redactAuditValue(decoded)
encoded, err := json.Marshal(decoded)
if err != nil {
return "[内容已隐藏]"
}
return string(encoded)
}
func redactAuditValue(value interface{}) {
switch current := value.(type) {
case map[string]interface{}:
for key, item := range current {
if isSensitiveAuditKey(key) {
current[key] = "[已隐藏]"
continue
}
redactAuditValue(item)
}
case []interface{}:
for _, item := range current {
redactAuditValue(item)
}
}
}
func isSensitiveAuditKey(key string) bool {
normalized := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(key, "_", ""), "-", ""))
switch normalized {
case "password", "oldpassword", "newpassword", "token", "refreshtoken", "secret", "code", "uuid":
return true
default:
return false
}
}
// SetDBOperLog 写入操作日志表 fixme 该方法后续即将弃用
func SetDBOperLog(c *gin.Context, clientIP string, statusCode int, reqUri string, reqMethod string, latencyTime time.Duration, body string, result string, status int) {
@@ -0,0 +1,25 @@
package middleware
import (
"strings"
"testing"
)
func TestSanitizeAuditJSON(t *testing.T) {
input := `{"username":"operator","password":"secret-value","nested":{"newPassword":"another-secret"},"token":"jwt-value"}`
got := sanitizeAuditJSON(input)
for _, secret := range []string{"secret-value", "another-secret", "jwt-value"} {
if strings.Contains(got, secret) {
t.Fatalf("sensitive value leaked: %s", got)
}
}
if !strings.Contains(got, "operator") {
t.Fatalf("non-sensitive context was unexpectedly removed: %s", got)
}
}
func TestSanitizeAuditJSONRejectsUnstructuredBodies(t *testing.T) {
if got := sanitizeAuditJSON("password=secret-value"); strings.Contains(got, "secret-value") {
t.Fatalf("unstructured body leaked: %s", got)
}
}
@@ -4,6 +4,7 @@ import (
"github.com/casbin/casbin/v2/util"
"net/http"
"git.ilapage.cn/ila/yovision/Sense/server/common/middleware/handler"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk"
"github.com/go-admin-team/go-admin-core/sdk/api"
@@ -49,6 +50,7 @@ func AuthCheckRole() gin.HandlerFunc {
c.Next()
} else {
log.Warnf("isTrue: %v role: %s method: %s path: %s message: %s", res, v["rolekey"], c.Request.Method, c.Request.URL.Path, "当前request无权限,请管理员确认!")
handler.WriteIdentityAudit(c, "1", "拒绝访问 "+c.Request.Method+" "+c.FullPath(), userNameFromClaims(v))
c.JSON(http.StatusOK, gin.H{
"code": 403,
"msg": "对不起,您没有该接口访问权限,请联系管理员",
@@ -59,3 +61,10 @@ func AuthCheckRole() gin.HandlerFunc {
}
}
func userNameFromClaims(claims jwtauth.MapClaims) string {
if value, ok := claims["nice"].(string); ok {
return value
}
return ""
}
+1 -35
View File
@@ -6,38 +6,4 @@ type UrlInfo struct {
}
// CasbinExclude casbin 排除的路由列表
var CasbinExclude = []UrlInfo{
{Url: "/api/v1/dict/type-option-select", Method: "GET"},
{Url: "/api/v1/dict-data/option-select", Method: "GET"},
{Url: "/api/v1/deptTree", Method: "GET"},
{Url: "/api/v1/db/tables/page", Method: "GET"},
{Url: "/api/v1/db/columns/page", Method: "GET"},
{Url: "/api/v1/gen/toproject/:tableId", Method: "GET"},
{Url: "/api/v1/gen/todb/:tableId", Method: "GET"},
{Url: "/api/v1/gen/tabletree", Method: "GET"},
{Url: "/api/v1/gen/preview/:tableId", Method: "GET"},
{Url: "/api/v1/gen/apitofile/:tableId", Method: "GET"},
{Url: "/api/v1/getCaptcha", Method: "GET"},
{Url: "/api/v1/getinfo", Method: "GET"},
{Url: "/api/v1/menuTreeselect", Method: "GET"},
{Url: "/api/v1/menurole", Method: "GET"},
{Url: "/api/v1/menuids", Method: "GET"},
{Url: "/api/v1/roleMenuTreeselect/:roleId", Method: "GET"},
{Url: "/api/v1/roleDeptTreeselect/:roleId", Method: "GET"},
{Url: "/api/v1/refresh_token", Method: "GET"},
{Url: "/api/v1/configKey/:configKey", Method: "GET"},
{Url: "/api/v1/app-config", Method: "GET"},
{Url: "/api/v1/user/profile", Method: "GET"},
{Url: "/info", Method: "GET"},
{Url: "/api/v1/login", Method: "POST"},
{Url: "/api/v1/logout", Method: "POST"},
{Url: "/api/v1/user/avatar", Method: "POST"},
{Url: "/api/v1/user/pwd", Method: "PUT"},
{Url: "/api/v1/metrics", Method: "GET"},
{Url: "/api/v1/health", Method: "GET"},
{Url: "/", Method: "GET"},
{Url: "/api/v1/server-monitor", Method: "GET"},
{Url: "/api/v1/public/uploadFile", Method: "POST"},
{Url: "/api/v1/user/pwd/set", Method: "PUT"},
{Url: "/api/v1/sys-user", Method: "PUT"},
}
var CasbinExclude = []UrlInfo{}