当前位置: 首页 > news >正文

HandlerIntercepter,JWT和杂项知识点

一、 HandlerInterceptor (Spring MVC 的智能请求处理中枢)

它不仅仅是一个“保安”,更像是一个嵌入在 Spring MVC 内部工作流中的、拥有上下文感知能力的**“流程经理”**。它与只能在大门外检查包裹的 Filter 不同,这位“流程经理”深入内部,知道请求要交给哪个具体的 Controller 方法处理,因此能进行更精细化的业务操作。

  • 工作流程详解:

    1. boolean preHandle(request, response, handler)请求的准入与预处理阶段

      • 执行时机:在 Spring 的DispatcherServlet将请求分发给你的 Controller 方法之前,它是第一道(也是最重要的一道)关卡。

      • 核心任务:进行前置条件判断。这是执行业务逻辑前的最后防线。

        • 认证 (Authentication):检查用户是否登录。典型的逻辑是:从 request.getHeader("Authorization") 中获取 Token,然后调用如 JwtUtil.parseJWT(...) 的方法进行验证。

        • 授权 (Authorization):在认证通过后,进一步检查用户权限。例如,从解析出的 Token Claims 中获取用户角色,判断其是否有权访问 handler 所代表的目标方法(例如,普通用户禁止访问标有 @AdminOnly 注解的方法)。

      • 关键返回值

        • return true;:准予放行。请求会继续流转到下一个拦截器(如果有的话),或者最终到达 Controller

        • return false;立即中断。请求到此为止,不会再继续。此时你必须通过 response 对象给客户端一个明确的答复,否则客户端会一直等待直到超时。例如:response.setStatus(401); 并写入提示信息。

    2. void postHandle(request, response, handler, modelAndView)业务逻辑的后处理与视图增强阶段

      • 执行时机:在你的 Controller 方法执行完毕,但 Spring 的视图解析器(ViewResolver)还未渲染最终页面(如HTML)之前。

      • 核心任务:对 Controller 的处理结果进行补充和修改

        • 修改模型数据Controller 可能只返回了核心业务数据。在这里,你可以向 modelAndView.addObject(...) 中添加所有页面都需要的公共数据,比如页面顶部的导航栏分类、当前用户名、购物车商品数量等。这避免了在每个 Controller 方法中重复编写获取这些公共数据的代码。

        • 修改视图名称:在特定条件下,你甚至可以改变原定要渲染的视图,modelAndView.setViewName("another-page");

      • 注意:如果 Controller 方法执行中抛出了异常,postHandle不会被执行。

    3. void afterCompletion(request, response, handler, ex)请求全流程的终结与清理阶段

      • 执行时机:在整个请求处理流程完全结束,即视图已经成功渲染并发送给客户端之后。

      • 核心任务:执行资源清理不影响主流程的收尾工作

        • 性能监控:在 preHandle 中记录一个开始时间戳并存入 request,在此处取出时间戳,与当前时间相减,得到整个请求的总耗时,并记录到日志中。

        • 异常日志ex 参数是此方法的一个关键。如果 Controller 或视图渲染过程中发生了异常,ex 对象就不会为 null。你可以据此记录详细的、全局统一的异常日志,而无需在每个 Controller 中都写 try-catch

        • 资源释放:如果 preHandle 中获取了某些需要手动释放的资源(例如线程局部变量 ThreadLocal),这里是执行清理操作最安全的地方,因为它总会被调用(只要 preHandle 返回了 true)。


二、 JWT (JSON Web Token - 无状态认证的基石)

JWT 本质上是一个经过加密签名、URL安全的字符串,其结构为 Header.Payload.Signature,三部分由 . 分隔。

  • 组成结构详解:

    1. Header (头部):一个JSON对象,描述了JWT的元数据。经过 Base64Url 编码后形成第一部分。

      • alg: 签名算法,例如 "HS256"。

      • typ: 令牌类型,固定为 "JWT"。

    2. Payload (载荷):一个JSON对象,存放了实际需要传递的数据,也称为 Claims (声明)。经过 Base64Url 编码后形成第二部分。注意:Payload中的信息是可被破译的(Base64可逆),绝不能存放敏感信息如密码!

      • Registered Claims (标准声明):建议使用但不强制的预定义声明,如 iss (签发者), exp (过期时间戳), sub (主题)。

      • Private Claims (私有声明):这是我们自定义的数据,例如 userId: 1001, username: "zhangsan", role: "ADMIN"

    3. Signature (签名):JWT安全性的核心。它是将编码后的 HeaderPayload 和一个仅服务器端知道的 secretKey (密钥),通过指定的签名算法(alg)混合加密生成的。

      • 作用:① 验证发送者:只有拥有正确密钥的服务器才能生成有效的签名。② 防篡改:接收方收到JWT后,会用相同的密钥和算法重新计算签名。如果计算出的签名与JWT自带的签名不一致,说明中间有人篡改了 HeaderPayload,该JWT将被视为无效。


