管理主页分类

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

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

多来买:鉴权安全边界与演进

3450 字
17 分钟
多来买:鉴权安全边界与演进

多来买:鉴权安全边界与演进#

核心问题:当前鉴权实现在哪些边界上可信,哪些只是候选风险,应该怎样安排演进优先级
事实来源:user、gateway、common 及下游身份读取代码
证据口径:代码已确认;风险未做运行复现,演进方案不是当前已实现能力

认证链路的价值不只在于“能登录”,还在于明确谁可以制造正式身份、身份在什么条件下失效、下游能信任什么。本文把错误响应、IP 绑定、下游读取、异常与并发边界集中到一处,避免与登录和 Gateway 主流程重复。

1. 身份可信边界#

当前代码在“有效会话存在”时会用 Redis userId 写入 Header。

但是在“公开接口 + 无有效会话”场景中,它没有主动删除客户端原本提交的 userId Header。

由于 request.mutate 会保留原始 Header,代码存在以下候选风险:

GET <公开路径> HTTP/1.1
userId: <客户端伪造值>

若该请求被匿名放行,并且下游公开接口直接信任 userId Header,就可能把伪造值当成身份。

受保护路径在 userId 为 null 时会被拦截,所以风险主要落在“公开但会根据可选 userId 改变行为”的接口。

更稳健的边界通常是:

  1. 进入 Gateway 后先移除外部 userId。
  2. 只有会话验证成功后才重新写入服务端恢复的 userId。
  3. 通过网络隔离或服务间认证防止绕过 Gateway 直连业务服务。

这是从当前 Header 处理分支得出的候选风险,本轮没有构造请求复现。

2. Gateway 鉴权失败响应#

2.1 out 方法#

源码节选:

  • 路径:duolaimall-gateway/src/main/java/com/cskaoyan/mall/gateway/filter/AuthFilter.java
  • 类与方法:AuthFilter#out
Result<Object> result =
Result.build(null, resultCodeEnum);
byte[] bits =
JSONObject.toJSONString(result)
.getBytes(StandardCharsets.UTF_8);
DataBuffer wrap =
response.bufferFactory().wrap(bits);
response.getHeaders().add(
"Content-Type",
"application/json;charset=UTF-8"
);
return response.writeWith(Mono.just(wrap));

两个业务码来自 ResultCodeEnum:

业务码message触发条件
204非法请求Redis 会话 IP 与当前 IP 不一致
208未登陆受保护路径没有有效会话

这里的 204 是 JSON 中的业务 code,不等同于 HTTP 204 No Content。

2.2 HTTP 状态码边界#

out 方法没有调用 response.setStatusCode。

因此,当前客户端主要依赖 Result.code 区分鉴权错误;实际 HTTP 状态是否保持默认值,本轮没有运行确认。

不能把当前实现描述成“Gateway 返回 HTTP 401”,因为代码里没有设置 401。

这正是本项目与很多 JWT 教程实现的一个明显差异。

3. IP 绑定#

3.1 登录阶段如何取 IP#

UserController 调用:

String ip =
IpUtil.getIpAddress(request);

Servlet 版本的选择顺序可以概括为:

x-forwarded-for
-> Proxy-Client-IP
-> WL-Proxy-Client-IP
-> request.remoteAddr
-> 如果是 IPv4 回环地址,则尝试本机网卡地址
-> 多代理时取逗号前第一段

3.2 Gateway 阶段如何取 IP#

Gateway 调用:

String nowIp =
IpUtil.getGatwayIpAddress(request);

Reactive 版本还会依次检查:

  • HTTP_CLIENT_IP;
  • HTTP_X_FORWARDED_FOR;
  • X-Real-IP;
  • 最后使用 remoteAddress。

登录端和网关端的提取规则并非完全相同。

3.3 IP 绑定带来的收益#

如果攻击者只拿到了 Token,但请求来源 IP 与登录 IP 不同,Gateway 会返回非法请求。

这为 Token 泄漏增加了一道使用条件。

