上一轮用公开接口做对照实验,确认了服务端响应语义: - 正确加密但 ticket 无效 → 「用户名或密码错误」(解密成功) - 裸 ticket / 乱码密文 → 「参数解码错误」(解密失败) - 空 ticket → 500 同时已排除代码侧原因(Go 与站点 security.js 产物逐字节一致、 chunkSize=126 一致、sec_js 禁缓存、算法自 v1.2.157 未变)。 但「参数解码错误」仍缺一处可见性:CAS 换票拿到的 ticket 本身是什么形态、 加密后几块、服务端原始响应是什么,线上日志都没有。没有这些信息, 下一次复现只能继续猜。 新增(均不含凭证本体): - CAS 换票成功:ticketLen / hasSTPrefix / hasSpace / service - 加密完成:ticketLen / dkLen / encLen / blockCount (blockCount>1 才可能出现多块空格,届时 body 是否被 URL 编码才有区别; 这条日志能一次性回答该问题) - 登录被拒:httpStatus / message / bodySnippet(截断 300 rune) 配套 truncateForLog:按 rune 截断,不切断多字节字符。 测试覆盖短串不截断、超长加标记、中文不产生乱码、边界值。
This commit is contained in:
@@ -77,6 +77,15 @@ func XsgzLogin(ctx context.Context, schoolID, casTicket string) (*XsgzSession, e
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ticket 加密失败: %w", err)
|
||||
}
|
||||
// 诊断日志:便于线上区分「ticket 本身异常」与「加密产物异常」。
|
||||
// blockCount>1 说明产生了多块(用空格连接),此时 body 若被 URL 编码
|
||||
// 会引入 '+' 差异;encLen 与 dkLen 用于核对密钥是否取对。
|
||||
// 不打印密文与 DK 本体。
|
||||
slog.Info("CAS 登录加密完成",
|
||||
"ticketLen", len(casTicket),
|
||||
"dkLen", len(rsa.DK),
|
||||
"encLen", len(enc),
|
||||
"blockCount", strings.Count(enc, " ")+1)
|
||||
|
||||
// body 与前端保持一致:前端 OAuthCallback 组件是
|
||||
// const u = window.encrypt(ticket); await getOAuthToken(u);
|
||||
@@ -116,6 +125,13 @@ func XsgzLogin(ctx context.Context, schoolID, casTicket string) (*XsgzSession, e
|
||||
if msg == "" {
|
||||
msg = "未知错误"
|
||||
}
|
||||
// 记录 HTTP 状态与响应体摘要:服务端对「解密失败」与「ticket 无效」
|
||||
// 返回的文案不同(前者「参数解码错误」,后者「用户名或密码错误」),
|
||||
// 这是区分两类问题最直接的证据。限制长度避免刷日志。
|
||||
slog.Error("CAS 登录被拒绝",
|
||||
"httpStatus", resp.StatusCode(),
|
||||
"message", msg,
|
||||
"bodySnippet", truncateForLog(string(body), 300))
|
||||
return nil, fmt.Errorf("登录失败: %s", msg)
|
||||
}
|
||||
|
||||
@@ -754,6 +770,14 @@ func CasTicketForService(ctx context.Context, cfg CasServiceConfig) (string, err
|
||||
if ticket == "" {
|
||||
return "", fmt.Errorf("回调地址未携带 ticket")
|
||||
}
|
||||
// 诊断日志:CAS 换票结果只在这里可见,出问题时若没有这些信息,
|
||||
// 只能猜测「参数解码错误」是 ticket 异常还是加密异常。
|
||||
// 只记录长度与形态特征,不打印 ticket 本体,避免凭证泄露。
|
||||
slog.Info("CAS 换票成功",
|
||||
"ticketLen", len(ticket),
|
||||
"hasSTPrefix", strings.HasPrefix(ticket, "ST-"),
|
||||
"hasSpace", strings.Contains(ticket, " "),
|
||||
"service", cfg.Service)
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
@@ -946,3 +970,16 @@ func XsgzCasServiceURL(schoolID string) string {
|
||||
}
|
||||
return base + "/oauth/callback"
|
||||
}
|
||||
|
||||
// truncateForLog 限制日志字符串长度,避免把整个响应体刷进日志。
|
||||
// 按 rune 截断,避免切断多字节字符产生乱码。
|
||||
func truncateForLog(s string, maxRunes int) string {
|
||||
if maxRunes <= 0 {
|
||||
return ""
|
||||
}
|
||||
r := []rune(s)
|
||||
if len(r) <= maxRunes {
|
||||
return s
|
||||
}
|
||||
return string(r[:maxRunes]) + "...(truncated)"
|
||||
}
|
||||
|
||||
@@ -548,3 +548,28 @@ func TestCasTicketNameVariants(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// truncateForLog 截断保护:不刷日志、不切断多字节字符。
|
||||
func TestTruncateForLog(t *testing.T) {
|
||||
if got := truncateForLog("abc", 10); got != "abc" {
|
||||
t.Fatalf("短串不应截断,实际 %q", got)
|
||||
}
|
||||
long := strings.Repeat("中", 400)
|
||||
got := truncateForLog(long, 300)
|
||||
if !strings.HasSuffix(got, "...(truncated)") {
|
||||
t.Fatal("超长应加截断标记")
|
||||
}
|
||||
// 中文按 rune 截断,不应出现乱码(每个中文字仍是完整字符)
|
||||
body := strings.TrimSuffix(got, "...(truncated)")
|
||||
if len([]rune(body)) != 300 {
|
||||
t.Fatalf("应保留 300 个 rune,实际 %d", len([]rune(body)))
|
||||
}
|
||||
for _, r := range body {
|
||||
if r != '中' {
|
||||
t.Fatalf("出现乱码字符 %q", r)
|
||||
}
|
||||
}
|
||||
if got := truncateForLog("x", 0); got != "" {
|
||||
t.Fatalf("maxRunes<=0 应返回空串,实际 %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user