Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions middleware/auth_api_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func Auth() gin.HandlerFunc {
// 获取请求头中 token,实际是一个完整被签名过的 token;a complete, signed token
tokenStr := extractTokenStr(c)
if tokenStr == "" {
zlog.Debug("无token")
zlog.Debug(fmt.Sprintf("请求未携带令牌 path:%v 来源IP:%v", c.Request.URL.Path, utils.GetManageClientIP(c)))

response.AuthFailWithMessage("鉴权失败", c)
c.Abort()
Expand Down Expand Up @@ -238,15 +238,29 @@ func bindFailCounterTTL() time.Duration {
func extractTokenStr(c *gin.Context) string {
reqPath := c.Request.URL.Path
if reqPath == "/api/v1/ws" {
return c.GetHeader("Sec-WebSocket-Protocol")
return normalizeTokenStr(c.GetHeader("Sec-WebSocket-Protocol"))
}
if strings.HasPrefix(reqPath, "/api/v1/waflog/attack/download") {
return c.Query("X-Token")
return normalizeTokenStr(c.Query("X-Token"))
}
if c.GetHeader("X-Login-Type") == "mobile" {
return c.GetHeader("X-Mobile-Token")
return normalizeTokenStr(c.GetHeader("X-Mobile-Token"))
}
return c.GetHeader("X-Token")
return normalizeTokenStr(c.GetHeader("X-Token"))
}

// normalizeTokenStr 把"没有令牌"的几种写法统一成空串。
//
// 字面量 null/undefined 要当成没有:WebSocket 握手带不了自定义头,令牌只能放进子协议,
// 而浏览器会把 JS 里的 null 原样字符串化发出来。不归一的话它是个非空字符串,
// 会一路走到缓存查询并被判成"令牌不存在"——日志上看就像有人拿着无效令牌反复试,
// 实际只是登录页在重连。
func normalizeTokenStr(raw string) string {
token := strings.TrimSpace(raw)
if token == "null" || token == "undefined" {
return ""
}
return token
}

// isFingerprintExemptPath 判断当前请求是否豁免设备指纹比对。
Expand Down
36 changes: 36 additions & 0 deletions middleware/auth_ws_token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,39 @@ func TestFingerprintExemptWithQueryString(t *testing.T) {
})
}
}

// 未登录时前端从 localStorage 取不到令牌,浏览器会把 JS 的 null 原样字符串化成 "null"
// 放进子协议发出来。不归一的话它是个非空字符串,会一路走到缓存查询并被判成"令牌不存在",
// 登录页停着不动也会每隔几秒刷一条无效令牌日志。
func TestExtractTokenStrTreatsNullLiteralAsEmpty(t *testing.T) {
cases := []struct {
name string
target string
header string
value string
}{
{"WebSocket 子协议 null", "/api/v1/ws", "Sec-WebSocket-Protocol", "null"},
{"WebSocket 子协议 undefined", "/api/v1/ws", "Sec-WebSocket-Protocol", "undefined"},
{"WebSocket 子协议空白", "/api/v1/ws", "Sec-WebSocket-Protocol", " "},
{"常规请求头 null", "/api/v1/host/list", "X-Token", "null"},
{"常规请求头 undefined", "/api/v1/host/list", "X-Token", "undefined"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := ctxFor(t, http.MethodGet, tc.target, map[string]string{tc.header: tc.value})
if got := extractTokenStr(c); got != "" {
t.Fatalf("%q 应被当作没有令牌,实际取到 %q", tc.value, got)
}
})
}
}

// 归一化不能误伤正常令牌
func TestExtractTokenStrKeepsRealToken(t *testing.T) {
c := ctxFor(t, http.MethodGet, "/api/v1/host/list", map[string]string{
"X-Token": "0d42ce0c1f2a3b4c5d6e7f8091a2b3c4",
})
if got := extractTokenStr(c); got != "0d42ce0c1f2a3b4c5d6e7f8091a2b3c4" {
t.Fatalf("正常令牌被改动了:%q", got)
}
}
Loading