三、 Spring Boot 核心自动化魔法

  • @ConfigurationProperties (智能配置绑定器)

    • 深度作用:它不仅仅是绑定,更是实现了配置与代码的强类型关联。你将零散、易错的字符串配置,转化为了一个结构清晰、有完整数据类型的Java对象。这使得你在代码中调用 jwtProperties.getTtlMillis() 时,得到的是一个 long 类型,而不是一个需要手动转换的 String,极大地提升了代码的健壮性和可维护性。

    • 启用机制详解 (@Component vs @Enable...)

      • Spring的世界由一个个 Bean 组成。一个普通的Java类(POJO)对于Spring来说是不可见的。

      • @Component 是最通用的方式,它告诉Spring:“把我变成一个Bean,纳入你的管理体系。”

      • @EnableConfigurationProperties(MyProps.class) 是一种更专门、更推荐的方式,它清晰地表达了意图:“请专门为这个配置类MyProps激活属性绑定功能,并将它注册为一个Bean。” 这使得配置的启用更加集中和可控。

  • 反射 (Reflection - 现代框架的灵魂)

    • 核心思想:赋予Java在程序运行时检查和操作自身的能力。正常代码在编译时就确定了要调用什么方法,而反射则允许在运行时再做决定。

    • 为何不可或缺:如果没有反射,Spring Binder 就无法知道你的 JwtProperties 类里到底有哪些字段和方法。为了实现自动绑定,Spring的开发者可能就得要求你:

      • 继承特定基类public class JwtProperties extends AbstractProperties { ... },通过父类方法来注册属性。

      • 实现特定接口public class JwtProperties implements Configurable { ... },实现接口中定义的方法来接收属性。

      • 使用XML大量配置:回到“远古时代”,在XML文件中手动声明每个类和每个属性的映射关系。

    • 反射带来的解放:通过反射,Spring可以直接在运行时“读取”你的类,发现 setAdminTokenName 方法,然后“动态地”调用它。这让你只需编写一个干净的POJO类和几个注解,所有繁杂的连接工作都由框架在幕后通过反射自动完成。


深度复习思考题 

  1. 职责与容错:在记录用户查看订单的日志时,为什么 afterCompletion 是比直接在Controller方法中更好的选择?请从以下三个角度深入分析:

    • 单一职责原则Controller 的核心职责应该是什么?记录日志是它的核心职责吗?

    • 事务与一致性:如果 Controller 方法的主要逻辑(比如扣减库存)成功执行并提交了数据库事务,但紧接着的日志记录操作因为数据库日志表的问题失败了,会发生什么?这种设计是否理想?

    • 可扩展性:如果未来新增了“查看商品详情”、“查看文章详情”也需要记录日志,你的日志逻辑应该如何设计才能避免在每个新的Controller中复制粘贴代码?

  2. JWT安全攻防:一名攻击者通过某种手段获取了你签发JWT时使用的 secretKey。请详细描述一次攻击者伪造管理员身份的步骤:

    1. 他会如何构造一个恶意的 Payload (载荷)?

    2. 他会如何利用偷来的 secretKey 为这个恶意Payload生成一个看起来完全合法的 Signature (签名)?

    3. 为什么你的服务器在收到这个伪造的 token 后,parseJWT 方法会验证通过,从而让攻击者得逞?

  3. 配置的“层叠”艺术:你的Java代码中 private long ttlMillis = 3600000L; (1小时) 设置了默认值。你的 application.yml 中配置了 jwt.user.ttl-millis: 7200000 (2小时)。而你的运维同事在启动服务器时,使用了环境变量 JWT_USER_TTLMILLIS=600000 (10分钟)。请问:

    • 应用最终使用的 ttlMillis 是多少?

    • 这个结果体现了 Spring Boot 配置加载的哪两个重要特性?(提示:一个关于命名,一个关于优先级)

  4. 反思“反射”:如果Java语言没有提供“反射”功能,请你设想一下,一个现代Web框架可能会如何设计,来实现配置的自动注入?请描述一种可能的替代方案(例如,基于注解处理器在编译期自动生成代码,或者基于特定的XML映射文件),并简要分析其与反射方案相比的优缺点。

  5. 串联完整故事:请你用尽可能详细的语言,以第一人称(“我是一个HTTP请求”)的视角,讲述一次完整的JWT认证之旅:

    • 第一幕:登录获取令牌 - “我”是一个POST /login请求,带着用户名和密码...服务器后台发生了什么?createJWT如何为我打造了一张“身份证”?服务器最后是如何把这张“身份证”交给我的?

    • 第二幕:携带令牌访问 - 现在“我”是一个GET /api/me的请求,要去获取个人信息。我是如何携带我的“身份证”的?

    • 第三幕:关卡验身 - 当“我”到达服务器时,HandlerInterceptor是如何拦下我的?preHandle方法里,parseJWT如何查验我的“身份证”?最终,我是如何被放行,并成功到达Controller的?

