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

搞懂Spring Boot登录认证:从UUID到JWT,一次完整的架构推演

文章目录

  • 从UUID到JWT再到Filter/Interceptor:Spring Boot登录认证进阶之路
    • 1. 基础登录:模拟数据 + UUID令牌
      • 1.1 项目结构
      • 1.2 请求DTO
      • 1.3 Service——模拟用户与令牌管理
      • 1.4 Controller
      • 1.5 测试
    • 2. 从UUID到JWT:让令牌自带“身份证”
      • 2.1 有状态 vs 无状态对比
      • 2.2 添加JWT依赖
      • 2.3 编写JwtUtil工具类
      • 2.4 精简Service
    • 3. 踩坑:Bearer前缀与测试那些事
      • 3.1 另一个坑:JWT立即过期
    • 4. 过滤器Filter:第一道防线
      • 4.1 Filter的作用
      • 4.2 创建LoginCheckFilter(Spring Boot 3.x 版本)
      • 4.3 Filter的尴尬:异常无法被Spring全局捕获
    • 5. Interceptor登场:纳入Spring的异常体系
      • 5.1 Filter vs Interceptor
      • 5.2 自定义未授权异常
      • 5.3 编写LoginCheckInterceptor
      • 5.4 配置拦截器白名单
    • 6. 统一异常处理:@RestControllerAdvice
    • 7. 总结:一张清单回顾所有要点
    • 最后的话:

从UUID到JWT再到Filter/Interceptor:Spring Boot登录认证进阶之路

这篇文章要带你从零实现一个Spring Boot登录接口,并一步步将它从“临时UUID令牌”演变成无状态的JWT,再通过Filter → Interceptor → 统一异常处理,最终得到一个规范、可维护的认证架构。我们不依赖前端,只使用IDEA内置的HTTP Client做所有测试。所有代码都会给出,你可以复制即用。


1. 基础登录:模拟数据 + UUID令牌

我们先从最简单的入手:接收用户名密码,验证后返回一个临时令牌。所有用户数据先用HashMap硬编码在内存里,令牌就用UUID随机生成。

1.1 项目结构

src/main/java/com/example/demo ├── DemoApplication.java // 启动类 ├── config │ └── WebConfig.java // 配置拦截器、跨域等 ├── controller │ └── UserController.java // 登录、用户接口 ├── dto │ └── LoginRequest.java // 登录请求体 ├── exception │ ├── GlobalExceptionHandler.java // 全局异常处理 │ └── UnauthorizedException.java // 自定义未授权异常 ├── filter │ └── LoginCheckFilter.java // 登录校验过滤器(可选) ├── interceptor │ └── LoginCheckInterceptor.java // 登录校验拦截器 ├── service │ └── UserService.java // 用户服务(验证逻辑) └── util └── JwtUtil.java // JWT 工具类

1.2 请求DTO

// LoginRequest.javapublicclassLoginRequest{privateStringusername;privateStringpassword;// 必须有无参构造,Spring才能把JSON转成对象publicLoginRequest(){}// getter/setter 略}

注意:如果只有全参构造而没有无参构造,Spring反序列化时会直接报400,这是一个新手非常容易踩的坑。

1.3 Service——模拟用户与令牌管理

@ServicepublicclassUserService{// 模拟数据库中的用户privatestaticfinalMap<String,String>MOCK_USERS=newHashMap<>();static{MOCK_USERS.put("admin","123456");MOCK_USERS.put("user","password");}// 临时存储已登录的令牌(有状态方案)privatestaticfinalSet<String>TOKEN_STORE=ConcurrentHashMap.newKeySet();publicStringlogin(LoginRequestrequest){Stringpwd=MOCK_USERS.get(request.getUsername());if(pwd!=null&&pwd.equals(request.getPassword())){Stringtoken=UUID.randomUUID().toString();TOKEN_STORE.add(token);// 记住这个令牌returntoken;}returnnull;}publicbooleanisValidToken(Stringtoken){returntoken!=null&&TOKEN_STORE.contains(token);}}

1.4 Controller

@RestControllerpublicclassUserController{@AutowiredprivateUserServiceuserService;@PostMapping("/api/login")publicResponseEntity<?>login(@RequestBodyLoginRequestrequest){Stringtoken=userService.login(request);if(token!=null){returnResponseEntity.ok(Map.of("token",token));}else{returnResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("msg","用户名或密码错误"));}}}