3.4 IP 绑定的边界#

场景可能影响
移动网络切换、家庭宽带重拨合法用户 IP 改变,Token 可能被拒绝
多级代理头格式差异登录端与 Gateway 解析结果可能不同
转发头可由公网客户端直接伪造IP 绑定的可信度下降
登录端与 Gateway 使用不同 Header 顺序同一请求链可能得到不同字符串
第一段 IP 带空格当前两处代码没有统一 trim
Gateway remoteAddress 为空末级读取可能出现异常
Redis 中 ip 字段为空ip.equals 可能出现空指针

生产环境通常需要由受信任代理统一清洗并重写转发头,应用只信任约定好的来源。

当前仓库没有部署拓扑和可信代理配置证据,因此不能确认 IP 绑定在真实网络中的有效性。

4. 下游如何读取身份#

4.1 AuthContext 不是 ThreadLocal#

源码:

  • 路径:duolaimall-common/common-util/src/main/java/com/cskaoyan/mall/common/util/AuthContext.java
  • 类:AuthContext
public static String getUserId(
HttpServletRequest request
) {
String userId =
request.getHeader("userId");
return StringUtils.isEmpty(userId)
? ""
: userId;
}
public static String getUserTempId(
HttpServletRequest request
) {
String userTempId =
request.getHeader("userTempId");
return StringUtils.isEmpty(userTempId)
? ""
: userTempId;
}

AuthContext 只是请求头读取工具:

  • 不保存 ThreadLocal;
  • 不查询 Redis;
  • 不验证 Token;
  • 缺失时返回空字符串,不返回 null。

因此它依赖一个前提:请求确实经过可信 Gateway,而且 userId Header 已经过正确清洗与重建。

4.2 购物车的实际消费入口#

源码节选:

  • 路径:duolaimall-cart/cart-service/src/main/java/com/cskaoyan/mall/cart/controller/CartController.java
  • 类与方法:CartController#cartList
String userId =
AuthContext.getUserId(request);
String userTempId =
AuthContext.getUserTempId(request);
List<CartInfoDTO> cartList =
cartService.getCartList(
userId,
userTempId
);

这就是身份透传与购物车合并链路的连接点:

  • 登录用户有 userId;
  • 游客可能只有 userTempId;
  • 刚登录用户可能同时携带两者;
  • CartService 再决定正式购物车和临时购物车如何合并。

4.3 秒杀的实际消费入口#

PromoController 的多个受保护接口调用 AuthContext.getUserId。

例如获取秒杀下单码时,代码从请求头取得 userId,再把它用于生成用户相关下单码。

这说明 Gateway 的 userId 不只是展示信息,而会进入后续业务标识计算。

因此,身份头的可信来源和服务是否可绕过 Gateway 是整个系统的重要安全边界。

5. 配置入口#

5.1 仓库内 application.yml 能确认什么#

user-service 与 server-gateway 都声明:

  • spring.application.name;
  • dev profile;
  • Nacos discovery;
  • Nacos config;
  • 服务专属配置导入;
  • common.yaml 导入。

脱敏后的结构语义如下:

spring:
application:
name: <service-user 或 server-gateway>
profiles:
active: dev
cloud:
nacos:
discovery:
server-addr: <由本机或环境配置>
config:
server-addr: <由本机或环境配置>
file-extension: yaml
config:
import:
- nacos:<service-name>-<profile>.yaml
- nacos:common.yaml

这是用途示意,不复制仓库中的具体地址、账号或密码。

5.2 本链路依赖的外部属性#

属性语义消费位置用途
authUrls.urlAuthFilter单个受保护路径模式
spring.redis.host / port / passwordRedissonConfig创建单机 RedissonClient
hutool.snowflake.workerIdHutoolIdUtil雪花节点标识
hutool.snowflake.datacenterIdHutoolIdUtil雪花数据中心标识
Gateway routesNacos 专属配置将入口请求路由到 user-service
数据源配置user-service 外部配置查询 user_info