深度复习思考题 - 答案详解 

问题一:为何日志记录在 Interceptor 中优于在 Controller 中?

答案详解: 将日志记录放在InterceptorafterCompletion中,而不是直接写在Controller方法的最后,是更优的架构设计,主要基于以下三点考虑:

  1. 职责分离 (Single Responsibility Principle)

    • Controller的核心职责是处理核心业务逻辑。例如,getOrderById方法的职责是:接收请求、验证参数、调用服务层获取订单数据、将数据打包并返回。它的工作应该像一个专注的“业务专家”。

    • Interceptor的核心职责是处理“横切关注点”(Cross-Cutting Concerns),即那些会散布在多个业务模块中的通用功能,日志、权限、事务都是典型的横切关注点。它应该像一个“流程总管”。

    • 结论:将日志逻辑放入Interceptor,可以让Controller保持纯粹和整洁,只关注业务。这使得代码结构更清晰,日后无论是修改业务逻辑还是修改日志逻辑,都只需要去各自专属的地方,互不干扰,维护成本大大降低。

  2. 事务与容错性 (Transaction & Fault Tolerance)

    • Controller方法通常被声明式事务(如@Transactional)所包裹。如果日志记录代码在Controller方法的最后,它也被包含在了这个事务中。

    • 风险在于:假设获取订单的核心业务成功了,数据库事务正准备提交,但此时日志系统突然故障,导致日志写入失败并抛出异常。这会引起整个事务回滚,从而导致用户看到一个“获取订单失败”的错误,而实际上订单数据本身是正常的。为了一个非核心的日志功能,导致核心功能失败,这是非常不理想的设计。

    • afterCompletion的优势:它在Controller的事务完全提交之后才执行。这意味着,核心业务的成功与否和日志记录的成功与否是解耦的。核心业务成功了,就应该先让用户得到成功的响应。日志记录作为善后工作,即使失败了,也只影响日志系统本身,可以通过我们之前讨论的“消息队列”等方式进行补救,绝不应该影响到主流程的用户体验。

  3. 可扩展性与复用性 (Extensibility & Reusability)

    • 如果今天在getOrderById里写了日志代码,明天产品经理说“查看商品详情也要记录日志”,你就必须把类似的代码复制一份到getProductById这个Controller里。未来再有“查看文章”,就再复制一份。这会导致代码大量重复,一旦日志格式需要修改,你需要去几十个地方同步更新,这是灾难性的。

    • Interceptor的方案:你只需要编写一个AuditLoggingInterceptor,然后通过配置,让它拦截所有符合特定规则的URL(例如 /**/get* 或所有标有 @Loggable 注解的方法)。未来新增任何需要日志记录的Controller,只要符合这个规则,就自动被纳入了日志体系,完全不需要在新Controller中添加任何代码。这才是“一次编写,处处生效”的优雅设计。

问题二:JWT secretKey 泄露的后果是什么?

答案详解: secretKey (密钥) 是JWT安全体系的绝对命脉。一旦泄露,整个基于JWT的认证系统将形同虚设,攻击者可以完美地伪造任何用户的身份

一次典型的攻击步骤如下:

  1. 构造恶意载荷 (Craft Malicious Payload):攻击者首先会用文本编辑器创建一个JSON对象,声称自己是管理员或任何他想冒充的用户。这个载荷会包含所有必要的字段,并且他会把过期时间设置得非常长。

    JSON

    {"userId": 1001,"username": "I_am_the_hacker","role": "ADMIN",  // <-- 关键的提权!"exp": 2147483647 // <-- 设置一个遥远的未来作为过期时间
    }
    
  2. 生成合法签名 (Generate Valid Signature):攻击者将上述载荷和JWT的Header部分进行Base64Url编码,然后用 . 连接起来。最关键的一步是,他使用**偷来的那个secretKey**和服务器所使用的完全相同的签名算法(例如HS256),对这个连接后的字符串进行加密签名,生成一个Signature

  3. 服务器被欺骗 (Server is Fooled):攻击者将他伪造的 header.payload.signature 作为一个完整的token,放入Authorization请求头中,发送给服务器的受保护接口。

    • 服务器的Interceptor接收到这个token后,调用parseJWT方法进行验证。

    • parseJWT方法执行内部验证逻辑:它取出headerpayload,然后用自己**保存在服务器上的secretKey**去重新计算签名。

    • 致命的问题出现了:因为攻击者使用的密钥和服务器的密钥是完全一样的,所以服务器计算出的签名,将和攻击者伪造的token里附带的签名一模一样

    • 签名验证通过,过期时间也未到期。parseJWT方法认为这是一个完全合法的token,便成功返回了解析出的Claims(其中roleADMIN)。

    • 后续的业务逻辑根据这个Claims进行授权判断,最终赋予了攻击者管理员权限。攻击宣告成功。

