管理主页分类

拖动分类调整顺序,并选择是否在主页显示。设置仅保存在当前浏览器。

  • 微服务课件 31
  • Java 基础 28
  • Java Web 开发 25
  • 多来买 18
  • LeetCode 题解 8
  • 开发工具 8
  • 网络工具 2
  • 黑马头条 2
  • Java 记忆恢复 1

多来买:Gateway 鉴权与身份透传实战

1998 字
10 分钟
多来买:Gateway 鉴权与身份透传实战

多来买:Gateway 鉴权与身份透传实战#

核心问题:Gateway 怎样判断路径是否需要登录、恢复正式身份并透传临时身份
事实来源:server-gateway、user-api、common-util 当前源码
证据口径:代码已确认;未读取 Nacos 中实际 authUrls 与路由配置,未发送真实请求

用户服务负责建立会话,Gateway 负责在每次入口请求上解释会话。当前过滤链先匹配受保护路径,再从 Header 或 Cookie 读取 Token,查询 Redis、比较 IP,最后将认证得到的 userId 和客户端临时标识 userTempId 写入下游请求。

1. 受保护请求的端到端时序#

1.1 携带 Token 访问受保护接口#

sequenceDiagram participant C as "客户端" participant F as "AuthFilter" participant R as "Redis" participant IP as "IpUtil" participant API as "下游业务服务" C->>F: "请求 + Header/Cookie token" F->>F: "PathPattern 匹配 authUrls.url" F->>F: "Header 优先,Cookie 兜底读取 token" alt "没有 token" F->>F: "userId = null" else "存在 token" F->>R: "GET user:login:{token}" alt "Key 不存在或已过期" R-->>F: "null" F->>F: "userId = null" else "会话存在" R-->>F: "UserLoginInfoDTO" F->>IP: "提取当前 IP" alt "IP 不一致" F-->>C: "业务码 204 非法请求" else "IP 一致" F->>F: "取得 userId" end end end alt "需要登录且 userId 为空" F-->>C: "业务码 208 未登录" else "允许放行" F->>F: "写入 userId / userTempId" F->>API: "转发变更后的请求" API-->>C: "业务响应" end

1.2 匿名身份与正式身份#

flowchart LR Client["客户端请求"] --> Gateway["AuthFilter"] Gateway --> Token{"Token 会话有效?"} Token -->|是| UserId["写入 userId"] Token -->|否| Anonymous["不建立正式身份"] Gateway --> Temp{"有 userTempId?"} Temp -->|是| TempId["透传 userTempId"] Temp -->|否| NoTemp["无临时身份"] UserId --> Downstream["下游服务"] Anonymous --> Downstream TempId --> Downstream NoTemp --> Downstream

userId 与 userTempId 的可信等级不同:

  • userId 理论上应来自 Redis 中的有效会话。
  • userTempId 本来就由客户端生成或保存,只用于匿名数据归属,不等价于认证身份。

2. Gateway:路径是否需要登录#

2.1 外部配置入口#

AuthFilter 通过属性 authUrls.url 取得一个路径模式。

运行中调用:

boolean isNeedLogin =
judgeNeedLoginByUrl(path, authUrl);

实际匹配方法:

private boolean judgeNeedLoginByUrl(
String path,
String authUrl
) {
PathPattern pattern = parser.parse(authUrl);
PathContainer container =
PathContainer.parsePath(path);
return pattern.matches(container);
}

代码出处:

  • 路径:duolaimall-gateway/src/main/java/com/cskaoyan/mall/gateway/filter/AuthFilter.java
  • 类与方法:AuthFilter#judgeNeedLoginByUrl

当前运行入口没有使用同类中保留的 AntPathMatcher 方法 judgeNeedLogin。

2.2 单模式边界#

authUrl 是一个 String,代码没有:

  • 读取 List;
  • 按逗号拆分;
  • 遍历多个模式;
  • 显式维护白名单。