本轮没有读取 Nacos 中的实际值,也没有连接任何外部基础设施。

6. 安全、并发与异常边界#

6.1 已确认实现与边界矩阵#

维度当前实现能解决什么不能证明或存在的边界
密码存储无盐 MD5 后查库不直接用明文条件查询抗离线爆破能力弱
Token雪花 ID 字符串通常唯一、生成快不是高熵安全随机令牌
会话Redis RBucket,TTL 2h可集中失效、自动过期每次请求依赖 Redis
退出删除当前 Token Key服务端立即失效只读 Header,GET 改状态
IP 绑定登录 IP 与请求 IP 精确相等降低异地盗用可能代理、NAT、移动网络边界
路径保护一个外部 PathPattern集中判断受保护路径Nacos 值与覆盖范围未验证
正式身份Gateway 写 userId下游免查 Redis公开匿名请求未清除原 userId
临时身份Header/Cookie 透传支持游客购物车userTempId 本身不可信
错误响应Result 业务码前端统一解析未显式设置 HTTP 401
多端登录每个 Token 独立 Key可同时保留多会话没有单点登录或全端退出
会话续期固定有效期简单活跃用户也不会滑动续期
输入校验直接解引用 DTO代码短null、空白、格式均未显式校验
数据唯一性selectOne期待唯一匹配仓库没有唯一索引证据
CORS任意 Origin Pattern、方法、Header,允许凭据开发联调方便生产边界过宽
服务访问边界依赖 Gateway 透传头入口逻辑集中未发现部署隔离证据

6.2 Redis 不可用时会怎样#

Gateway 的 hasLogin 直接调用 bucket.get,代码中没有:

  • 本地会话缓存;
  • Redis 异常降级;
  • 超时后的匿名回退;
  • 熔断或隔离逻辑;
  • 对受保护接口的备用认证源。

安全上不应该在 Redis 异常时默认放行。

当前异常最终如何呈现还取决于 Gateway 全局异常处理,本轮没有运行验证。

6.3 登录并发#

多个并发登录请求之间没有共享用户级锁,也没有按 userId 覆盖旧会话。

正常情况下,每个请求取得不同雪花 ID并写不同 Key,互不覆盖。

真正需要关注的是雪花节点配置是否唯一。如果不同实例错误使用同一节点标识并产生相同 ID,后写入会覆盖同名 Key。

这是一项配置层候选风险,不是当前已复现问题。

6.4 Redis 会话与数据库状态不同步#

会话建立后,Gateway 只读取 Redis,不再查询 user_info。

如果用户账号随后被禁用、删除或权限变化:

  • 当前 Token 仍可能在 TTL 内有效;
  • 除非业务主动删除会话 Key;
  • 或下游每次重新校验账号状态。

当前 UserInfo 没有看到登录状态字段参与认证,仓库也没有全局强制下线链路证据。

6.5 全局异常处理#

user-service 扫描 common 包,common-service 中 GlobalExceptionHandler 会把普通 Exception 转成 Result.fail。

这意味着空请求体、passwd 为 null、selectOne 多结果等异常有机会被统一业务响应接管。

但它会调用 e.printStackTrace,且当前文档没有运行确认具体 HTTP 状态与序列化结果。

不能把“存在异常处理器”写成“所有异常场景已验证可控”。

7. 当前候选风险清单#

下面只记录静态代码中有明确触发机制的候选风险,不代表已经运行复现。

P0:身份可信边界#

  1. 公开匿名请求没有主动移除外部 userId Header,下游若信任该值可能接收伪造身份。
  2. 是否能绕过 Gateway 直接访问业务服务没有部署证据。
  3. authUrls.url 的真实模式未读取,不能证明所有敏感接口均受保护。
  4. authUrls.url 当前只按单个字符串解析,没有多模式列表逻辑。