结论secretKey必须被当作最高机密来保护,绝不能硬编码在代码中,应使用安全的方式(如环境变量、配置中心加密存储)进行管理。

问题三:配置的优先级是什么?

答案详解:

  • 最终值 (Final Value):应用最终使用的 ttlMillis 将是 600000 (10分钟)。

  • 体现的特性 (Features Demonstrated)

    1. 外部化配置及其优先级 (Externalized Configuration & Priority Order):这是Spring Boot最强大的特性之一。它允许配置从代码中分离出来,并且可以从多个来源加载,这些来源形成了一个严格的优先级“层叠”顺序。一个高优先级的配置源中的属性,会覆盖掉一个低优先级配置源中的同名属性。

      • 在这个场景中,优先级从高到低是:环境变量 > application.yml > Java代码中的默认值

      • 因此,环境变量 JWT_USER_TTLMILLIS=600000 的优先级最高,它覆盖了application.yml中的7200000;而application.yml的值又覆盖了代码中的默认值3600000。所以最终生效的是最高优先级的值。

    2. 宽松命名绑定 (Relaxed Binding):这个特性让不同环境下的配置命名更加灵活。环境变量通常要求大写字母和下划线(JWT_USER_TTLMILLIS),而.yml文件推荐使用烤串式(jwt.user.ttl-millis),Java代码则使用驼峰式(ttlMillis)。Spring Boot的Binder能够智能地将这些不同的风格识别并绑定到同一个Java字段上,极大地方便了开发者和运维人员。

问题四:如果没有“反射”,框架会怎么样?

答案详解: 如果Java语言没有提供“反射”功能,像Spring这样依赖“约定优于配置”和注解驱动的框架,其设计将发生根本性的改变,自动化程度会大大降低。

