首页 / 资讯中心 / 文章详情

SpringBoot+Vue前后端分离注销登录实战:JWT黑名单与Nginx部署踩坑

SpringBoot+Vue前后端分离注销登录实战:JWT黑名单与Nginx部署踩坑 ★ FEATURED ARTICLE
前后端分离项目做到第七个模块基础功能都已经跑通到了注销登录这个环节。很多人觉得这不就是从localStorage里清个token、跳转回登录页嘛十行代码搞定。但真当你把项目部署到服务器上从浏览器实际点击退出按钮走一遍完整链路你就会发现事情没有那么简单接口要不要调后端token失效怎么处理部署环境下的跨域、路由、安全头哪个没配好都会出幺蛾子。这篇文章就把我在SpringBootVUE这套组合下做注销登录的完整思路、代码和部署踩坑记录整理出来希望对正在做同类项目的朋友有帮助。1. 先聊清楚前后端分离下注销登录到底在注销什么1.1 一个常见的误解前端清掉Token就完事了吗第一次做完登录功能的时候我也觉得注销就是前端删数据的事。当时的逻辑很简单登录成功把token存到localStorage退出时调用一个清理函数把token和用户信息删掉然后router.push跳回登录页。这套流程在本地开发调试的时候确实挺顺畅点击退出、跳转、重新登录一气呵成。但后来我把系统部署到测试环境拿给项目里其他同事试用问题就来了。有人反映我在A浏览器登录了账号退出之后。用同一个浏览器打开另一个系统的页面再切回来居然还能看到原来那个账号的信息。还有一次更尴尬——账号已经被管理员在后台禁用但由于前端只清了自己的token服务端那边token还有效禁用的账号通过旧的token仍然能请求到业务数据。这些现象背后的道理其实很简单在前后端分离架构里token是服务端签发的一个通行证前端删除只是在本地把它撕掉了服务端并不知情。如果token本身还有效它依然可以访问接口。从前端视角看用户是退出了从服务端视角看这个凭证还活着。所以不要以为清掉localStorage就是注销成功了这只是整个注销流程的前半段。1.2 业界主流的三种注销方案对比那服务端怎么感知用户的注销行为呢我梳理下来常见的方案主要有三种。第一种是纯前端注销也就是我刚说的删localStorage、跳登录页服务端完全不管。这个方案最简单适合token有效期很短比如15分钟以内、且系统对安全性要求不高的场景。缺点是token在剩余有效期内仍然可以用来请求接口严格来说存在安全隐患。第二种是Session配合Cookie。服务端把登录状态存在Session里Cookie只保存一个SessionId注销时调用后端接口让Session失效。这种方案在后端渲染的传统项目中很常见但放在前后端分离、接口需要被多端调用的场景下就不太灵活了。第三种是JWT配合服务端黑名单。JWT本身是无状态的我们无法直接让它失效但可以在服务端维护一个已注销token的黑名单在接口鉴权时先检查token是否在黑名单里如果在就拒绝访问。这就是我在模拟项目X里采用的方案。三种方案各有利弊但如果你跟我一样用的是SpringBootVue并且token是JWT那方案三是最贴合实际需求的。黑名单机制不复杂却能真正实现服务端感知注销而且对现有接口代码的侵入很小。1.3 我们这套项目在注销环节的具体选型我们做的是一套企业内部的管理系统技术栈是SpringBoot 2.7 Vue 3 Vitetoken用的是JWT前端状态管理用的Pinia路由用的Vue Router 4。部署环境是Nginx托管前端静态资源SpringBoot以jar包形式跑在一台服务器上通过Nginx反向代理把/api/路径转发到后端服务。基于这个背景我在设计注销登录时定了几条原则前端必须调用后端注销接口由后端把token加入黑名单确保服务端态度明确。前端在收到注销接口成功响应后再清理本地存储的状态顺序不能反。无论注销接口是否调用成功前端本地状态最终都要清理不能让用户卡在退出中的界面上。多标签页场景下一个标签页退出其他标签页也要感知到并同步清理状态。部署环境下要保证注销后的页面跳转不会出现404、死循环之类的状况。这几条原则看起来简单但每一条在编码时都有对应的细节要处理。后面我会逐个展开。2. 后端实现SpringBoot注销接口与Token失效机制2.1 登录时的Token签发逻辑是怎么设计的要讲清楚注销得先回头看登录。我们的登录接口在验证账号密码通过后会生成一个JWT然后在token里携带了三类信息用户ID、用户名、以及一个全局唯一的jtiJWT ID。这个jti用UUID生成每次登录都不一样它的用途就是让token可以被唯一标识后面做黑名单的时候全靠它。签发token的代码大致是这个样子private String generateToken(LoginUser loginUser) { String jti UUID.randomUUID().toString(); long now System.currentTimeMillis(); long expireMillis now jwtProperties.getExpireMinutes() * 60 * 1000; return Jwts.builder() .setId(jti) .setSubject(loginUser.getUserId().toString()) .claim(username, loginUser.getUsername()) .setIssuedAt(new Date(now)) .setExpiration(new Date(expireMillis)) .signWith(Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8))) .compact(); }这里有一个容易被忽略的点jti是后来专门加进去的字段。早期版本我以为JWT只用sub和exp就够了等到要设计注销功能时才发现没有jti的话服务端根本没有办法精确地指认哪一个token需要被注销。同一个用户多点登录时会签发多个token光靠userId做不了唯一标记。所以如果你现在还没给JWT加jti建议在实现注销之前先补上。2.2 注销接口的签名设计与返回格式注销接口我定义成了一个POST请求路径是/api/auth/logout而不是DELETE。主要原因有两个一是很多浏览器和网络代理对DELETE的支持不够友好二是注销这个动作在业务语义上是使凭证失效并不真的是在删除某个资源对象POST更贴合场景。接口接收的参数只是请求头里那个Authorization即Bearer xxx格式的token。这里面有个关键问题既然token在鉴权失败时会返回401那么注销接口自身要不要做鉴权我的做法是注销接口不走全局的鉴权拦截器但仍然会尝试解析请求头里带过来的token。这样设计是刻意为之。原因在于用户的token可能已经过期了客户端带着过期的token来注销如果我们的拦截器先抛一个401用户的实际体验就是我点了退出前端却报401。正确的做法是只要请求头里有token不管是否过期后端就尝试解析并将对应的jti加入黑名单如果完全没有token也直接返回成功因为这种情况下服务端本来就没有可注销的东西。接口的核心代码如下PostMapping(/logout) public ResultVoid logout(HttpServletRequest request) { String token resolveToken(request); if (StringUtils.hasText(token)) { try { Claims claims Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .getBody(); tokenBlacklistService.addToBlacklist( claims.getId(), claims.getExpiration().getTime() - System.currentTimeMillis() ); } catch (JwtException e) { // token已过期或非法不需要加黑名单直接忽略 } } return Result.success(); }注意看catch分支里的注释这里有个很微妙的地方。如果token因为过期被解析失败说明它本来就已经过期了服务端不需要再把它加进黑名单因为鉴权逻辑本身会校验过期时间。真正需要加黑名单的只有那些还没过期但用户主动退出的token。2.3 Token黑名单方案落地为什么用Redis而不是内存Map黑名单存储我选的是Redis没有用应用内存的Map。理由很现实这个系统以后一定会上多实例部署。一旦做了负载均衡用户第一次请求落在A实例把token写进A的内存下一次请求被转发到B实例B的内存里根本没有这条黑名单记录token照样能通过鉴权那注销就等于白做了。Redis是所有实例共享的存储不存在这个问题如果你的项目规模比较小确定不会做多实例临时用内存Map也不是不行但我不建议这么干部署架构只要动一次改造的成本就够你喝一壶了。往Redis里写黑名单记录时key我用的是logout:token:加jtivalue存的是1这样的占位符最重要的是设置过期时间。过期时间不能瞎写我用的逻辑是取出token的过期时间戳减去当前时间戳得到剩余有效毫秒数转成秒后作为Redis的TTL。这样token原本什么时候过期黑名单记录就在什么时候自动消失不需要额外做定时清理任务。接入黑名单校验也不复杂。在SpringBoot的全局鉴权拦截器里解析出token的jti后先查一下Redis里有没有logout:token:加jti这个key如果有直接视为未登录返回401。这一步是一个简单的Redis查询性能损耗非常小但换来的是服务端真正掌握了注销的主动权。2.4 容易被忽略的细节幂等性与并发场景注销接口还有个细节是幂等性。用户在前端连续点了两次退出按钮或者第一次请求超时后自动重试后端会不会出问题由于黑名单写入用的是Redis的SET命令同样的jti重复设置几次结果都是一样的天然幂等所以这个接口安全。但并发场景有点意思。假如用户同时打开了两个页面一个页面执行注销另一个页面正在发起业务请求。这两个请求几乎同时到达后端业务请求先到鉴权通过注销请求后到token进黑名单。这是顺序执行的情况没问题。另一种情况是注销先到、业务请求后到那后来的业务请求就会被拦截返回401前端拦截器发现401就会强制跳登录页。这其实是合理的表现——用户确实退出了另一个页面的请求失败是预期行为。有点意思的是如果业务请求正在处理的过程中已经通过了鉴权还没返回结果注销请求同时进来这个处理到一半的业务还是能正常返回。因为JWT黑名单的检查只发生在请求进入拦截器的那一刻不中断已经在执行的服务逻辑。理解这一点很重要能让你在设计失败重试机制时心里有底。3. 前端实现Vue端的注销流程编排与状态清理3.1 退出按钮的事件绑定与交互细节前端这边的入口通常在顶部的用户信息下拉菜单里一个退出登录按钮。Vue组件模板里大概是这样的a-dropdown span classuser-info{{ userStore.name }}/span template #overlay a-menu a-menu-item keylogout clickhandleLogout退出登录/a-menu-item /a-menu /template /a-dropdown点击事件对应的处理函数我倾向于加一个确认弹窗。虽然看起来多了一步操作但在真实业务里非常必要。很多误操作就是这样发生的——在公共电脑上只想暂时离开电脑结果手滑点了退出回来要重新输入账号密码。加个确定要退出登录吗的确认提示能挡住大部分误触。如果用的是Element Plus或Ant Design Vue用它们的Modal.confirm组件写确认框就好。这里有一点要提醒不要在确认框的回调里直接把确认操作写成异步等待否则弹窗关闭时机和后续跳转顺序容易错乱。我踩过一次坑最后把确认回调里的逻辑封装成独立函数确保持仓的清理和路由跳转在弹窗关闭后再执行。3.2 注销接口调用与本地状态清理的正确顺序这是整个前端注销流程里最核心的一段代码。正确的顺序应该是先调用后端注销接口再清理前端本地状态最后跳转登录页。const handleLogout async () { try { // 1. 通知后端把token加入黑名单 await authApi.logout(); } catch (error) { // 即使后端接口异常也继续后面的清理动作不留死角 console.warn(注销接口调用异常继续清理本地状态:, error); } finally { // 2. 清理本地存储 userStore.resetState(); // 3. 跳转登录页并带上重定向参数 router.replace({ path: /login, query: { redirect: router.currentRoute.value.fullPath } }); } };这里我把finally这个块特意用了三层注释拆开因为每一步的作用都值得说清楚。为什么要先调后端再清本地因为如果先清本地token随后发送的logout请求就不会再携带Authorization头后端就收不到这个token黑名单自然也没法写。有些朋友可能会说那我在按钮点击的回调里先记录一下token的值然后再清最后请求时手动把旧token传上去也不是不行。但这样代码就多了一个中转变量而且容易在代码评审时被同事问一句为什么不用拦截器统一加。放在finally里清理是第二重保险。设想一个场景后端接口超时前端等了30秒才报错如果代码是先await再清用户在这30秒内什么都点不了体验很差。如果接口返回的是网络错误请求根本没有到达服务端但用户已经决定要退出了本地状态必须清掉否则用户刷新一下页面又进系统了那才是真的尴尬。所以无论结果如何finally里清理本地状态是必须的。3.3 路由守卫、Axios拦截器与注销流程的联动做注销登录不能只盯着注销按钮这一个入口。系统里还有两个隐形出口需要处理一是token过期二是访问被拦截。Axios响应拦截器是处理当前携带的token已失效的哨兵。我在拦截器里增加了针对401的专门处理service.interceptors.response.use( (response) response, (error) { if (error.response?.status 401) { // 如果当前已经在登录页不重复处理 if (router.currentRoute.value.path /login) { return Promise.reject(error); } // 防止并发多个请求都触发401重复弹跳转 if (!isRedirecting) { isRedirecting true; userStore.resetState(); router.replace({ path: /login, query: { redirect: router.currentRoute.value.fullPath } }).finally(() { isRedirecting false; }); } } return Promise.reject(error); } );这里有两个细节是实际使用中才暴露出来的。第一个是重复跳转问题一个页面同时发出三个业务请求三个全部收到401拦截器跑了三次就会触发三次router.replace控制台一片报错。我加了个isRedirecting的布尔标志确保同一时刻只处理一次跳转。第二个是当前已在登录页的短路条件否则登录页自己发出的验证码接口如果返回401会把用户又带进登录页造成死循环。路由守卫这块我用的是白名单加token判断。白名单包括/login这个路径其他所有页面都要求token存在才放行。这个机制配合上面的401拦截器基本上能覆盖绝大多数非法进入系统的场景。有一点要特别提一下如果前端用的是router.replace而不是router.push来做登出跳转用户按浏览器返回键是回不到跳转前那个受保护页面的。replace会替换当前历史记录而push会新增一条后者给了用户返回的漏洞。我见过有项目登出后按返回键又看到系统页面的案例排查到最后就是push的问题。3.4 多标签页场景下的同步注销管理系统里用户开多个标签页是非常普遍的行为。用户在一个标签页点了退出跑到另一个标签页发现居然还是登录状态这个问题在早期的版本里真实困扰过我们。后来我用浏览器的storage事件解决了。思路是这样的在应用初始化时监听window的storage事件当数据存储发生变化时判断变化涉及的是不是token相关的key如果token被清除了就说明有另一个标签页执行了注销当前标签页也立即清理状态并跳转登录页。window.addEventListener(storage, (event) { if (event.key TOKEN_KEY !event.newValue) { userStore.resetState(); router.replace(/login); } });这个方案看起来直接但有个前提必须说明storage事件只在其他标签页触发不会在触发修改的那个标签页里触发。所以它恰好覆盖了多标签页这个场景与单标签页的正常注销流程互不干扰。更妙的是我们只需要监听token被清除的情况不需要额外写广播消息的逻辑代码量极少稳定性却不错。4. 部署环节注销登录在Nginx与SpringBoot部署环境下的联动4.1 前后端分离部署的基本结构与影响开发环境下前端跑在Vite的5173端口后端跑在8080端口两者天然跨域。开发时我们用Vite的proxy把/api代理到后端一切正常。但部署到生产环境如果还是前端一个域名、后端一个端口那跨域问题会延续到线上CORS、Cookie、Authorization头都要额外配置麻烦不说还容易留下安全隐患。我推荐的生产环境部署方式是同域部署Nginx同时承担静态资源服务器和反向代理服务器。前端构建后的dist目录放到Nginx的html目录下用户访问的是同一个域名和端口Nginx把/api/开头的请求转发给内网里的SpringBoot服务。这样前端页面和接口在用户眼里是同源的跨域问题从根源上消失Cookie之类的处理也简单很多。这个结构对注销登录的影响是正向的。最典型的一点是前后端同域之后我们可以在前端直接使用相对路径/api/auth/logout而不需要维护一份区分开发环境的API地址配置。少一个变量就少一份出错的可能。4.2 Nginx反向代理配置对登录注销的影响Nginx配置有几个关键位置直接关系到注销登录能否正常工作。下面是我在实际项目中使用的配置片段server { listen 443 ssl; server_name example.com; # 前端静态资源目录 root /usr/share/nginx/html; index index.html; # 支持History模式路由刷新或直接访问深层路径时不返回404 location / { try_files $uri $uri/ /index.html; } # API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }注释里写了一个非常关键的点try_files那条配置。前端路由用的是History模式URL里没有#号。如果用户访问的是系统里的用户管理页面路径是/user/list直接刷新浏览器Nginx会在静态目录里找user/list这个文件找不到就返回404。有了try_files配置Nginx会把所有找不到的路径都回退到index.html由前端路由接管并渲染对应页面。这个配置跟注销登录的关系在于用户退出后跳转的登录页通常是/login如果没配try_files退出跳转在部署环境里就会变成404白屏。还有一点要留意的是proxy_pass后面的斜杠。我写的是http://127.0.0.1:8080/注意这个斜杠不能省。它会把请求路径中/api/前缀的那部分去掉再转发给后端。比如前端请求/api/auth/logoutNginx转发给后端的是/auth/logout。如果去掉斜杠转发路径就变成/api/auth/logout而后端Controller里定义的是auth/logout没有/api这个前缀就会直接404。这个坑非常隐蔽因为开发环境下Vite proxy不一定有同样的行为代码在本地怎么测都好使一部署就挂。4.3 HTTPS与安全响应头的处理部署环境还有一个和注销登录强相关的点HTTPS。现在但凡面向真实用户的系统基本都上了HTTPS因为HTTP协议下传输的明文token很容易被中间人截获。登录、注销都是涉及凭证的操作更应该在HTTPS的加密通道里进行。我们为Nginx配置了SSL证书并做了HTTP强制跳转HTTPS。此外还加上了几个安全相关的响应头add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always; add_header Strict-Transport-Security max-age31536000 always;如果登录时用的不是localStorage而是httpOnly Cookie还需要在Set-Cookie里加上Secure和SameSiteStrict属性。httpOnly能防止JavaScript读取Cookie从根上拦住了XSS窃取凭证的攻击路径。不过这要看项目的登录方案选型如果坚持用localStorage存token那httpOnly这条就不适用了。我们自己这个项目考虑到后续要做单点登录对接token方案更灵活所以保留了localStorage但这类安全策略是否调整还是建议形成团队共识后再定。4.4 部署后的注销链路验证清单部署完之后我习惯用一套完整的验证清单来检查注销登录功能是否百分百正常。这套清单是项目从测试环境到生产环境反复验证后总结出来的照着走一遍能帮你覆盖大多数问题。第一项正常退出流程验证。登录系统点击退出确认弹窗出现确认后跳转登录页URL正确且没有404。打开浏览器开发者工具Network里能看到/logout请求返回200。第二项返回键验证。退出登录后按浏览器返回键确认不会回到受保护页面。如果还能看到系统首页说明router用的是push而不是replace或者路由守卫有遗漏。第三项token失效验证。退出后把之前留在localStorage里的token复制出来通过Postman或curl手动请求一个业务接口确认返回401而不是200。这一步验证的是后端黑名单是否真的生效。curl -X POST https://example.com/api/auth/logout \ -H Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... -i curl -X GET https://example.com/api/user/list \ -H Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... -i第一条命令执行后隔几秒再跑第二条返回401就说明黑名单生效了。第四项多标签页验证。开两个标签页都登录在一个标签页退出另一个标签页应该在几秒内自动回到登录页。如果没反应检查storage事件的监听代码是否只在开发环境跑了。第五项刷新验证。退出状态下的登录页按F5刷新应该还是停留在登录页不会跳回系统。这五步走完基本可以确认部署环境下的注销登录链路是通的。5. 实战踩坑记录注销登录相关的常见问题速查5.1 退出登录后按浏览器返回键还能看到系统页面这个问题遇到过不止一次也帮别人排查过。根本原因几乎都是前端跳转用了router.push而不是router.replace。push的含义是在当前历史栈上压入一条新记录所以退出前的页面地址还在历史栈里用户按返回键就能回到它。replace则是用新地址替换当前记录退出前的地址不存在了返回键自然回不到系统里。解决办法是把退出和401跳转里的router.push统一改成router.replace。注意除了手动点击退出按钮Axios拦截器里的401跳转也要同步修改很多项目漏掉的就是后者。如果项目里有多处跳转登录页的代码建议全局搜一下beforeEach、catch里所有涉及登录页跳转的地方。还有一种隐蔽情况是浏览器页面用bfcache缓存了页面状态返回时直接从缓存恢复连JavaScript都没重新执行路由守卫拦不住。这种情况最彻底的方案是在登录页挂载时用performance.navigation.type判断一下是不是通过返回键进入如果是就直接清状态。不过这种情况在真实项目中比较少见遇到再处理也来得及。5.2 注销接口明明只改了Redis却偶尔出现未注销成功这个问题的排查过程有点曲折。现象是用户退出后立即用旧token请求接口偶尔能成功过几秒再请求就401了。一开始我以为Redis写入延迟后来发现不是。真正的原因是Nginx的缓存。我们的Nginx配了proxy_cache后端响应会被缓存。注销接口返回成功后用户拿着同一张token再次访问业务接口本来应该被鉴权拦截但由于业务接口的响应被Nginx缓存了请求根本没有打到后端自然也不会经过后端的Redis黑名单检查直接由Nginx返回了缓存结果。用户看到的现象就是token还能用。对API接口开启缓存必须非常谨慎尤其是涉及认证鉴权的接口。正确的做法是对包含Authorization头的请求一律跳过Nginx缓存或者干脆在API代理配置里关闭缓存只对静态资源开缓存。我最终选择了关闭API层的proxy_cache只保留前端静态资源的浏览器缓存问题彻底消失。这个教训让我对Nginx默认配置多留了个心眼。5.3 前端拿到了401就跳转但登录成功后却回不到原页面这个问题跟注销登录间接相关但流程是连在一起的。项目中配置了redirect参数退出时跳转/login?redirect/user/list登录成功后应该回到/user/list。但有时候登录成功后却被带到了首页仔细排查发现是sessionStorage里存的redirect路径在页面刷新后丢了。我们用的是Pinia管理状态但Pinia默认是纯内存的刷新页面后状态清空。如果redirect只放在Pinia里刷新一下就没有了。实现登录后回跳的两个要素是跳转前把redirect值写入sessionStorage登录成功后先读sessionStorage再决定路由目标读取后清掉这个值。sessionStorage是标签页级别的存储恰好每个标签页的登录状态是独立的用在这里语义正好。5.4 退出之后重新登录浏览器自动填充了旧密码安全吗这是一个容易被忽视的体验和安全问题。退出登录后系统自动跳到登录页浏览器很可能把之前记住的账号密码自动填充到表单里用户只需点一下登录就能进去。对个人电脑来说这是方便但如果是公共电脑这就是安全隐患。处理方式有两个层面。第一个是前端在登录成功后把autocomplete设为off提示浏览器不要记住这个输入框的密码。但autocompleteoff在部分浏览器里并不总是生效尤其Chrome对密码框的干预很强。第二个更稳妥的做法是在系统里提供一个清除本地缓存的选项退出时不仅清token也可以询问用户是否需要清除浏览器保存的账号信息或者干脆在公共电脑场景下引导用户使用无痕模式。这个问题技术上没有银弹但要意识到它的存在在产品层面做取舍。5.5 注销期间遇到网络闪断本地状态清理了服务端黑名单没写入最后聊一个分布式场景下绕不开的问题网络闪断导致前端发起注销请求时请求根本没有到达服务端但前端拿到了网络错误于是按finally里的逻辑把本地状态清掉了。这时候用户的视角是我退出了但服务端视角是token还在白名单里。有一种思路是前端检测到网络闪断后把待注销的token存到本地一个待注销清单里下次网络恢复时补发。但说实话这个方案在真实项目中性价比不高因为token本来就有有效期就算黑名单没写入它也会在到点后自动过期。如果非要在网络层补一手建议给注销接口的上游再加一道会话标识校验比如要求请求同时携带userId和当前活跃的token一旦userId对应的活跃token列表发生变化旧会话立即失效。这个做法比前端补发更可靠但要改登录会话管理的整体设计一般系统用不上。我在这个模拟项目X里采用的是折中策略黑名单机制保证正常网络下服务端能感知注销token的短有效期兜底异常网络下最坏情况也只是等token自然过期两者配合既能满足安全要求又不至于把架构搞复杂。整个注销登录模块从设计到部署花的时间比我预期多不少。但做下来最深的体会是登录是发凭证注销是收凭证两端不对称必须同一套设计语言。前端清状态让大家感觉退出了后端黑名单让大家确实退出了部署环境的Nginx配置则是把这条链路稳稳地接到生产环境。这里面每一步都不算重但每一步都值得亲自用真实环境验证一遍。如果你正在做类似的前后端分离系统建议把本文提到的验证清单拉到项目里逐条跑一遍尤其是返回键和401拦截这两块测过之后你会对注销登录这四个字有新的认识。
阅读完成 · 觉得有帮助?
咨询建站