P1:安全与会话一致性#

  1. 密码使用无盐 MD5。
  2. Token 使用雪花 ID,不是密码学安全随机值。
  3. IP 解析信任多个转发头,但没有可信代理名单或统一清洗证据。
  4. Gateway 支持 Cookie Token,logout 只支持 Header Token。
  5. 会话固定两小时且不续期。
  6. 同一用户可以保留多个会话,没有全端退出。
  7. Redis 会话建立后不会自动感知用户状态变化。
  8. CORS 对来源、方法和 Header 较宽,同时允许凭据。

P2:工程契约与异常表达#

  1. 登录 DTO 复用字段过多,且没有 @Valid 或空值校验。
  2. 响应 nickName 实际取 UserInfo.name,字段语义不一致。
  3. 七天 USERKEY_TIMEOUT 常量未被登录逻辑使用,容易误读。
  4. UserConstants 与 RedisConst 重复声明登录常量。
  5. Header 名称在多处直接写字符串,USER_LOGIN_TOKEN_HEADER 未实际复用。
  6. 网关错误只写业务码,未显式设置 HTTP 401。
  7. hasLogin 用 “-1” 字符串表达 IP 异常,状态类型不够明确。
  8. PathPattern 每次请求重新解析,错误配置可能直接影响过滤流程。
  9. 登录端与 Gateway 的 IP 提取规则不完全一致。
  10. 当前没有自动化测试覆盖登录、过期、Cookie、伪造 Header 与代理场景。

8. 如果未来演进,优先级如何判断#

本文不实施代码修复,只从现有边界给出可讨论的演进顺序。

第一层:先封住身份边界#

  • Gateway 先移除外部 userId,再按有效会话重新写入。
  • 敏感接口只允许通过 Gateway 或采用服务间认证。
  • 对 authUrls 的受保护路径建立集中、可审查的配置与测试。

第二层:提升凭据安全#

  • 使用高熵随机 Token,或引入明确设计的认证令牌方案。
  • 用 BCrypt、Argon2id 或 PBKDF2 代替 MD5。
  • 加入登录限流、失败审计和账号状态校验。
  • 明确 Header 与 Cookie 的唯一契约;如果用 Cookie,需要定义安全属性与 CSRF 策略。

第三层:完善会话治理#

  • 明确固定 TTL 还是滑动 TTL。
  • 明确单设备、多设备、踢下线和全端退出语义。
  • 建立 userId 到 Token 的反向索引或会话集合。
  • 设计账号禁用后的会话失效机制。
  • 设计 Redis 高可用与认证失败时的安全策略。

这些是设计讨论,不属于当前项目已实现功能。

9. 四条短复习点#

  1. 认证只回答“请求对应哪个用户”,订单、地址等资源归属仍需下游授权。
  2. 外部 userId 是客户端可控输入;可信身份必须由 Gateway 验证会话后重新写入。
  3. IP 绑定能降低异地盗用概率,但会受代理、NAT 和移动网络变化影响。
  4. 改进优先级应先封住身份边界,再升级密码和 Token,最后完善会话治理与可用性。

10. 关键源码导航#

文件关键位置本篇关注点
duolaimall-gateway/src/main/java/com/cskaoyan/mall/gateway/filter/AuthFilter.javafilterhasLoginoutHeader 信任、错误响应、Redis 与 IP 分支
duolaimall-common/common-util/src/main/java/com/cskaoyan/mall/common/util/IpUtil.javaServlet / Gateway 两套方法转发头与来源 IP 解释
duolaimall-common/common-util/src/main/java/com/cskaoyan/mall/common/util/AuthContext.javagetUserIdgetUserTempId下游读取请求头
duolaimall-cart/cart-service/src/main/java/com/cskaoyan/mall/cart/controller/CartController.java身份读取入口正式购物车与临时购物车
duolaimall-promo/promo-service/src/main/java/com/cskaoyan/mall/promo/controller/PromoController.java身份读取入口秒杀业务使用 userId

11. 本章总结#

本文中的 P0/P1/P2 是静态审查优先级,不代表漏洞已经被运行利用;是否能绕过 Gateway 还取决于部署网络边界。

文章分享

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

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

文章目录