news 2026/6/3 4:06:28

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
搞懂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),这会是你成为后端熟手的重要一步。欢迎在评论区分享你的练习心得!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/3 4:06:09

2026年论文降AIGC怎么弄?亲测10个免费降AI率工具,一键降低AI率

前阵子知网查重结果出来&#xff0c;我当场就傻了——熬了大半个月啃出来的初稿&#xff0c;就用AI润色了两三段&#xff0c;AIGC疑似度居然飙到45%&#xff0c;差点被判定非原创&#xff01;连着熬三个通宵试了二十多款降AI工具&#xff0c;踩坑踩得快哭&#xff1a;有的改完驴…

作者头像 李华
网站建设 2026/6/3 4:04:51

终极AMD Ryzen处理器调试指南:免费开源工具解锁硬件性能潜能

终极AMD Ryzen处理器调试指南&#xff1a;免费开源工具解锁硬件性能潜能 【免费下载链接】SMUDebugTool A dedicated tool to help write/read various parameters of Ryzen-based systems, such as manual overclock, SMU, PCI, CPUID, MSR and Power Table. 项目地址: http…

作者头像 李华
网站建设 2026/6/3 4:04:45

【Agent智能体17 | 工具使用-MCP协议】

声明&#xff1a;本篇博客是以吴恩达的【Agent智能体】教程为基础&#xff0c;并对其中的内容做了笔记整理以及个人收获的总结。在之前&#xff0c;每让一个大模型连接一个不同的工具&#xff0c;开发者都需要编写大量定制化的“胶水代码”&#xff0c;MCP的出现改变了这个局面…

作者头像 李华
网站建设 2026/6/3 4:02:54

C语言写的MD5计算工具包:带头文件、源码和测试程序

本文还有配套的精品资源&#xff0c;点击获取 简介&#xff1a;一套开箱即用的C语言MD5实现&#xff0c;包含md5.h头文件、md5.c核心算法和test.c测试代码&#xff0c;编译后可直接运行验证。输入任意长度字符串&#xff0c;输出标准128位十六进制摘要&#xff0c;结果与主流…

作者头像 李华
网站建设 2026/6/3 4:00:28

工业质检实战:用YOLOv8+DCNv4搞定NEU-DET钢材缺陷检测,mAP提升3个点

工业质检实战&#xff1a;YOLOv8DCNv4在钢材缺陷检测中的工程化落地指南钢铁生产线上的质检工程师每天需要处理数以万计的钢材表面图像&#xff0c;细微的裂纹或麻点往往隐藏在复杂纹理中。传统人工检测不仅效率低下&#xff0c;漏检率更是高达15%-20%。我们团队在最近一个钢厂…

作者头像 李华