feat: 重建 Sense 认证权限与审计 (#64)
This commit is contained in:
@@ -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,
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
|
||||
Reference in New Issue
Block a user