ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot中JWT登录认证:Filter与Interceptor职责详解

Spring Boot中JWT登录认证:Filter与Interceptor职责详解 前后端分离做久了几乎每个Java后端都要和登录认证打交道。JWT、Filter、Interceptor这三个词经常被放在一起讨论但很多人实际写起来容易犯迷糊明明Filter也能做鉴权为什么还要用InterceptorJWT令牌到底应该在哪里解析谁负责放行谁负责拒绝这篇文章就围绕这三个核心点把一条完整的JWT登录认证链路拆开讲明白。我要做的不是贴一堆零散代码而是帮大家理清整个请求从进入Tomcat到命中Controller中间到底经过哪些环节每个环节该承担什么职责。读完你会搞清楚如何自己写一套可维护的JWT认证方案如何正确配置Filter和Interceptor以及SPA项目里token续签、验证码、注销失效、更新用户信息后重新签发令牌这些实际需求到底该怎么落地。如果你对Spring Boot的请求链路还停留在“Controller里加个拦截判断”的认知这篇文章会对你有帮助。无论你是刚写Java Web的实习生还是被老项目折腾过几次的后端开发把下面的原理和代码过一遍都会对认证系统的设计多一层理解。1. 项目概述JWT、Filter、Interceptor到底在解决什么问题1.1 无状态登录为什么大家都在用JWT一句话解释JWT它是JSON Web Token一种紧凑且自包含的令牌格式。登录成功后服务端把用户身份信息加密签名后生成一个字符串令牌返回给客户端。后续请求只要带上这个令牌服务端就能通过验签确认“这个人是谁”不需要在服务端保存会话数据。传统CookieSession方案依赖服务端存储会话放到分布式环境下就要考虑Session共享、粘滞会话、Redis同步这些问题。JWT把用户信息直接编码进令牌里服务端天然无状态非常适合微服务、前后端分离、移动端App这些场景。但“无状态”也有代价。JWT一旦签发在有效期内很难主动让它失效所以我们要在生成规则和校验逻辑上做文章。这里的核心不是“JWT多厉害”而是“在使用JWT时怎么把认证和授权体系设计得可维护、可扩展”。这套体系里Filter负责认证Interceptor负责授权职责分明才不会让代码变成一团乱麻。我更愿意把JWT理解成一颗“防伪二维码”Payload里写的用户信息相当于二维码里存的内容Signature相当于防伪标识。只要服务端密钥不泄露别人就伪造不了但二维码本身可以被复制使用所以过期时间和失效机制必须做好。1.2 Filter、Interceptor、AOP的定位差异很多人知道这三个东西但分不清差别。我用一个生活化类比说明Filter像小区门口的保安所有进出小区的人和车都要过。它工作在Servlet容器层比你Spring MVC的DispatcherServlet还早。Interceptor像公司楼层里的闸机只针对要去对应楼层的人。AOP则像部门内部的审批流程只作用于某些具体业务方法。执行顺序是这样的请求先进入Filter链Filter处理完后再进入DispatcherServletSpring MVC根据URL找到Handler后HandlerInterceptor的preHandle开始执行如果通过就调用Controller方法。方法执行过程中AOP切面可以在方法前后增强逻辑。业务完成后Interceptor的postHandle和afterCompletion依次执行最后再回到Filter链继续往下走。这里有个很关键的区别Filter能覆盖所有Servlet资源包括静态资源和非Spring MVC路径Interceptor只对Spring MVC处理器生效但它能拿到HandlerMethod也就是Controller方法上的注解信息。AOP更灵活可以切入任意bean的方法但通常不会用来做HTTP层面的认证。实际项目里最常见的组合就是Filter解析JWT并设置用户上下文Interceptor做接口权限校验和放行判断。这套组合让我后面对“谁在什么时候处理什么”非常清晰。2. 整体设计与技术选型思路2.1 登录认证的完整流程设计先不看代码把整体流程画在脑子里。我设计一个标准的前后端分离登录认证流程客户端请求登录接口携带用户名、密码和验证码。服务端校验验证码校验用户名密码都通过后生成JWT令牌返回。客户端把令牌放在请求头的Authorization字段里格式通常是Bearer token。请求到达后端Filter最先解析请求头里的token校验签名和过期时间。Filter解析成功并把用户信息放进request attribute或ThreadLocal。Interceptor拿到请求路径和HandlerMethod判断当前接口需不需要登录、需要什么权限。Controller直接从上下文里拿用户信息处理业务并返回数据。这个链路看下来Filter像是一个身份识别卡刷卡器只负责“验明正身”Interceptor像是访客系统决定“你能不能进这个房间”。如果两者混在一起后面加一个新接口需要不同权限时你会在Filter里堆满if-else代码很快变成一坨。还有一点值得注意登录接口本身不应该要求携带token验证码接口也不应该要求登录。所以Filter要对这些白名单路径做放行或者至少不能拦截没有token的请求。真正的强制登录检查放在Interceptor里通过排除路径配置来管理白名单更直观。2.2 为什么选Filter做JWT解析Interceptor做权限校验有人会问既然Filter可以做所有事为什么不直接在Filter里判断权限原因有三个。第一Filter拿不到Controller方法上的注解。权限控制想做到“在方法上加一个RequirePermission注解就能生效”必须在Interceptor里读取HandlerMethod这是Filter做不到的。Filter处理的是HttpServletRequest和HttpServletResponse天然比Spring MVC层要原始。第二Interceptor的路径匹配方式更适合业务。addPathPatterns(/api/**)、excludePathPatterns(/login)这种配置比Filter的url-pattern更直观也更接近Spring MVC的开发习惯。一个业务开发人员看WebMvcConfigurer里的拦截器配置马上能知道哪些接口需要登录如果这些规则埋在Filter里排查起来就费劲。第三多层职责拆分有助于测试和扩展。Filter只做token解析Interceptor只做权限校验单测覆盖起来非常容易。未来如果项目引入Spring Security你只要替换Filter层权限注解层还能保留反过来也一样。但这不代表Filter里一定不能做权限判断。如果你的项目特别简单只有“必须登录”和“完全公开”两种状态一个Filter也能搞定。可一旦权限粒度细化到角色、部门、操作级别Interceptor的价值就很明显了。我个人的习惯是Filter只做认证不鉴权Interceptor里做硬性路径拦截和注解权限校验。3. 核心细节解析JWT令牌生成、解析与校验3.1 JWT结构Header、Payload、Signature一个JWT字符串由三部分组成用点号分隔xxxxx.yyyyy.zzzzz。Header通常长这样{alg:HS256,typ:JWT}表示签名算法和令牌类型。Payload包含注册声明和自定义声明。注册声明里有sub主题通常存用户ID、iat签发时间、exp过期时间、jti令牌唯一标识等。自定义声明可以放username、role这些业务字段。Signature是防篡改的关键。以HS256为例它的计算过程是把Header和Payload分别做Base64Url编码后用点号拼接再用HMAC-SHA256算法和密钥进行签名最后对签名结果再做Base64Url编码。这里要特别提一个新手容易忽略的知识点JWT里的Base64Url编码不是普通Base64它会把替换成-、/替换成_并且去掉末尾的。在线网站解析JWT时经常看到一些特殊字符不用害怕那是正常的。还要清楚一件事Header和Payload只是Base64编码并没有加密任何拿到token的人都能解码看到里边的业务信息。所以千万不要把密码、身份证号、手机号这些敏感信息直接放进JWT。有人觉得“JWT很安全”其实它的核心保障是签名防篡改不是数据加密。3.2 后端生成JWT用Java和jjwt实现Java生态里操作JWT最主流的库是jjwt我简单演示一下集成过程。先在pom.xml里加入依赖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 /dependency然后在配置文件里写入密钥和过期时间配置jwt: # HS256要求密钥至少32字节部署时务必通过环境变量注入 secret: ${JWT_SECRET:please-change-me-to-a-long-random-string-at-least-32-bytes} expire: 7200JWT工具类可以这样写Component public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private long expire; private SecretKey getSecretKey() { return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } public String generateToken(String userId, String username) { Date now new Date(); Date expireAt new Date(now.getTime() expire * 1000); return Jwts.builder() .setSubject(userId) .claim(username, username) .setIssuedAt(now) .setExpiration(expireAt) .signWith(getSecretKey(), SignatureAlgorithm.HS256) .compact(); } public Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(getSecretKey()) .build() .parseClaimsJws(token) .getBody(); } }代码逻辑不复杂但有几处一定要注意。Keys.hmacShaKeyFor要求字节数组至少32字节如果你的密钥是“123456”这种短字符串运行时会直接抛WeakKeyException。其次expire单位是秒我一般在配置里写7200表示2小时计算时统一乘1000转成毫秒避免出现过期时间比预期长1000倍的情况。生成token时注册声明里的sub用于用户IDusername放自定义claim。Controller登录成功后调用generateToken返回给前端正好对应了“更新用户登录信息并生成返回JWT令牌”这个常见需求。3.3 校验JWT时最容易踩的坑解析JWT看起来简单但实际翻车场景非常多。第一个坑没有使用SecretKey对象直接把字符串传给parseClaimsJws。老版本jjwt可能给了兼容逻辑但新版API要求传入Key对象用不对就会抛异常。第二个坑密钥太短。HS256要求至少256位也就是32字节。很多同学喜欢写mySecret这种字符串结果本地没报错部署到服务器上偶发报错排查半天才发现是环境变量覆盖的密钥长度不够。第三个坑没有统一处理JwtException。解析失败时如果直接往外抛用户会看到500错误。正确做法是在Filter里捕获ExpiredJwtException、SignatureException、MalformedJwtException然后返回401和一段JSON错误信息。第四个坑把算法混淆当不存在。JWT的Header是客户端可修改的如果服务端解析时不强制指定算法攻击者可以把alg改成none或者尝试把RS256改成HS256利用公钥伪造签名。所以服务端应该使用固定的Key和算法不要信任Header里的算法声明。第五个坑忽略并发环境下的时间精度。某些场景下服务器时间不一致会导致JWT签发时间和校验时间出现偏差过期判断不稳定。项目里尽量统一使用同一边的时钟基准或者做好服务器时间同步。这些坑我基本都踩过最后总结出来一条经验JWT相关代码不要自己发明算法直接用成熟库的默认行为验签失败就拒绝过期就拒绝不让任何异常穿透到业务层。4. 实操过程用Filter和Interceptor搭建登录认证4.1 实现OncePerRequestFilter请求只过滤一次Spring Boot里实现Filter我建议继承OncePerRequestFilter不要直接实现Filter。原因很简单OncePerRequestFilter能保证在同一个请求内只执行一次过滤避免Servlet转发、包含等场景下Filter重复执行。转发场景很隐蔽用普通Filter可能一个请求过滤两次token解析会被执行两遍虽然不一定出事故但性能和日志都会受影响。来看核心代码Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Autowired private JwtUtil jwtUtil; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader request.getHeader(Authorization); if (StringUtils.hasText(authHeader) authHeader.startsWith(Bearer )) { String token authHeader.substring(7); try { Claims claims jwtUtil.parseToken(token); request.setAttribute(userId, claims.getSubject()); request.setAttribute(username, claims.get(username)); } catch (Exception e) { writeUnauthorized(response); return; } } filterChain.doFilter(request, response); } private void writeUnauthorized(HttpServletResponse response) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\token无效或过期\}); } }注意这个Filter不负责强制要求登录。如果请求没带token它就直接放行让后面的Interceptor去判断当前接口需不需要登录。这样做的好处是验证码接口、登录接口、静态资源都可以在没有token的情况下访问只有标记为需要登录的接口才会被拦截下来。如果你非得在Filter里做强制登录那白名单只能靠路径匹配逻辑会很绕。4.2 实现HandlerInterceptor基于注解的权限控制权限这块我推荐用自定义注解拦截器的方式比在代码里写死角色名要优雅得多。先定义一个注解Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface RequirePermission { String value() default ; }然后在Controller方法上标注RestController RequestMapping(/api/user) public class UserController { GetMapping(/info) RequirePermission(user:info) public Result userInfo(HttpServletRequest request) { String userId (String) request.getAttribute(userId); return Result.ok(userId); } }拦截器读取HandlerMethod上的注解判断是否放行public class PermissionInterceptor implements HandlerInterceptor { private final SetString whiteList Set.of(/api/auth/login, /api/auth/captcha); Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equalsIgnoreCase(OPTIONS)) { return true; } if (!(handler instanceof HandlerMethod handlerMethod)) { return true; } String userId (String) request.getAttribute(userId); // 模拟从缓存或DB里查出来的用户权限 ListString userPermissions getUserPermissions(userId); RequirePermission requirePermission handlerMethod.getMethodAnnotation(RequirePermission.class); if (requirePermission ! null StringUtils.hasText(requirePermission.value())) { if (userId null) { writeError(response); return false; } if (!userPermissions.contains(requirePermission.value())) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:403,\msg\:\权限不足\}); return false; } } return true; } }这里有个细节跨域场景下浏览器会先发送OPTIONS预检请求预检请求通常不会带业务token。如果Interceptor把所有请求都拦死前端控制台会看到一堆CORS报错。所以我在预检请求时直接放行让跨域过滤器去处理。还有一点如果handler不是HandlerMethod说明请求交给了静态资源处理器或者其它Spring MVC处理器Interceptor直接放行。吃不准的请求就先放过去不要让权限判断误伤静态资源。4.3 注册顺序与路径配置Filter和Interceptor配置起来容易混淆我放在一起说明。Filter注册用FilterRegistrationBean手动控制顺序Configuration public class FilterConfig { Bean public FilterRegistrationBeanJwtAuthenticationFilter jwtFilterRegistration(JwtAuthenticationFilter filter) { FilterRegistrationBeanJwtAuthenticationFilter registration new FilterRegistrationBean(); registration.setFilter(filter); registration.addUrlPatterns(/api/*); registration.setOrder(1); return registration; } }Interceptor注册通过WebMvcConfigurerConfiguration public class WebConfig implements WebMvcConfigurer { Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; // 仅为了保持依赖可见性不是必须 Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new PermissionInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/captcha); } }我们可以理一下执行顺序请求/api/user/info时FilterRegistrationBean里的JwtAuthenticationFilter先执行解析token并设置attribute接着进入DispatcherServletPermissionInterceptor的preHandle接着执行从request attribute里取userId并判断权限最后Controller处理业务。配置顺序有讲究。Filter的order数值越小越先执行Interceptor的addInterceptor顺序则按照注册顺序执行。项目中我习惯把Filter的order固定为1把Interceptor的路径匹配放在WebConfig里统一管理这样团队里新同学接手时只需要看一个配置类就能理解认证规则。4.4 更新用户登录信息后重新签发JWT“更新用户登录信息并生成返回JWT令牌”这个热词场景很典型。用户修改了密码、绑定了手机号、重置了角色这时候原来的token里存的旧信息可能已经不对了而且出于安全考虑应该让旧token尽快失效。传统JWT因为无状态服务端没法主动把旧token作废所以常见做法是引入“版本号”机制。生成token时额外塞一个ver字段这个版本号存到Redis里。用户修改关键信息后把Redis里的版本号加一旧token因为版本号对不上而失效然后再重新生成一个新token返回给前端。代码思路大致是这样public String refreshTokenAfterUpdate(String userId, String username) { long version redisUtil.incr(jwt:version: userId); return jwtUtil.generateTokenWithVersion(userId, username, version); }工具类里生成token时增加一个claimJwts.builder() .setSubject(userId) .claim(username, username) .claim(ver, version) // ... .compact();Filter解析出token后拿解析得到的ver和Redis里当前版本对比Long currentVersion redisUtil.get(jwt:version: userId); if (currentVersion ! null !currentVersion.equals(claims.get(ver, Long.class))) { writeUnauthorized(response); return; }这种方案比单纯维护黑名单好维护因为黑名单要在token过期前一直存着Redis压力大版本号只需存一个整型更新代价低。要注意的是若Redis里没有版本号可以默认放行避免老用户升级后被强制下线。5. 常见问题与排查技巧实录JWT漏洞、续签与SPA适配5.1 JWT常见漏洞盘点与防范既然热搜词里有“jwt漏洞总结”这块就必须好好讲。我在团队内部分享时列过一张漏洞清单最常见的几类如下第一算法混淆攻击。攻击者把token的Header算法改成none如果服务端没拒绝无签名token就能伪造任何用户身份。更隐蔽的玩法是RS256转HS256服务端用公钥验签RS256攻击者把算法改成HS256同时用公钥内容作为HMAC密钥去签名如果服务端没限制算法验签就会通过。对策很简单解析时使用固定的签名算法服务端不依赖Header里的alg。第二弱密钥爆破。HS256是对称密钥密钥越短越容易被人手工爆破。很多在线JWT工具都支持字典爆破一旦密钥泄露攻击者就能随意伪造token。对策是使用足够长的随机密钥并且部署时通过环境变量注入。第三Payload泄露信息。再次强调JWT的Payload不是密文Base64解码后人人可见。如果你把手机号、密码放了进去等于把敏感信息暴露在令牌里。第四重放攻击。JWT在过期时间内可以重复使用即使token泄露只要没过期就能冒充用户。对策是缩短access token有效期加入jti唯一标识并在服务端维护已注销的jti黑名单。第五存储侧XSS漏洞。如果把token放在localStorage一旦页面被注入XSS脚本token就会被盗走。更安全的方式是放在httpOnly的Cookie里但这样又引入了CSRF风险需要额外的CSRF token配合。真正的安全不是一个JWT能包圆的它需要结合传输加密、存储策略、失效机制一起考虑。每次做登录模块我都要求团队把“token放哪里”、“过期怎么办”、“泄露是否可检测”这三件事想清楚。5.2 Token续签滑动续期与Refresh Token热搜词里还有“jwt实现token续签”这是前后端分离项目中刚需的功能。JWT过期后用户如果正在填写表单突然跳到登录页体验很差。常见的续签方案有两种。第一种是滑动续期。每次请求如果发现token剩余有效期低于某个阈值就在响应头里放一个新token。前端axios拦截器检测到新token就更新本地存储。实现时在Filter里增加逻辑long expiration claims.getExpiration().getTime(); long remaining expiration - System.currentTimeMillis(); if (remaining 30 * 60 * 1000) { String newToken jwtUtil.generateToken(claims.getSubject(), (String) claims.get(username)); response.setHeader(X-New-Token, newToken); }这种方案优点是无状态实现简单缺点是每次续签会生成新token旧token在剩余时间内依然有效没法做到“旧token立即失效”。第二种是Refresh Token双token机制。服务端签发两个tokenaccess_token有效期短比如2小时refresh_token有效期长存在Redis或者数据库比如7天。前端真正访问接口用的是access_token发现access_token过期后带着refresh_token请求一个刷新接口拿到新的access_token。如果refresh_token也过期才跳登录页。Refresh Token方案更安全因为access_token暴露时间窗口短但实现复杂度高还要考虑并发刷新问题。很多SPA项目为了省事直接用滑动续期也能稳定跑起来。我个人的结论是面向C端的敏感业务优先用Refresh Token内部后台管理系统用滑动续期即可。5.3 SPA项目中的验证码、登录态和并发问题“spa项目开发之jwt验证码实现”这块我聊聊实际落地的几个细节。登录验证码通常有两种做法纯前端验证码比如滑块验证后端生成图片验证码。后端生成图片验证码的逻辑是登录前先请求/api/auth/captcha服务端生成一个图片和UUID把正确答案存到Redis过期时间5分钟key是UUID。前端登录时把UUID和用户输入的验证码一起提交。服务端先校验验证码再校验用户名密码校验完删除Redis里的验证码防止暴力重放。因为JWT是无状态的所以验证码不能直接存到JWT里用Redis是最合适的。即使将来服务端集群化Redis本身也是共享的。SPA还有一个并发问题需要处理access_token过期后页面可能同时发出多个请求每个请求都返回401前端如果对每个401都去调刷新接口刷新接口会被连续调用多次后端压力大且容易出错。前端axios拦截器里通常要做并发控制第一次遇到401时用一个Promise保存刷新动作其它请求在等待同一个Promise等token刷新完再重放请求。这个“singleFlight”模式能避免很多线上问题。5.4 问题排查速查表我在排查认证问题时经常用下面这张表快速定位问题现象可能原因解决方案登录后请求接口返回401token没放进请求头或没有加Bearer前缀前端axios统一设置Authorization头所有接口都返回401JWT密钥和生成时不一致或密钥太短检查环境变量确保长度至少32字节接口时而能访问时而401token过期时间不一致服务器时间偏差检查签发和校验时使用的时钟源本来公开的接口也要求登录Interceptor排除路径没配全在addInterceptors的excludePathPatterns中加白名单跨域请求失败OPTIONS预检请求被拦截在Interceptor中放行OPTIONS请求修改密码后旧token还能用JWT本身无状态无法主动失效引入ver版本号或黑名单机制token能在JWT官网解出用户密码开发把敏感信息放到Payload立即下线敏感字段重新签发tokenFilter里打印日志发现执行两次用了普通Filter存在转发场景换成OncePerRequestFilter这张表基本能覆盖新手团队70%以上的认证问题。遇到半天查不出原因的先别急着改代码把请求链路日志打出来看看Filter有没有执行、Interceptor有没有放行、Controller有没有收到请求。6. 进一步优化与个人经验6.1 Spring Security加持后的进化路线如果你觉得手写这套FilterInterceptor已经够用但项目复杂度还在上升可以考虑引入Spring Security。Spring Security里可以通过SecurityFilterChain配置JWT过滤器用PreAuthorize注解做方法级权限控制会话管理、CSRF、OAuth2这些能力都是现成的。手写方案和Spring Security不存在谁绝对更好。对于只有几个接口的简单后台手写代码反而直观对于大型分布式系统Spring Security的生态和扩展性更值得依赖。我见过不少团队一上来就上Spring Security结果被坑了近一个月最后连默认登录页为什么拦截请求都没搞清楚。我的建议是先从手写方案跑通业务等遇到多租户、OAuth2、细粒度权限模型时再平滑迁移到Spring Security这不丢人。6.2 落地这套方案时我踩过的几个坑最后分享几句大实话。第一不要把业务逻辑塞进Filter。我见过有人把统计逻辑、行为日志、甚至参数加密都塞进Filter导致Filter里一团糟。Filter层只需要做好两件事读token、验token、放行或拒绝。其它事情放到Service或AOP里。第二使用ThreadLocal保存用户信息要记得清理。如果你在Filter里把用户对象放到ThreadLocal别忘了在finally里remove掉。因为Tomcat线程池会复用线程不清理的话下一个请求可能读到上一次请求的用户信息这种偶现串号问题特别难以排查。第三日志里尽量不要打完整token。JWT里可能包含用户ID、角色、过期时间一旦日志系统被攻击者看到token等于变相泄露。要打就只打token前缀或用户ID并且做好脱敏。第四配置管理要用环境变量。代码仓库里的application.yml不要写死生产密钥。我在项目里用的是${JWT_SECRET:默认值}的方式开发环境用默认值生产环境用环境变量覆盖这样既方便本地调试也不会把生产密钥提交到Git。这套JWT、Filter、Interceptor的组合方案我在好几个项目中都用得挺顺手。它的好处不是某一个技术点有多高级而是把认证和授权拆成两条清晰的链路Filter解决“你是谁”Interceptor解决“你能做什么”。只要这个职责边界不被打破后面无论加验证码、续签、改密失效都只是在这些节点上做增强不会让整套认证体系崩掉。
返回列表