中间件读不到cookie主因是set-cookie响应头domain或samesite配置不匹配:跨子域需设domain=".example.com",samesite=lax时跨站post不发cookie,none则必须配secure=true;前端fetch须带credentials:'include'。

中间件里读不到 Cookie 是因为没设 Domain 或 SameSite
Cookie 在跨子域或 HTTPS 场景下失效,90% 的原因是 Set-Cookie 响应头没配对。比如用户从 auth.example.com 登录后跳转到 app.example.com,但服务端写 Cookie 时只设了 Domain: auth.example.com,浏览器就不会把 Cookie 发给 app 域。
必须显式设置 Domain: ".example.com"(注意开头的点),且 SameSite 不能是 Strict;若用 None,则 Secure: true 必须开启。
http.Cookie{Domain: ".example.com", SameSite: http.SameSiteLaxMode, Secure: true, HttpOnly: true}- 前端 fetch 必须带
credentials: 'include',否则浏览器压根不发 Cookie - 检查响应头中是否真有
Set-Cookie: token=xxx; Domain=.example.com; Path=/; HttpOnly; Secure; SameSite=Lax
校验逻辑别写在 handler 里,要统一收口到中间件
很多人把 token 解析、过期判断、签名校验全塞进 /api/user 这类 handler,结果每个接口都 copy-paste 一遍,漏掉一个就绕过鉴权。正确做法是用中间件拦截所有需要登录的路由分组,比如 apiGroup := e.Group("/api"),然后 apiGroup.Use(authMiddleware)。
中间件里不要用 c.Get("user") 这种弱类型取值——它依赖上层中间件已 set,且 IDE 无法提示字段名。应该用强类型 context key:
type userCtxKey struct{}
// ...
cookie, err := c.Cookie("token")
if err != nil {
return c.JSON(http.StatusUnauthorized, echo.Map{"error": "missing token"})
}
// 解析 JWT 后
user := &User{ID: 123, Role: "admin"}
c.Set(userCtxKey{}, user) // 而不是 c.Set("user", user)
JWT 签名校验必须走 KeyFunc,不能硬编码密钥
直接在中间件里写 jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) { return []byte("my-secret"), nil }) 是错的。一旦密钥轮换或要用 RSA 公钥验签,就得改代码、重启服务。
正确方式是把密钥获取逻辑抽成函数,由 KeyFunc 动态返回:
- 对称密钥:从环境变量或配置中心读取,加一层缓存避免每次解析都 IO
- 非对称密钥:用
rsa.PublicKey,从 PEM 文件加载,别用ParseRSAPublicKeyFromPEM直接解析原始字节(容易 panic) - 必须校验
claims.Issuer和claims.Audience,防止 token 被其他服务复用
Cookie 过期和篡改检测要分开做,顺序不能反
先校验签名再检查过期时间。如果签名无效,说明整个 payload 可能被篡改,此时连 exp 字段都不能信——攻击者可以伪造一个“未过期”的 exp 来绕过时间检查。
另外,http.Cookie.MaxAge 和 JWT 的 exp 是两回事:MaxAge 控制浏览器何时删 Cookie,exp 控制 token 是否有效。两者都要校验,但来源不同、作用域不同。
容易忽略的一点:Chrome 120+ 对 SameSite=Lax 的跨站 POST 请求默认不发 Cookie,如果你的登录回调是 POST 到 /callback,得确认前端跳转方式是不是用了 form.submit() 或 fetch(..., {method: 'POST'}) ——这种场景下,SameSite=None 才是安全选择。