1.5 测试

POST http://localhost:8080/api/login Content-Type: application/json { "username": "admin", "password": "123456" }

成功返回200和一个随机的UUID。虽然跑通了,但这个方案有两大问题:

  • 令牌随机,不携带任何用户信息,服务端必须维护一个TOKEN_STORE才知道谁是谁。
  • 有状态:一旦重启应用,所有登录状态全丢,扩展多实例时还需要共享存储。

2. 从UUID到JWT:让令牌自带“身份证”

我们希望令牌自己能“说话”,携带用户名和有效期,服务端不用再记——这就是无状态的JWT(Json Web Token)。

2.1 有状态 vs 无状态对比

方案状态存储位置优点缺点
UUID令牌有状态服务器内存/Redis实现简单扩展性差,内存占用
JWT无状态客户端本地服务端无需存储,自带用户信息,防篡改无法主动注销(需配合黑名单),payload仅Base64不加密

2.2 添加JWT依赖

pom.xml中加入:

<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.5</version><scope>runtime</scope></dependency>

2.3 编写JwtUtil工具类

publicclassJwtUtil{privatestaticfinalKeyKEY=Keys.secretKeyFor(SignatureAlgorithm.HS256);// 随机密钥privatestaticfinallongEXPIRATION_MS=3600_000;// 1小时publicstaticStringgenerateToken(Stringusername){Datenow=newDate();Dateexpiration=newDate(now.getTime()+EXPIRATION_MS);returnJwts.builder().setSubject(username)// 主题放用户名.setIssuedAt(now).setExpiration(expiration).signWith(KEY).compact();}publicstaticClaimsparseToken(Stringtoken){returnJwts.parserBuilder().setSigningKey(KEY).build().parseClaimsJws(token).getBody();}}

2.4 精简Service

@ServicepublicclassUserService{privatestaticfinalMap<String,String>MOCK_USERS=newHashMap<>();static{MOCK_USERS.put("admin","123456");MOCK_USERS.put("user","password");}// 不再需要 TOKEN_STORE !publicStringlogin(LoginRequestrequest){Stringpwd=MOCK_USERS.get(request.getUsername());if(pwd!=null&&pwd.equals(request.getPassword())){returnJwtUtil.generateToken(request.getUsername());}returnnull;}publicbooleanisValidJwt(Stringtoken){try{JwtUtil.parseToken(token);returntrue;}catch(Exceptione){returnfalse;}}}

Controller也相应调整:/api/info接口从请求头提取JWT并解析,获取用户名。此时我们会遇到一个重要的HTTP细节:Bearer前缀。


3. 踩坑:Bearer前缀与测试那些事

我们测试/api/info时,要求请求头写:

Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

如果你只写了Authorization: 你的token,服务器会认为格式错误,返回401。Bearer是一种认证方案标识,告诉服务器“后面跟的是持有者令牌”。解析时我们用substring(7)跳过了“Bearer ”这7个字符。

3.1 另一个坑:JWT立即过期

测试时我们故意把EXPIRATION_MS改成了10秒,想验证过期效果,结果发现怎么快都提示过期。排查后发现是过早复制了错误单位(比如写了1毫秒)。后来改成10_000就正常了。过期时间的单位必须是毫秒


4. 过滤器Filter:第一道防线

现在我们想统一校验所有需要登录的请求,而不是在每个Controller里重复写解析代码。首先想到的就是Servlet Filter

4.1 Filter的作用

Filter运行在Servlet容器层,在请求进入Spring MVC的DispatcherServlet之前执行,可以拦截任何资源。

4.2 创建LoginCheckFilter(Spring Boot 3.x 版本)

注意:Spring Boot 3.x 使用jakarta.servlet.*,2.x 是javax.servlet.*,下面的代码基于3.x。

@ComponentpublicclassLoginCheckFilterimplementsFilter{@OverridepublicvoiddoFilter(ServletRequestreq,ServletResponseres,FilterChainchain)throwsIOException,ServletException{HttpServletRequestrequest=(HttpServletRequest)req;HttpServletResponseresponse=(HttpServletResponse)res;Stringurl=request.getRequestURL().toString();if(url.endsWith("/api/login")){chain.doFilter(req,res);// 登录接口直接放行return;}StringauthHeader=request.getHeader("Authorization");if(!StringUtils.hasText(authHeader)||!authHeader.startsWith("Bearer ")){response.setContentType("application/json;charset=UTF-8");response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}");return;}Stringtoken=authHeader.substring(7);try{Claimsclaims=JwtUtil.parseToken(token);request.setAttribute("username",claims.getSubject());chain.doFilter(req,res);// 校验通过放行}catch(Exceptione){response.setContentType("application/json;charset=UTF-8");response.getWriter().write("{\"code\":401,\"msg\":\"Token无效\"}");}}}

这样Controller里的校验代码就可以删掉了,直接从request.getAttribute("username")取用户信息。

4.3 Filter的尴尬:异常无法被Spring全局捕获

Filter中一旦校验失败,我们只能手动拼接JSON并用response.getWriter()写回。这样不但繁琐,而且抛出的异常不会被Spring的@RestControllerAdvice捕获,因为Filter在Spring MVC的外层。这就引出了更优雅的方案:拦截器(Interceptor)


5. Interceptor登场:纳入Spring的异常体系

Interceptor是Spring MVC提供的拦截器,它位于DispatcherServlet之后、Controller之前,所以其抛出的异常可以被Spring的全局异常处理器捕获

5.1 Filter vs Interceptor

对比项FilterInterceptor
所处层次Servlet容器Spring MVC
能否被Spring异常处理
适用场景编码过滤、安全过滤登录校验、日志、权限

5.2 自定义未授权异常

publicclassUnauthorizedExceptionextendsRuntimeException{privateintcode=401;publicUnauthorizedException(Stringmsg){super(msg);}// getter}

5.3 编写LoginCheckInterceptor

@ComponentpublicclassLoginCheckInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){Stringurl=request.getRequestURL().toString();if(url.endsWith("/api/login")){returntrue;// 放行}StringauthHeader=request.getHeader("Authorization");if(!StringUtils.hasText(authHeader)||!authHeader.startsWith("Bearer ")){thrownewUnauthorizedException("未登录或Token格式错误");}Stringtoken=authHeader.substring(7);try{Claimsclaims=JwtUtil.parseToken(token);request.setAttribute("username",claims.getSubject());}catch(Exceptione){thrownewUnauthorizedException("Token无效或已过期");}returntrue;// 放行}}

5.4 配置拦截器白名单

@ConfigurationpublicclassWebConfigimplementsWebMvcConfigurer{@AutowiredprivateLoginCheckInterceptorloginCheckInterceptor;@OverridepublicvoidaddInterceptors(InterceptorRegistryregistry){registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/api/login");// 登录接口不拦截}}

这样我们将LoginCheckFilter注释掉,完全由拦截器接管JWT校验,并且校验失败时抛出的UnauthorizedException会被接下来要写的全局异常处理器兜底。


6. 统一异常处理:@RestControllerAdvice

有了自定义异常,我们就可以集中管理所有错误响应,确保前端收到统一的JSON结构。

@RestControllerAdvicepublicclassGlobalExceptionHandler{@ExceptionHandler(UnauthorizedException.class)publicResponseEntity<Map<String,Object>>handleUnauthorized(UnauthorizedExceptione){Map<String,Object>result=newHashMap<>();result.put("code",e.getCode());result.put("msg",e.getMessage());returnnewResponseEntity<>(result,HttpStatus.UNAUTHORIZED);}@ExceptionHandler(Exception.class)publicResponseEntity<Map<String,Object>>handleOther(Exceptione){Map<String,Object>result=newHashMap<>();result.put("code",500);result.put("msg","服务器内部错误:"+e.getMessage());returnnewResponseEntity<>(result,HttpStatus.INTERNAL_SERVER_ERROR);}}

现在再访问不带token的/api/info,你会看到响应状态码是401,而JSON内容也规范了。我们不再需要手动拼接JSON字符串,Interceptor只需抛出异常,一切交给全局处理器。


7. 总结:一张清单回顾所有要点

主题关键点
基础登录接收@RequestBody,用HashMap模拟用户,返回UUID令牌
JWT无状态令牌jjwt依赖,生成/解析JWT,setSubject(username)存储用户标识
Bearer前缀HTTP认证方案标识,提取时需substring(7)去除
FilterServlet层拦截,手动response.getWriter(),异常无法被Spring全局捕获
InterceptorSpring MVC层拦截,可抛出异常交@RestControllerAdvice处理
统一异常处理@RestControllerAdvice+@ExceptionHandler定义统一JSON错误响应
包版本适配Spring Boot 3.x 用jakarta.servlet.*,2.x 用javax.servlet.*

最后的话:

我们从一段简单的登录接口出发,经历了UUID的临时方案,演化到JWT无状态认证,再通过Filter和Interceptor的对比实践,最终用全局异常处理收尾。现在你不但会写登录,更理解了背后分层与拦截器的设计思想。建议你把代码自己敲一遍,改一改白名单,尝试加入密码加密(BCrypt),这会是你成为后端熟手的重要一步。欢迎在评论区分享你的练习心得!

http://www.jsqmd.com/news/939922/

相关文章:

  • 2026年知名的苏州薄膜ALD/ALD技术/ALD工艺开发公司对比推荐 - 品牌宣传支持者
  • 2026年靠谱的苏州中空重载旋转平台/高精度中空旋转平台批量采购厂家推荐 - 行业平台推荐
  • AI模型注册平台选型难题:3类典型失败案例+4步标准化整合落地法
  • 智能驾驶NOA全解析:从技术原理到产业未来
  • MATLAB四阶矩可靠度计算工具:含熵辅助、偏导数值求解与改进算法
  • 大语言模型(LLM,Large Language Model)是一类基于深度学习、参数量通常达数十亿至数万亿级别的神经网络模型
  • 2026年5月观澜权威人流手术医院探寻
  • 2026年比较好的ALD设备/苏州光伏ALD稳定供货厂家推荐 - 行业平台推荐
  • PDF补丁丁:重新定义PDF文档处理的免费开源解决方案
  • 2026年质量好的贵州铝型材挂牌/贵州广告牌用户口碑推荐厂家 - 行业平台推荐
  • 工业质检实战:用YOLOv8+DCNv4搞定NEU-DET钢材缺陷检测,mAP提升3个点
  • ARL Docker 一键部署
  • 保姆级教程:手把手教你用Canmv IDE给K210开发板烧录.bin和.kmodel文件到Flash
  • 容器通过操作系统级虚拟化(OS-level virtualization),直接复用宿主机的 Linux 内核,无需像传统虚拟机(VM)那样为每个实例运行独立的 Guest OS
  • 黑马点评笔记千年后的大总结
  • 2026年质量好的农业机械力传感器/航空航天力传感器/苏州机器人力传感器/自动化设备力传感器优质厂家汇总推荐 - 行业平台推荐
  • 别再凭感觉画线了!用这个在线工具5分钟搞定PCB电源线宽(附电流计算表)
  • 深入解析JetBrains Maple Mono字体合成架构与实现原理
  • 山东大学项目实训个人纪实(6)——降低唇形同步延迟及性能需求
  • 现在不整合AI与开发工具,半年后将丧失交付竞争力:2024Q2 DevOps Survey揭示的3个临界阈值与紧急应对清单
  • [智能体-225]:智能体大模型体系 VS 冯诺依曼计算机硬件类比详解
  • 茄子快传与 WeTransfer 差距在哪?Bending Spoons 收购后 WeTransfer 月流水涨至 400 万+美元
  • 【Tilelang入门】Tilelang Puzzles 08
  • Translumo:如何在3分钟内掌握Windows实时屏幕翻译的终极技巧
  • 告别拥堵!用Python+SUMO+TraCI手把手教你打造一个会‘自学’的智能交通体(附完整代码)
  • 保姆级教程:在Windows/Linux上为YOLOv8s模型生成GradCAM热力图(避坑CUDA/CPU环境配置)
  • 【AI监控融合实战指南】:20年运维专家亲授5大落地陷阱与避坑清单
  • 导师骂你PPT逻辑乱?这个网站,自动帮你把论文变答辩神器
  • 告别旧版!Vitis Unified IDE 2023.2 保姆级配置指南:从OpenCV到Vision库,手把手搞定HLS开发环境
  • 从日常运维到脚本编写:详解Windows批处理中find与findstr的10个经典使用场景