一种最可能的替代方案是编译期代码生成 (Compile-Time Code Generation),通过**注解处理器 (Annotation Processor)**来实现。

  • 替代方案工作方式

    1. 你依然编写你的JwtProperties类,并用@ConfigurationProperties注解。

    2. 在你点击“Build”或“Compile”项目时,Java编译器会调用一个你引入的、由Spring提供的注解处理器。

    3. 这个处理器会扫描你的代码,找到@ConfigurationProperties注解,然后自动生成一个新的Java源文件,例如JwtProperties_Binder.java。这个生成的文件里不包含任何反射代码,而是硬编码的、高性能的绑定逻辑,伪代码可能像这样:

      Java

      // Auto-generated by a processor
      public final class JwtProperties_Binder {public static void bind(JwtProperties target, ConfigSource source) {String val1 = source.getProperty("jwt.admin-token-name");if (val1 != null) {target.setAdminTokenName(val1);}String val2 = source.getProperty("jwt.user.ttl-millis");if (val2 != null) {target.setTtlMillis(Long.parseLong(val2)); // Hard-coded type conversion}}
      }
      
    4. 在应用运行时,Spring框架不再需要使用反射,而是直接调用这个已经生成好的、高性能的JwtProperties_Binder.bind()方法。

  • 优缺点分析

    • 优点

      • 性能极高:运行时没有反射带来的性能开销,启动速度会更快。

      • 错误提前暴露:一些配置错误(如类型不匹配)可以在编译期就被发现,而不是等到运行时才报错。

      • 更适合原生镜像:对于GraalVM等原生镜像技术非常友好,因为所有动态行为都转化为了静态代码。

    • 缺点

      • 构建过程变慢:编译期需要额外执行代码生成,增加了构建时间。

      • 灵活性降低:反射提供了极大的运行时动态能力,而编译期方案则相对固定。

      • 开发体验可能更复杂:IDE的配置、构建工具的整合、以及调试自动生成的代码,都可能带来额外的复杂性。

问题五:串联JWT认证的完整故事

答案详解: (以第一人称“我是一个HTTP请求”的视角)

第一幕:我的诞生与授权 —— 登录 “我”诞生于用户的浏览器,是一个POST /login请求。我的“身体”里携带着用户输入的用户名和密码。当我到达服务器后,DispatcherServlet把我交给了负责处理登录的LoginController

LoginController打开我的“身体”,拿出用户名和密码,去数据库里核对。核对成功后,Controller知道需要给我签发一张“数字身份证”以便我后续通行。于是,它调用了JwtUtil.createJWT()方法。

在这个方法里,一个专属于这位用户(比如userId: 1001)的Payload被创建了,并且盖上了一个“有效期至1小时后”的时间戳。然后,服务器拿出了它珍藏的、绝不示人的secretKey(绝密印章),用HS256算法,为我的HeaderPayload郑重地盖上了防伪签名。

最后,一张由三个部分组成的、长长的、独一无二的token字符串制作完成了。服务器没有把它自己存起来(这是无状态的关键!),而是把它放入了响应体中,随着200 OK的状态码一起,交还给了用户的浏览器。浏览器非常小心地把这张“身份证”保存在了本地存储(如LocalStorage)里。我的第一次旅程结束了。

第二幕:我的新任务 —— 携带令牌访问 片刻之后,用户点击了“我的资料”按钮。浏览器立刻创造了我的一个新化身:一个GET /api/me请求。这次,浏览器在出发前,把我上次获得的那张“身份证”token,郑重地放入了我的“护照夹”里,也就是Authorization请求头中,格式通常是 Bearer <长长的token字符串>。我出发了。

第三幕:我的身份验证 —— 关卡验身 当我再次抵达服务器时,一位精神抖擞的“流程经理”——JwtInterceptor拦住了我。它在preHandle方法里开始对我进行盘查。

它首先查看我的“护照夹” (Authorization 请求头),取出了我的“身份证”token。然后,它立刻调用了JwtUtil.parseJWT()这个“验身神器”,并递上了服务器自己的那把“绝密印章”secretKey

parseJWT对我进行了严格的检查:它用“绝密印章”重新计算了我身份证的签名,和我自带的签名完美匹配,确认我不是伪造的。接着,它检查了我的“有效期”,确认我没有过期。

一切无误!parseJWT满意地点了点头,把我身份证里的个人信息(Claims)提取出来,交给了InterceptorInterceptor一看,身份合法,于是preHandle方法返回了true

我被放行了!我继续前进,畅通无阻地到达了目的地UserControllerUserController从容地处理了我的请求,查询数据库,最终把我需要的所有个人资料返回给了浏览器。我的第二次任务,圆满完成。在此之后,只要我的“身份证”不过期,我就可以一次又一次地重复这第三幕的旅程。

http://www.lryc.cn/news/589737.html

相关文章:

  • LeetCode Hot 100 二叉树的最大深度
  • 【Java】【力扣】94.二叉树的中序遍历
  • C#获取当前系统账户是否为管理员账户
  • LeetCode经典题解:141、判断链表是否有环
  • LeetCode Hot100【4. 寻找两个正序数组的中位数】
  • C++之unordered_xxx基于哈希表(链地址法)的自我实现(难)
  • 逆向入门(39、40)程序逆向篇-DaNiEl-RJ.1、genocide1
  • 【LeetCode 热题 100】543. 二叉树的直径——DFS
  • STM32-RTC内部时钟
  • fastadmin会员单点登录
  • C#语法基础总结(超级全面)
  • flutter app内跳转到其他安卓 app的方法
  • HTML 入门教程:从零开始学习网页开发基础
  • HTML基础P1 | HTML基本元素
  • Android 升级targetSdk无法启动服务
  • APIs案例及知识点串讲(上)
  • FreeRTOS中断管理STM32
  • Java-74 深入浅出 RPC Dubbo Admin可视化管理 安装使用 源码编译、Docker启动
  • 【docker】将本地镜像打包部署到服务器上
  • LVS:高性能负载均衡利器
  • CVE-2005-4900:TLS SHA-1 安全漏洞修复详解
  • WIN10系统优化篇(一)
  • Samba服务器
  • 【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)
  • Vue 结合 Zabbix API 获取服务器 CPU、内存、GPU 等数据
  • Thymeleaf 基础语法与标准表达式详解
  • [Linux入门] Linux 账号和权限管理入门:从基础到实践
  • 如何通过扫描电镜检测汽车清洁度中的硬质颗粒并获取成分信息
  • 【云原生网络】Istio基础篇
  • 数字IC后端培训教程之数字IC后端项目典型问题解析