尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java登录校验:JWT与Session原理对比,手写Filter/Interceptor实现

Java登录校验:JWT与Session原理对比,手写Filter/Interceptor实现 1. 这一章先解决“为什么登录之后还要做校验”先聊点实在的。Java后端做登录校验几乎每个项目都躲不开。从最早期的Session/Cookie到后来前后端分离架构下的JWT再到Spring Security、Sa-Token这类封装好的框架核心要解决的始终是同一个问题HTTP协议本身是无状态的服务器怎么知道“当前发起请求的人是不是之前登录的那个人”所以你去看各个公司的Java面试题登录校验、JWT、Filter、Interceptor几乎是必考题。不是面试官卷而是这个点确实能考察一个开发者的基本功——懂不懂HTTP无状态特性、有没有真正理解Token是什么、会不会在框架层面拦截请求做统一校验。它不像某个冷门框架的源码那么偏但能问的层次非常深。这篇博文我会从底层原理开始把Session和Token两条路对比清楚然后手写一个基于JWT的登录校验Demo分别用Filter和Interceptor两种方式实现最后把常见的面试题和实战中容易踩的坑一次说透。整个篇幅会比较长但每一段都尽量保证不废话该给代码给代码该讲原理讲原理。如果你现在处于这几个阶段这篇文章应该能帮到你刚学完Java Web基础准备接触真实的登录认证场景工作中要在前后端分离项目里做接口鉴权在准备Java后端面试想系统整理登录校验相关知识点自己写项目练手想让登录功能更规范、更接近企业级做法我在写代码示例时尽量保持轻依赖核心逻辑手写让你能看清每一步到底发生了什么而不是被框架封装得稀里糊涂。2. 登录校验的整体思路两种方案四条主线2.1 Session 方案到底是什么先说最早也最经典的方案——Session。它的工作流程其实很简单用户第一次来访问服务器收到请求后在内存里或者Redis里创建一条会话记录生成一个全局唯一的Session ID然后通过响应头Set-Cookie把Session ID发给浏览器。浏览器之后每次请求都会自动带上这个Cookie服务器拿着它去查对应的会话记录如果查到了、也没过期就认为请求是合法的。这个方案在传统的服务端渲染项目里非常好用因为前后端在一个部署环境里Cookie天然被浏览器管理开发者甚至不需要手动处理Token的存储。但它的痛点也很明显第一Session数据如果放在服务器内存里那么无法横向扩展。你部署多台应用服务器用户在A服务器登录了下一次请求被负载均衡转发到B服务器B服务器内存里没有这个Session用户就得重新登录。虽然可以用粘滞会话Sticky Session或者Session同步来解决但架构上就变得很别扭。第二Cookie在跨域场景下非常难处理。现在的前后端分离项目前端跑在localhost:5173后端跑在localhost:8080Cookie要配Domain、要处理SameSite一个不小心就会被浏览器拦掉。我自己就在这个事上浪费过很长时间。第三Session天生是“有状态”的。服务器必须记住每一个登录用户如果用户量上来存储压力会越来越大分布式环境下还要引入Redis做集中式存储整套东西并不轻松。2.2 Token 方案又好在哪Token方案主要是JWT走的是完全相反的路子服务端不保存登录状态。用户登录成功后服务器把用户ID、过期时间这些信息通过签名算法打包生成一个字符串这个字符串就是Token返回给客户端。客户端通常是前端把它存在localStorage里之后每次请求在请求头里加上Authorization: Bearer token。服务端拿到Token后验签、解析确认没问题就直接认为请求合法。这样服务端就彻底“无状态”了不占内存、天然支持水平扩展。你部署一百台服务器用户拿着同一个Token在任意一台验签都能通过根本不需要关心请求落到哪台机器上。JWT的另一个好处是结构标准化。它是一个经过编码的JSON对象由三部分组成Header头部、Payload载荷、Signature签名。每一部分用Base64URL编码后用点号拼接最终长这样eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c用任何在线工具甚至自己写代码解码都能看到它的内容Header部分包含签名算法和Token类型Payload部分包含业务信息比如用户ID、用户名、过期时间Signature则是对前两部分做签名运算后的结果用来防止内容被篡改。2.3 为什么我今天重点讲JWT而不是Session其实聊到这里你应该能感觉到JWT方案更适合当前主流的前后端分离架构也是Spring Boot默认推荐的方向之一。它解决了Session的分布式共享痛点也让前后端在接口对接时不需要关心Cookie的各种限制。但JWT也不是没有坑Token一旦签发在过期之前是“无法主动作废”的。用户改了密码怎么办管理员把用户封了怎么办这些问题都要靠额外手段兜底。所以我会在后面专门用一个章节讨论JWT的局限性以及项目中实际的补救做法。3. JWT核心原理拆解三段结构三个关键点3.1 Header、Payload、Signature 逐个击破先看一下Header部分它是JSON格式的{ alg: HS256, typ: JWT }alg是签名算法常用的是HS256对称加密和RS256非对称加密typ固定是JWT。这部分的Base64URL编码不是乱码是能解出来的。Payload部分存放实际要传递的信息也叫Claims声明。JWT规范里预定义了一些字段比如sub主题、exp过期时间、iat签发时间、iss签发者你完全可以自定义字段{ sub: 1234567890, name: 张三, role: admin, iat: 1516239022, exp: 1617239022 }你可能会问Payload里能放密码吗放手机号行不行我的建议是只放非敏感信息。因为Payload只是Base64URL编码并没有加密任何拿到Token的人都可以直接解码看到里面的内容。你把密码放进去等于明文裸奔。Signature部分是整个JWT的安全核心。如果用的是HS256它的签名公式是这样的HMACSHA256(base64UrlHeader . base64UrlPayload, secret)也就是说服务端用同一个密钥把前两段的编码结果拼接起来做哈希得到一段签名。这个签名附在Token末尾客户端理论上是无法伪造的因为它不知道你的密钥。3.2 Base64URL 和 Base64 的区别这里有一个很多新手会忽略的细节JWT用的不是普通的Base64编码而是Base64URL编码。它俩的区别在于Base64编码里有两个字符在URL场景下会有问题——和/。放在URL参数里会被解析成空格/会破坏路径结构。所以Base64URL把换成-把/换成_并且去掉末尾的填充符。因此你看到的JWT字符串里永远不会有和/这就是特意为URL场景做的优化。3.3 算法选择HS256 vs RS256这两个算法在实际项目里的选择面试里基本都会问到。HS256是对称加密签名和验签都用同一个密钥。它的优点是实现简单、性能好缺点是密钥一旦泄露攻击者可以同时伪造和验签。对于纯后端内部系统来说HS256完全够用。RS256是非对称加密私钥在认证服务手里签名时用私钥验签的公钥可以分发给任意微服务实例验签时用公钥。它的好处是安全边界更清晰非常适合分布式微服务架构。比如你有一个独立的认证中心其他业务服务只需要拿到公钥就能验签根本不需要知道私钥。通常在单体项目或者中小型系统里HS256足够。到了微服务架构我更建议直接上RS256把验签用的公钥配到每个服务里避免密钥通过内网传输带来风险。4. 代码实战手写一个基于JWT的登录校验4.1 项目结构设计与技术选型接下来进入代码环节。我先把工程结构摆出来你照着搭就行。技术栈是Spring Boot 2.7 JWT工具库jjwt 0.11.5。com.example.jwtlogin ├── controller │ ├── AuthController.java │ └── UserController.java ├── interceptor │ └── JwtInterceptor.java ├── filter │ └── JwtAuthFilter.java ├── util │ └── JwtUtil.java ├── config │ ├── WebConfig.java │ └── FilterConfig.java └── JwtLoginApplication.java我用的数据库是MySQL但为了降低复杂度这里不做数据库操作用户表简化成一个静态Map。实际项目中换成MyBatis-Plus或JPA即可登录鉴权的核心逻辑不受影响。先来看pom.xml里需要引入的依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency4.2 JWT工具类生成与解析的核心代码JwtUtil是整个登录系统的核心封装了Token的生成、解析和校验逻辑。我用HS256算法密钥直接写死在配置里这个在生产环境要放到配置中心或环境变量里。Component public class JwtUtil { // 生产环境一定不要硬编码建议放到环境变量或配置中心 private static final String SECRET_KEY my-secret-key-my-secret-key-my-secret-key; // 至少32字节 private static final long EXPIRE_TIME 1000 * 60 * 60 * 24; // 24小时 private static final SecretKey key Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8)); /** * 生成Token */ public static String generateToken(Long userId, String username, String role) { Date now new Date(); Date expireDate new Date(now.getTime() EXPIRE_TIME); return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .claim(role, role) .setIssuedAt(now) .setExpiration(expireDate) .signWith(key, SignatureAlgorithm.HS256) .compact(); } /** * 解析Token获取Claims */ public static Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); } /** * 判断Token是否过期 */ public static boolean isExpired(String token) { try { Claims claims parseToken(token); return claims.getExpiration().before(new Date()); } catch (ExpiredJwtException e) { return true; } } }关于上述代码有几点要特别说明密钥长度不能太短HS256要求密钥至少256位32字节否则运行时直接抛异常这是一个很容易踩的坑。如果你用短字符串比如secretjjwt会报WeakKeyException而且这个异常不是编译期报错是启动后第一次调用才炸出来。.claim(username, username)是自定义字段的写法实际项目中常见放userId、角色、昵称等非敏感信息。解析时通过claims.get(username)取出来。过期时间配置为24小时这在一些企业后台系统中偏短但安全性更好。实际项目建议根据业务场景定比如APP端的Token有效期可以做到7天甚至更长。4.3 Filter 实现登录校验基于 javax.servlet 的方案Filter是Servlet层面的组件它最早被定义在javax.servlet包中在Spring Boot应用里可以拦截所有请求优先级高于Interceptor。我需要先建一个继承了OncePerRequestFilter的过滤器类。这个类和普通的Filter接口区别在于它保证了一次请求只会执行一次过滤逻辑避免某些容器环境下过滤器被重复调用的尴尬Component public class JwtAuthFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 放行登录请求也就是 /login 接口 String requestURI request.getRequestURI(); if (requestURI.startsWith(/login)) { filterChain.doFilter(request, response); return; } // 获取请求头中的Authorization String authHeader request.getHeader(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { setUnauthorizedResponse(response); return; } String token authHeader.substring(7); try { Claims claims JwtUtil.parseToken(token); // 将用户信息放入request属性方便后续Controller获取 request.setAttribute(userId, Long.valueOf(claims.getSubject())); request.setAttribute(username, claims.get(username)); request.setAttribute(role, claims.get(role)); filterChain.doFilter(request, response); } catch (ExpiredJwtException e) { setUnauthorizedResponse(response, Token已过期); } catch (JwtException | IllegalArgumentException e) { setUnauthorizedResponse(response, Token无效); } } private void setUnauthorizedResponse(HttpServletResponse response) throws IOException { setUnauthorizedResponse(response, 未登录或Token无效); } private void setUnauthorizedResponse(HttpServletResponse response, String msg) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\ msg \}); } }过滤器里我把登录接口直接放行因为用户还没有Token不能让过滤器拦住。对于需要认证的接口先从Authorization请求头里取Bearer开头的Token串然后调用JwtUtil.parseToken验证签名和过期时间。验签通过后把用户信息塞进request属性后续所有处理链上的代码都能拿到当前用户信息。需要注意几个小细节请求头名称是Authorization这是HTTP标准推荐的自定义认证头。前端代码里用Axios的话需要配置拦截器统一加上这个头。Bearer前缀是一种约定来自OAuth2.0规范。你也可以不用直接放Token但建议遵循惯例因为很多框架和网关天然识别这种格式。异常处理必须细化。ExpiredJwtException和JwtException要分别处理这是一个非常影响使用体验的细节——用户Token过期了你返回的提示语应该告诉他是“过期”而不是笼统的“无效”。接下来把过滤器注册到Spring Boot的过滤器链中Configuration public class FilterConfig { Bean public FilterRegistrationBeanJwtAuthFilter jwtFilterRegistration(JwtAuthFilter filter) { FilterRegistrationBeanJwtAuthFilter registration new FilterRegistrationBean(); registration.setFilter(filter); registration.addUrlPatterns(/*); registration.setOrder(1); return registration; } }setOrder(1)是设置执行顺序数字越小优先级越高。如果你的项目里还有其他过滤器比如跨域过滤器、日志过滤器这个顺序需要规划好。我通常会让CORS过滤器在最前面登录校验过滤器紧随其后因为CORS跨域的问题不解决后续的请求根本到不了业务逻辑。4.4 Interceptor 实现登录校验Spring MVC 的 HandlerInterceptor如果说Filter是Servlet时代的产物那么HandlerInterceptor就是Spring MVC框架精心设计的拦截器。它的API设计更贴合开发者习惯可以在请求到达Controller之前和渲染完成之后分别插入逻辑。一个典型的登录拦截器长这样Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行OPTIONS预检请求 if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) { return true; } // 非控制器方法直接放行 if (!(handler instanceof HandlerMethod)) { return true; } String authHeader request.getHeader(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { writeErrorResponse(response); return false; } String token authHeader.substring(7); try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, Long.valueOf(claims.getSubject())); request.setAttribute(username, claims.get(username)); return true; } catch (ExpiredJwtException e) { writeErrorResponse(response, Token已过期); return false; } catch (JwtException | IllegalArgumentException e) { writeErrorResponse(response, Token无效); return false; } } private void writeErrorResponse(HttpServletResponse response) throws IOException { writeErrorResponse(response, 未登录或Token无效); } private void writeErrorResponse(HttpServletResponse response, String msg) throws IOException { response.setContentType(application/json;charsetUTF-8); response.setStatus(401); response.getWriter().write({\code\:401,\msg\:\ msg \}); } }preHandle方法的返回值很关键返回true表示放行进入Controller处理返回false表示拦截请求到此为止。然后通过实现WebMvcConfigurer接口来注册拦截器Configuration public class WebConfig implements WebMvcConfigurer { Autowired private JwtInterceptor jwtInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/**) .excludePathPatterns(/login, /register, /error); } }这里有一个很爽的点Interceptor可以精确配置需要拦截的路径和放行的路径。比如/login、/register这种不需要登录的接口直接在excludePathPatterns里排除掉不用像Filter那样在代码里手动判断URI前缀。4.5 登录 Controller 和业务逻辑联调基本的拦截和过滤器已经写完了现在需要一个登录接口和一个需要认证的业务接口来验证整个链路是否跑通。RestController RequestMapping(/api) public class AuthController { PostMapping(/login) public Result login(RequestBody LoginRequest req) { // 模拟数据库查询 if (admin.equals(req.getUsername()) 123456.equals(req.getPassword())) { String token JwtUtil.generateToken(1001L, admin, role_admin); return Result.ok(token); } return Result.error(用户名或密码错误); } GetMapping(/user/info) public Result userInfo(HttpServletRequest request) { Long userId (Long) request.getAttribute(userId); String username (String) request.getAttribute(username); // 实际业务中通过userId去数据库查询用户信息 return Result.ok(用户 username 的信息ID userId); } }LoginRequest就是一个简单的实体类包含username和password两个字段用RequestBody接收JSON。启动项目后先用Postman或curl请求登录接口curl -X POST http://localhost:8080/api/login \ -H Content-Type: application/json \ -d {username:admin,password:123456}拿到返回的Token后再访问需要认证的接口curl -X GET http://localhost:8080/api/user/info \ -H Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxx.yyy测试成功后把Token改一个字符再请求应该返回401。到此一个基于JWT的登录校验Demo就跑通了。这套代码麻雀虽小五脏俱全后面你往里接数据库、接Redis、接Spring Security都是在这个骨架上做扩展。5. Filter 和 Interceptor 的异同一张表说透刚才两套实现代码看起来功能一样但实际上它们是两个完全不同的东西。面试里这个问题也是高频考点我给你们一个可以直接参考的对比表维度FilterInterceptor规范归属Servlet规范Spring MVC框架执行时机请求进入Servlet之前Servlet处理后、进入Controller之前作用范围所有经过Servlet的请求由Spring MVC管理的请求是否可访问Spring Bean需要手动注册能注入但有些容器环境下不便本身就是Spring Bean自动注入路径配置使用URL通配符如/*使用Ant表达式如/**、/api/*获取处理器方法信息不能获取到具体Controller方法可以拿到HandlerMethod判断是不是Controller方法是否能被Spring AOP增强不能能底层原理依赖Servlet容器回调依赖Spring MVC的DispatcherServlet分发机制拦截粒度请求级别方法级别在实际项目里Filter和Interceptor并没有绝对的替代关系。如果你需要做全局的字符编码、CORS跨域、请求日志记录用Filter更合适因为它最早接触请求最“外围”。如果你要做登录鉴权、权限控制、方法级检查用Interceptor更顺手因为可以精确到某个Controller的某个方法。还有一点容易被忽视Filter里抛出的异常不会走Spring的RestControllerAdvice全局异常处理器。因为Filter执行时Spring MVC的异常解析链路还没准备好。但Interceptor的preHandle里抛出的异常可以被全局异常处理器捕获。这也是很多人把登录校验放在Interceptor而不是Filter的一个原因——异常处理更统一。6. 关于 JWT 的局限性以及我推荐的补救方案6.1 无状态是一把双刃剑JWT把状态存在客户端服务端不持久化这在带来扩展性优势的同时也带来了一个让人头疼的问题Token在过期之前始终有效。举几个实际场景用户修改密码后旧Token依然有效因为服务端不知道密码已经变了。管理员把某个账号封禁了但这个用户手里已有的Token在过期之前还是能用。用户在APP上退出登录Token放在本地没有清除下次装回来发现状态还是登录的。这些场景本质上都违反了直觉——用户以为“退出就是失效”但JWT做不到真正的即时失效。6.2 我的常用补救策略第一个策略是缩短Token有效期 Refresh Token。访问Token的有效期设短一些比如30分钟到2小时后端返回一个短期Token的同时再返回一个长期Refresh Token有效期7~30天。当访问Token过期时客户端拿着Refresh Token去请求一个新的访问Token。这个方案广泛应用于移动端和前后端分离项目。第二个策略是白名单或黑名单。在Redis里维护一个Token黑名单把需要立即失效的Token的jtiJWT ID存进去校验时先查一下黑名单。这个方案适合用户量不大但安全要求高的场景。第三个策略是把用户状态的关键信息做成可查询的动态字段。比如在Payload里不存角色而是存一个ver版本号字段用户修改密码或封禁时版本号加1服务端验签时去查当前用户的版本号和Token里的版本号是否一致。具体用哪种取决于业务需求。如果是内部管理系统直接用短Token Redis黑名单就够了如果是To C的APPRefresh Token是刚需。6.3 Token存储位置的选择还有一个很现实的问题Token到底让前端存在哪里存在localStorage里优点是JS可以轻松读取适合单页应用。缺点是容易受到XSS攻击——如果页面里被注入了恶意脚本Token会被直接偷走。存在Cookie里带上HttpOnly属性后JS无法访问能有效防御XSS但需要处理CSRF跨站请求伪造问题。我个人建议是如果项目里有XSS防护措施可以存在localStorage开发成本低如果安全要求高Token放HttpOnly Cookie里同时加CSRF Token防护。这个点也算面试加分项能体现你对前端的了解不是停留在表面。7. 面试考点这一节内容背熟就能扛住大部分追问7.1 高频八股文题目整理整理一下这个主题下经常被问到的几类问题说一下JWT由哪几部分组成各自的作用是什么JWT和Session有什么区别为什么现在更多用JWTFilter和Interceptor的区别是什么JWT的Token能不能主动失效如果不能项目里怎么处理JWT放在请求头里还是Cookie里为什么怎么处理JWT的过期时间JWT的Payload里可以放密码吗如果JWT被泄露了怎么办这些问题看上去都有标准答案但考官往往会追问“为什么”、“还有没有别的方案”。所以在回答时不要只背结论要把前后因果讲清楚。比如问“为什么JWT无状态性好”你可以从水平扩展、服务器集群、分布式Session共享这几个方面来展开。7.2 我总结的答题套路回答这类问题的核心是三步说清楚是什么 → 对比你之前用什么方案 → 解释你为什么选择它、放弃了什么。举个例子。面试官问“Session 和 JWT 你怎么选”你可以这样答Session方案的优点是服务端可以随时让某个会话失效数据存储在服务端也相对安全缺点是分布式环境下需要处理Session共享前后端分离场景跨域麻烦。JWT的优点是服务端无状态、天然支持分布式缺点是失效困难。如果是传统服务端渲染项目我会优先选Session省事又直观如果是前后端分离并且有多个后端实例我会用JWT配一个短时效加黑名单的方案来弥补主动失效的问题。这个答案既展示了你的知识广度也让面试官知道你面对具体业务时会权衡而不是死记硬背。7.3 实战面试中容易被追问的细节有些面试官会考得很细比如jjwt0.11.x版本里的setSubject和claim有什么区别setSubject是标准字段已经定义在Payload规范里claim是自定义字段。两者最终都以JSON字段的形式存放在Payload中。解析JWT时可能出现哪些异常主要有ExpiredJwtException过期、UnsupportedJwtException不支持的Token格式、MalformedJwtExceptionToken格式错误、SignatureException签名不匹配、IllegalArgumentExceptionToken为空或空白。实战中需要逐一捕获并给出不同的响应。密钥放在代码里吗正规做法是放到环境变量、配置中心或KMS密钥管理服务中。硬编码在代码里是严重的安全隐患。8. 调试记录第一次跑通时我踩过的几个坑8.1 Token每次都不一样导致本地无法“永久登录”这是我最早用JWT时最困惑的点。明明用户信息没变为什么每次登录生成的Token都不一样原因很简单Payload里我放了iat签发时间和exp过期时间这两个时间字段每次登录都会变化所以最终生成的Token字符串自然不同。这不是Bug这是正常现象。只要验签能通过Token不同完全无所谓。8.2 传递密钥到 jjwt 时提示 WeakKeyException写成Keys.hmacShaKeyFor(my-secret.getBytes())直接报了WeakKeyException。原因我前面提到过HS256要求密钥至少256位32字节不够就报错。这就是为什么我在示例里把密钥写成了那么长一串。8.3 Spring Boot 未授权时返回的默认 401 结构不是 JSON当Filter里没有手动写JSON响应体时前端拿到的错误信息是Spring Boot默认的Basic Auth错误页面而不是统一的接口返回结构。所以一定要在Filter里设置response.setContentType(application/json;charsetUTF-8)并自己写入JSON字符串否则前后端联调时又是一堆吐槽。8.4 前端跨域导致 Authorization 请求头丢失现在的开发基本是前后端分离前端端口和后端端口不同天然跨域。如果你在后端只配置了允许来源allowedOrigins没有暴露请求头前端自定义的Authorization头可能会在预检请求阶段被浏览器挡下来。解决方法是允许Authorization这个请求头。8.5 过滤器放行路径记得把错误页也带上Spring Boot的/error路径也要考虑如果Filter把所有请求都拦了遇到404或500这类错误跳转时可能因为拿不到Token直接又变成401前端排查问题时一脸懵。所以我在Interceptor示例里把/error也放进了excludePathPatterns。9. 附录演示用完整代码片段汇总为了方便你copy下来直接跑我把核心代码集中放一下。JwtLoginApplication入口类SpringBootApplication public class JwtLoginApplication { public static void main(String[] args) { SpringApplication.run(JwtLoginApplication.class, args); } }配置类ConfigConfiguration public class WebConfig implements WebMvcConfigurer { Autowired private JwtInterceptor jwtInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/**) .excludePathPatterns(/login, /register, /error); } }统一返回结果封装Resultpublic class Result { private int code; private String msg; private Object data; public static Result ok(Object data) { Result r new Result(); r.code 200; r.msg success; r.data data; return r; } public static Result error(String msg) { Result r new Result(); r.code 500; r.msg msg; return r; } // getter/setter 略 }这只是一个最简版真实项目中建议加上数据校验、全局异常处理、请求日志等。10. 关于登录校验后续可以怎么扩展我前面一直在强调一个观点登录校验不是孤立的一步它是整个安全体系的大门。前面这套JWT Filter/Interceptor的Demo只能算是骨架实际项目里通常还需要在上面叠加很多东西。比如内置一个RequireRole(admin)注解配合Interceptor里的HandlerMethod做细粒度的RBAC权限控制。比如加入登录失败次数的限制防止暴力破解。比如接入Spring Security的SecurityContext把JWT解析出来的用户信息无缝对接到Spring Security的权限体系中。再比如引入Redis做Token刷新和注销管理。我个人在实际项目中的体会是能把Session和JWT的底层原理讲透能把Filter和Interceptor的边界理清楚就算不使用任何一个安全框架你也能搭建出一个看得懂、能维护、能追踪问题的登录系统。框架永远是工具真正值钱的是对工具底层机制的理解。这套内容本身就是Java高级工程师和初级工程师的一个分水岭。
返回列表