因此当前实现从代码层只确认“一个 PathPattern 字符串”。

真实 Nacos 中配置了什么,本轮没有读取,不能确认覆盖了哪些接口。

2.3 登录接口为何必须是公开路径#

AuthFilter 对所有经过 Gateway 的请求执行。

如果 /user/login 被 authUrls.url 匹配为需要登录,就会形成循环:

  1. 用户没有 Token;
  2. 登录接口要求已有登录态;
  3. Gateway 返回未登录;
  4. 用户永远无法建立会话。

所以登录路径必须不匹配受保护模式。

这项要求来自鉴权逻辑,但当前 Nacos 值未验证。

3. Gateway:读取并验证会话#

3.1 主过滤流程#

源码节选:

  • 路径:duolaimall-gateway/src/main/java/com/cskaoyan/mall/gateway/filter/AuthFilter.java
  • 类与方法:AuthFilter#filter
String path = request.getURI().getPath();
boolean isNeedLogin =
judgeNeedLoginByUrl(path, authUrl);
String userId = hasLogin(request);
if ("-1".equals(userId)) {
return out(
response,
ResultCodeEnum.ILLEGAL_REQUEST
);
}
if (userId == null && isNeedLogin) {
return out(
response,
ResultCodeEnum.LOGIN_AUTH
);
}

顺序非常重要:

  1. 先判断路径是否需要登录;
  2. 仍然对请求调用 hasLogin;
  3. IP 异常优先拦截;
  4. 只有“未登录且路径受保护”才返回未登录;
  5. 其余情况继续构造下游请求。

因为 hasLogin 在公开路径上也会执行,公开请求若携带了一个存在但 IP 不匹配的 Token,同样会被判为非法请求,而不是自动降级成匿名请求。

3.2 Token 提取优先级#

源码节选:

String token =
request.getHeaders().getFirst("token");
if (StringUtils.isBlank(token)) {
HttpCookie cookie =
request.getCookies().getFirst("token");
if (cookie != null) {
token = cookie.getValue();
}
}
if (StringUtils.isBlank(token)) {
return null;
}

优先级是:

非空 Header token
> Cookie token
> 未登录

如果 Header 中存在一个无效 Token,同时 Cookie 中存在有效 Token,代码不会回退到 Cookie,因为只有 Header 为空时才读取 Cookie。

3.3 Redis 查询与 IP 比对#

源码节选:

  • 类与方法:AuthFilter#hasLogin
String key =
UserConstants.USER_LOGIN_KEY_PREFIX + token;
RBucket<UserLoginInfoDTO> bucket =
redissonClient.getBucket(key);
UserLoginInfoDTO loginInfo = bucket.get();
if (loginInfo == null) {
return null;
}
String ip = loginInfo.getIp();
String userId = loginInfo.getUserId();
String nowIp =
IpUtil.getGatwayIpAddress(request);
if (!ip.equals(nowIp)) {
return "-1";
}
return userId;

返回值被复用成三种状态:

hasLogin 返回值含义
null未携带 Token、Key 不存在或已过期
”-1”会话存在,但当前 IP 与登录 IP 不一致
其他非空字符串Redis 中保存的 userId

“-1” 是哨兵值,不是用户对象或枚举。

如果未来数据库可能存在 ID 为 -1 的用户,就会和异常状态冲突;当前 BaseEntity 使用自增 Long,未发现这种实际数据证据。

更清晰的长期设计可以返回专门的认证结果对象或枚举,但当前代码确实使用字符串哨兵。

4. Gateway 鉴权分支真值表#

4.1 路径、Token 与 IP#

是否受保护Token / Redis 状态IP 状态当前结果
无 Token不适用匿名放行
Token 对应 Key 不存在不适用匿名放行
有有效会话一致携带 userId 放行
有有效会话不一致返回业务码 204,拦截
无 Token不适用返回业务码 208,拦截
Token 对应 Key 不存在不适用返回业务码 208,拦截
有有效会话一致携带 userId 放行
有有效会话不一致返回业务码 204,拦截
Header tokenCookie token实际使用
未登录
有效Header
有效Cookie
有效 A有效 BHeader A
无效 A有效 B仍使用 Header A,不回退
纯空白有效 BCookie B

