162 日志(8884 容器,v1.2.157)实证: 16:25:58 INFO 刷新cookie: 已用长期凭证续期成功 userId=1279 16:25:58 ERROR 获取表单失败 taskId=337 error="...cpdaily_cookie_expired" 续期报告成功,紧接着业务调用仍过期。 根因:refreshCookieByLongTermToken 判定成功的唯一标准是 「结果里存在 MOD_ 前缀的 cookie」。但续期走的 /imUserToken/getImUserToken 接口只校验 sessionToken 头, **不校验 CAS 会话是否有效**;当 CASTGC/sessionToken 本身已过期时, 它仍可能返回 errCode=0,并把 jar 里那批无效 cookie 全数带回。 于是本次续期「成功」,调用方立刻重试又过期,用户只看到内部错误码。 改动: - 新增 cpd.VerifyCookieUsable:用业务同款最小请求(拉当天任务列表) 做只读探测。三种任务类型任一能正常返回即视为有效;返回 ErrCookieExpired 才判定凭证不可用;网络等其它错误不归咎于凭证。 该探测无任何写副作用。 - refreshCookieByLongTermToken 在回写 cookie 前先跑校验, 不通过则按续期失败返回,让上层走「请重新登录」引导, 而不是发出一轮注定失败的重试。 配合上一提交(07cf121)的两处升级与 userFacingError 兜底, 现在链路上任一环节的登录态失效都会转成明确的重登引导, 不再把 cpdaily_cookie_expired 暴露给用户。
This commit is contained in:
@@ -214,11 +214,41 @@ func refreshCookieByLongTermToken(user *model.SysUser) (string, error) {
|
||||
}
|
||||
|
||||
cookieStr := cpd.SerializeCookies(cookies)
|
||||
|
||||
// 关键:拿到 MOD_* 不等于续期有效。
|
||||
// RefreshCasCookie 走的 /imUserToken 接口只校验 sessionToken 头,
|
||||
// 不校验 CAS 会话是否仍然有效;当 CASTGC/sessionToken 本身已过期时,
|
||||
// 它仍可能返回 errCode=0 并把 jar 里(旧的/无效的)cookie 全数返回。
|
||||
// 于是这里会“续期成功”,而调用方重试立刻再次判定登录态失效。
|
||||
//
|
||||
// 因此用新 cookie 做一次轻量真实校验:能取到当天任务列表才算真的续上了。
|
||||
// 校验失败则按续期失败处理,让上层引导重新登录,而不是白白重试一轮。
|
||||
if verifyErr := verifyCpdailyCookie(cookieStr, user); verifyErr != nil {
|
||||
return "", fmt.Errorf("续期后凭证仍无效: %w", verifyErr)
|
||||
}
|
||||
|
||||
// 回写传入用户,供上层重试使用
|
||||
user.Cookie = cookieStr
|
||||
return cookieStr, nil
|
||||
}
|
||||
|
||||
// verifyCpdailyCookie 用指定 cookie 做一次只读校验,确认登录态真实可用。
|
||||
//
|
||||
// 选择「拉当天任务列表」是因为它最轻、几乎所有学号都有数据,
|
||||
// 且不会产生任何写入副作用(不签到、不改动任何服务端状态)。
|
||||
func verifyCpdailyCookie(cookieStr string, user *model.SysUser) error {
|
||||
m := cpd.ParseCookieJSON(cookieStr)
|
||||
if len(m) == 0 {
|
||||
return errors.New("续期结果为空")
|
||||
}
|
||||
host := getSchoolHost(user.SchoolID)
|
||||
if host == "" {
|
||||
return errors.New("未配置学校地址")
|
||||
}
|
||||
// 复用与正式调用相同的过期检测逻辑
|
||||
return cpd.VerifyCookieUsable(m, host)
|
||||
}
|
||||
|
||||
// withCookieRefresh 执行一次今日校园调用;若因登录态失效失败,则尝试续期 cookie 后重试一次。
|
||||
// - 返回 (调用结果, 续期后的 cookieMap, error)。cookieMap 有变化时调用方可用于后续调用。
|
||||
// - 续期失败(如 MFA 学校无法自动续期、密码已改)时,返回错误里带上 ErrCookieExpired,
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
@@ -1718,3 +1719,43 @@ func faceProgressIsTerminalFailure(msg string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// VerifyCookieUsable 用指定 cookie 做一次只读探测,确认登录态真实可用。
|
||||
//
|
||||
// 起因:自动续期拿到 MOD_* cookie 后被判定「续期成功」,但紧接着的业务调用
|
||||
// 仍返回 cpdaily_cookie_expired。根因是续期走的 /imUserToken 接口只校验
|
||||
// sessionToken 头、不校验 CAS 会话,在 CASTGC/sessionToken 已失效时
|
||||
// 仍可能返回成功,并带回一批无效 cookie。
|
||||
//
|
||||
// 因此这里用与业务相同的最小请求(拉当天任务列表)验证:
|
||||
// - 返回 ErrCookieExpired ⇒ 凭证确实不可用;
|
||||
// - 其它错误(网络/限流等)不归咎于凭证,向上返回但不当作过期;
|
||||
// - 正常返回(哪怕列表为空)⇒ 登录态可用。
|
||||
//
|
||||
// 该探测为只读,不会产生任何写操作副作用。
|
||||
func VerifyCookieUsable(cookieMap map[string]string, host string) error {
|
||||
if len(cookieMap) == 0 {
|
||||
return ErrCookieExpired
|
||||
}
|
||||
if host == "" {
|
||||
return fmt.Errorf("学校地址为空")
|
||||
}
|
||||
// 依次尝试三种任务类型:任一能正常返回即说明会话有效。
|
||||
// (学生未必同时拥有签到与查寝任务,单类型返回空不代表失效。)
|
||||
var lastErr error
|
||||
for _, tt := range []TaskType{TaskTypeSign, TaskTypeSleepCheck, TaskTypeReturnRegister} {
|
||||
if _, err := fetchTasksByEndpoint(cookieMap, host, tt, ""); err != nil {
|
||||
if errors.Is(err, ErrCookieExpired) {
|
||||
lastErr = err
|
||||
continue // 该类过期,再试其它端点确认
|
||||
}
|
||||
// 非过期类错误(网络等):不能据此判定凭证失效
|
||||
return nil
|
||||
}
|
||||
return nil // 任一端点可用即视为有效
|
||||
}
|
||||
if lastErr != nil {
|
||||
return lastErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package cpdaily
|
||||
|
||||
import "testing"
|
||||
|
||||
// VerifyCookieUsable 的边界:空 cookie / 空 host 必须判为不可用或报错,
|
||||
// 不能静默让上层以为凭证有效。
|
||||
func TestVerifyCookieUsableGuards(t *testing.T) {
|
||||
if err := VerifyCookieUsable(nil, "https://x.edu.cn"); err == nil {
|
||||
t.Fatal("空 cookie 应报错")
|
||||
}
|
||||
if err := VerifyCookieUsable(map[string]string{"MOD_AUTH_CAS": "x"}, ""); err == nil {
|
||||
t.Fatal("空 host 应报错")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user