fix(core): 基础设施故障不再伪装成凭据失效
鉴权过滤器里除了「token 无效」,还有两次外部依赖调用——核心实例的 /auth/user 与 Redis 的域名
白名单读。此前它们抛出的异常与「token 无效」一起被兜底成 401「请退出重新登录」,于是线上出现
这样的报文:
{"code":401,"message":"请退出重新登录","error":"...RedisException: SocketException: Connection reset"}
用户照着这句话去重新登录什么也解决不了——Redis 连接被重置跟他手里的 token 毫无关系;真正的故障
被「登录态异常」这层皮盖住,排查方向从一开始就被带偏;而且重新登录会把一份**完全有效**的凭据丢掉,
把一次秒级抖动放大成用户的重复劳动。
- 新增 AuthFailureUtil.isInfrastructureFailure(e):沿 cause 链找基础设施异常。必须沿链找,因为真实
异常是套娃的(RedisSystemException → io.lettuce.core.RedisException → SocketException;
IORuntimeException → ConnectException),只看最外层会漏判;凭据类异常
(MalformedJwtException / SignatureException / UsernameNotFoundException)的 cause 链里不会出现
这些类型。带遍历深度上限,防御自引用环。
- Constants 加 SERVICE_UNAVAILABLE_CODE(503) 与 SERVICE_UNAVAILABLE_MSG。
- JwtAuthenticationFilter:命中基础设施故障时返回 503 +「服务暂不可用,请稍后重试」,
其余异常仍按凭据失效处理。
- 补 AuthFailureUtilTest / JwtAuthenticationFilterTest(tokenKey 运行时用 JwtUtil.randomKey() 生成,
不含任何真实凭据)。
只改变「异常如何归类与报出」,不触碰正常路径与既有错误码。
This commit is contained in:
@@ -65,6 +65,16 @@ public class Constants {
|
||||
*/
|
||||
public static final String BAD_CREDENTIALS_MSG = "请退出重新登录";
|
||||
|
||||
/**
|
||||
* 服务暂不可用错误码(Redis 连接被重置、核心实例不可达等基础设施故障,不是用户凭据问题)
|
||||
*/
|
||||
public static final int SERVICE_UNAVAILABLE_CODE = 503;
|
||||
|
||||
/**
|
||||
* 服务暂不可用提示信息
|
||||
*/
|
||||
public static final String SERVICE_UNAVAILABLE_MSG = "服务暂不可用,请稍后重试";
|
||||
|
||||
/**
|
||||
* 表示升序的值
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.gxwebsoft.common.core.security;
|
||||
|
||||
import io.lettuce.core.RedisException;
|
||||
import org.springframework.data.redis.RedisConnectionFailureException;
|
||||
import org.springframework.data.redis.RedisSystemException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 鉴权链路上的失败分类:这次失败是<b>用户凭据问题</b>,还是<b>基础设施抖了一下</b>?
|
||||
*
|
||||
* <p><b>为什么必须分开</b>:鉴权过滤器里除了「token 无效」,还有两次外部依赖调用——
|
||||
* 核心实例的 {@code /auth/user} 与 Redis 的域名白名单读。此前它们抛出的异常与「token 无效」
|
||||
* 一起被兜底成 {@code 401 请退出重新登录},于是线上出现这样一条报文:</p>
|
||||
*
|
||||
* <pre>
|
||||
* {"code":401,"message":"请退出重新登录",
|
||||
* "error":"org.springframework.data.redis.RedisSystemException: Redis exception;
|
||||
* nested exception is io.lettuce.core.RedisException:
|
||||
* java.net.SocketException: Connection reset"}
|
||||
* </pre>
|
||||
*
|
||||
* <p>用户照着这句话去重新登录,什么也解决不了——Redis 连接被重置跟他手里的 token 毫无关系;
|
||||
* 而真正的故障(Redis / 核心实例)被「登录态异常」这层皮盖住了,排查方向从一开始就被带偏。
|
||||
* 重新登录还会把一份<b>完全有效</b>的凭据丢掉,把一次秒级抖动放大成用户的重复劳动。</p>
|
||||
*
|
||||
* <p><b>判定方式</b>:沿 cause 链找基础设施异常。之所以要沿链找,是因为真实抛出的异常是套娃的——
|
||||
* {@code RedisSystemException → io.lettuce.core.RedisException → java.net.SocketException}、
|
||||
* {@code cn.hutool.core.io.IORuntimeException → java.net.ConnectException},
|
||||
* 只看最外层的类型会漏判。凭据类异常({@code MalformedJwtException} / {@code SignatureException} /
|
||||
* {@code UsernameNotFoundException})的 cause 链里不会出现这些类型。</p>
|
||||
*/
|
||||
public final class AuthFailureUtil {
|
||||
|
||||
/** cause 链的遍历深度上限,防御自引用或异常实现里的环 */
|
||||
private static final int MAX_CAUSE_DEPTH = 16;
|
||||
|
||||
private AuthFailureUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为基础设施故障(Redis 连接被重置/超时、核心实例不可达等)。
|
||||
*
|
||||
* <p>这类失败<b>不该</b>报「请退出重新登录」,应报
|
||||
* {@link com.gxwebsoft.common.core.Constants#SERVICE_UNAVAILABLE_MSG},让用户稍后重试即可。</p>
|
||||
*
|
||||
* @param e 鉴权过程中抛出的异常
|
||||
* @return true 表示与用户凭据无关,是基础设施的问题
|
||||
*/
|
||||
public static boolean isInfrastructureFailure(Throwable e) {
|
||||
Throwable t = e;
|
||||
for (int depth = 0; t != null && depth < MAX_CAUSE_DEPTH; depth++) {
|
||||
if (t instanceof RedisConnectionFailureException
|
||||
|| t instanceof RedisSystemException
|
||||
|| t instanceof RedisException
|
||||
|| t instanceof IOException) {
|
||||
return true;
|
||||
}
|
||||
Throwable cause = t.getCause();
|
||||
t = (cause == t) ? null : cause;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,13 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
e.getMessage());
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
// Redis 连接被重置、核心实例不可达属于基础设施抖动,与用户凭据无关:
|
||||
// 报「请退出重新登录」既解决不了问题,又把真正的故障掩盖成一次登录态异常。
|
||||
if (AuthFailureUtil.isInfrastructureFailure(e)) {
|
||||
CommonUtil.responseError(response, Constants.SERVICE_UNAVAILABLE_CODE,
|
||||
Constants.SERVICE_UNAVAILABLE_MSG, e.toString());
|
||||
return;
|
||||
}
|
||||
CommonUtil.responseError(response, Constants.BAD_CREDENTIALS_CODE, Constants.BAD_CREDENTIALS_MSG,
|
||||
e.toString());
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user