4.3 userTempId#

Header userTempIdCookie userTempId透传结果
不新增临时身份头
有值 A写入 A
有值 B写入 B
有值 A有值 BHeader A 优先

userTempId 没有 Redis 校验和 IP 校验,这是匿名购物车标识,不应被当成登录认证结果。

5. Gateway:身份头透传#

5.1 写入 userId#

源码节选:

  • 路径:duolaimall-gateway/src/main/java/com/cskaoyan/mall/gateway/filter/AuthFilter.java
  • 类与方法:AuthFilter#filter
ServerHttpRequest.Builder builder =
request.mutate();
if (StringUtils.isNotBlank(userId)) {
builder.header("userId", userId);
}

只有 hasLogin 返回非空 userId 时,Gateway 才主动写入认证身份。

5.2 写入 userTempId#

String userTempId =
request.getHeaders().getFirst("userTempId");
if (StringUtils.isBlank(userTempId)) {
HttpCookie cookie =
request.getCookies()
.getFirst("userTempId");
if (cookie != null) {
userTempId = cookie.getValue();
}
}
if (StringUtils.isNotBlank(userTempId)) {
builder.header("userTempId", userTempId);
}

最后必须把变更后的 request 放回 exchange:

ServerHttpRequest newRequest = builder.build();
ServerWebExchange newExchange =
exchange.mutate()
.request(newRequest)
.build();
return chain.filter(newExchange);

如果只构造 newRequest,却仍把原 exchange 传给 chain.filter,下游就看不到新 Header。当前代码正确地传递了 newExchange。

6. 配置、风险与运行边界#

Gateway 的仓库内 application.yml 只能确认服务会从 Nacos 导入公共配置和服务专属配置。当前源码依赖:

  • authUrls.url:一个 PathPattern 字符串;
  • Redis / Redisson 连接参数;
  • Gateway 路由、前缀和路径改写规则;
  • 服务注册与发现。

这些外部值本轮均未读取,因此不能根据 Controller 路径反推出最终公网路径,也不能宣称所有敏感接口已被 authUrls.url 覆盖。

7. 短复习点#

  1. 路径匹配使用一个 PathPattern 字符串,不是代码内维护的多模式列表。
  2. Token 优先从 Header 读取,Header 为空时才读取 Cookie;无效 Header 不会回退有效 Cookie。
  3. hasLogin 用 null、"-1" 和 userId 字符串表达三种结果。
  4. userId 来自 Redis 会话;userTempId 来自客户端,只能作为匿名购物车标识。
  5. 下游必须接收 newExchange,否则 Gateway 新增的 Header 不会被转发。

8. 关键源码导航#

阅读顺序文件关键位置作用
1duolaimall-gateway/src/main/java/com/cskaoyan/mall/gateway/filter/AuthFilter.javafilter鉴权主流程与请求变更
2同上judgeNeedLoginByUrlPathPattern 匹配
3同上hasLoginToken、Redis 与 IP 校验
4duolaimall-user/user-api/src/main/java/com/cskaoyan/mall/user/dto/UserLoginInfoDTO.javaDTOGateway 读取的会话值
5duolaimall-common/common-util/src/main/java/com/cskaoyan/mall/common/util/IpUtil.javagetGatwayIpAddressReactive 请求 IP 提取

9. 本章总结#

Gateway 完成的是基础认证和身份恢复;订单归属、地址归属等资源级授权仍由业务服务负责。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

多来买:Gateway 鉴权与身份透传实战
https://firefly-mu-weld.vercel.app/posts/duolaimai-gateway-auth-identity/
作者
Daisy
发布于
2026-08-03
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Daisy
Hello, I'm Daisy.
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签

文章目录