package handler import ( "net/url" "strings" "unicode/utf8" ) const ( maxPageURLLen = 1000 maxPageTitleLen = 200 // 单会话最多保留轨迹条数(超出删最旧) maxPageViewsPerSession = 50 ) // 常见敏感 query 参数,上报 URL 时剥离 var sensitiveQueryKeys = map[string]bool{ "token": true, "access_token": true, "refresh_token": true, "password": true, "passwd": true, "pwd": true, "phone": true, "mobile": true, "tel": true, "idcard": true, "id_card": true, "secret": true, "key": true, "authorization": true, "auth": true, "session": true, "sid": true, "code": true, "otp": true, "verify": true, } func truncateRunes(s string, max int) string { s = strings.TrimSpace(s) if max <= 0 || s == "" { return s } if utf8.RuneCountInString(s) <= max { return s } r := []rune(s) return string(r[:max]) } // sanitizePageURL 规范化并脱敏 URL,非法则返回空。 func sanitizePageURL(raw string) string { raw = strings.TrimSpace(raw) if raw == "" { return "" } if utf8.RuneCountInString(raw) > maxPageURLLen*2 { raw = string([]rune(raw)[:maxPageURLLen*2]) } u, err := url.Parse(raw) if err != nil || u.Scheme == "" || u.Host == "" { // 允许无 scheme 的相对路径拼不成绝对时,若已是 http(s) 失败则截断原文 if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") { return truncateRunes(raw, maxPageURLLen) } return "" } if u.Scheme != "http" && u.Scheme != "https" { return "" } q := u.Query() changed := false for k := range q { lk := strings.ToLower(k) if sensitiveQueryKeys[lk] || strings.Contains(lk, "token") || strings.Contains(lk, "password") { q.Del(k) changed = true } } if changed { u.RawQuery = q.Encode() } // 保留 hash:大量 SPA(含演示站 demo-shop)靠 #/path 区分页面; // 若去掉 fragment,换页后 URL 相同会被服务端当成重复而丢弃轨迹。 // 仅截断过长 hash,避免异常数据撑爆字段。 if utf8.RuneCountInString(u.Fragment) > 200 { u.Fragment = string([]rune(u.Fragment)[:200]) } out := u.String() return truncateRunes(out, maxPageURLLen) } func sanitizePageTitle(raw string) string { return truncateRunes(raw, maxPageTitleLen) }