这几年我做API网关和开放平台类的项目不算少但真正让我下决心把整套技术栈推倒重来的还是这个API灵钥系统。简单说灵钥系统就是管理API Key的“管家”在过去它只是给内部服务发钥匙、验钥匙如今要改造成一个面向多方的API交易平台让开发者能在线申请密钥、购买调用额度、实时查看账单。原来的系统是Java 8 Vue2 jQuery混写功能能跑但已经撑不起新业务于是我从去年底启动了基于Java 17 Vue3的全量重构目标很朴素更强大、更安全、更好维护。这篇文章就围绕这次重构记录我踩过的坑、做过的取舍以及真正跑上线后才想明白的一些事。如果你也在做类似的后台管理系统或API开放平台这篇文章会给你一些能直接落地的思路尤其是密钥存储、交易计费、前端权限这些容易出问题的地方我都把当时的方案和理由摆出来方便你根据自己团队的情况取舍。1. 项目缘起为什么非重构不可先说清楚背景。灵钥系统是2018年上线的当时只服务公司内部两个产品线由后端团队顺手搭了套Spring Cloud Vue2 Element UI的单体站。功能不算复杂管理员创建分组组下面生成密钥其他业务系统调用时带着密钥头来换取能力。后来公司开始做开放平台API交易业务要求第三方开发者也能注册、购买API调用量这就开始露怯了。1.1 老系统撑不起来的三个直接原因第一个原因是代码耦合太严重。密钥的签发和校验逻辑散落在订单、用户、网关等多个服务里同一个密钥在三个地方分别查库改一处还要担心另外两处漏掉。新团队的成员接手时光摸清楚密钥状态流转就花了两周。第二个原因是安全等级跟不上。老系统的密钥是明文存在MySQL里的登录后台可以看到完整Key。内部使用还好开放给第三方便成了底线问题一旦数据库泄露所有密钥全部见光。而且当时用的是很简单的前后端一体登录没有细粒度的权限体系。坦白讲早该重构了。第三个原因是没法做交易。API交易平台要有配额、计费、账单、发票这些概念老系统只有“生效/失效”两个状态连“试用额度”“按量扣减”都没有数据模型硬往上加只会越加越乱。1.2 重构的目标与范围控制重构很容易翻车所以我一开始就和团队定了三条原则不改变已有密钥的调用协议保证老客户不感知只重构必需的核心链路砍掉所有“顺手优化”的冲动每一阶段必须可独立上线、可回滚。范围上锁定五块密钥生命周期管理、网关鉴权、配额计费、开发者控制台、运营后台。旧系统里确实有价值的业务逻辑比如签名算法兼容、错误码规范我直接保留不重新发明。这样定义下来整个重构就不是“推翻”而是“分层替换”。事实证明这个范围控制让重构在六周后就能灰度开放没有出现业务空窗期。2. 技术选型Java 17 与 Vue3 的组合逻辑选型这件事其实被很多人说复杂但只要想清楚要解决什么问题答案会自己浮出来。老后端是Java 8前端是Vue2重构自然要往上升级。Java 17是当时最新的LTSVue3也稳定了两年生态成熟度足够所以组合比较顺理成章。2.1 为什么从 Java 8 一步跳到 Java 17Java 8到Java 17跨度很大中间隔着9、11、17三个版本。我选择Java 17而不是Java 11主要是看重这几个点record定义API返回对象和值对象非常简洁。密钥申请返回的DTO就几行代码不用再写一大堆getter/setter/equals/hashCode。sealed class限制枚举类型继承适合做密钥状态的强约束编译期就把非法状态框死。switch 表达式与文本块日志模板和SQL片段用文本块读起来清爽很多switch表达式让状态机逻辑更紧凑。ZGCJava 17的ZGC已经不再是实验特性虽然老系统不是高并发低延迟场景但升级到17后GC停顿明显下降这是意外收获。需要注意Java 17里并没有虚拟线程那是Java 21的正式特性。如果团队有人跟你说“升级到17就能用虚拟线程”得先确认清楚。我觉得Java 17最大的价值不是某一个杀手级特性而是整个运行时在性能和容器支持上的成熟度。基于Linux容器可以自动识别CPU和内存限制没有JDK 8时代那么多奇奇怪怪的容器问题。2.2 Vue3 Vite TypeScript 的选择取舍前端我选了Vue3 Vite TypeScript Pinia Element Plus。Vue2到Vue3最直观的变化是组合式API把同一功能的逻辑收拢到一起这对灵钥系统特别实用。比如“密钥创建”这个功能原来在data、computed、methods、watch里各写一段改需求时得上下翻用组合式API之后我把创建逻辑封装成一个hook里面自己管理状态和副作用其他页面可以直接复用。Vite替代Webpack的收益是开发期几乎秒级启动。老前端项目改一次配置要等十几秒甚至几十秒迁移到Vite后热更新基本是即时的团队反馈“终于愿意改代码了”。TypeScript从第一天就上了API Key系统对类型安全的要求很高账单金额、配额数这些字段如果都是any后面做报表会哭。Pinia替换Vuex也让状态管理更轻没有mutations那层模板代码代码量肉眼可见地少了。2.3 前后端接口设计与版本管理为了让前端和后端能并行推进我花了两个工作日把接口规范定死。统一返回结构是{ code, message, data, requestId }业务错误也走HTTP 200只在code里区分这样前端拦截器处理起来简单。接口版本用URL路径/v1/keys、/v2/keys避免破坏性变更影响老用户。同时所有接口必须配OpenAPI文档后端用springdoc自动生成前端用openapi-typescript生成类型定义。这样前端拿到的类型是服务端定义直接产出的不会出现两边手写不一致的问题。鉴权上把“平台用户Token”和“开发者API Key”明确分开前端登录用的是JWT第三方调用用的是API Key签名两者走完全不同的验证链路。这个设计在后面做安全增强时省了大力气。3. 核心设计API交易平台的关键模块拆解API交易平台和普通管理系统最大的区别是它要把“密钥”当成商品来经营。密钥不再是数据库里一条状态记录而是活的生命体有创建、启用、暂停、轮换、过期、销毁这些状态还要跟配额、计费、限流、审计这些横切逻辑联动。我把核心模块拆成三个部分来讲。3.1 密钥全生命周期管理密钥的生命周期我定义了七个状态已创建、待激活、启用中、已暂停、已轮换、已过期、已销毁。从创建开始后端生成一串32字节的密钥但不再直接把完整密钥存库。密钥生成时的明文只允许在创建成功的弹窗里展示一次数据库里存的是带盐的SHA-256摘要配合加密后的密文做可逆校验这样既保证以后能识别密钥又避免了明文泄露一锅端的风险。这个方案会在第5章展开。每个密钥可绑定一组策略比如只能用哪些API、每天最大调用次数、允许的来源IP段、过期时间。密钥创建接口的入参里有一组PermissionSet系统会把策略转成内部权限树网关校验时直接匹配内存中的权限位图。权限校验设计成位操作而不是逐条数据库查询是因为网关层QPS上去后不能每次调用都查数据库。轮换逻辑很关键。老系统里运维要换密钥只能删除重建很容易导致线上调用中断。重构后支持“双密钥并行”新老密钥共存一段时间老密钥标记为“轮换中”等线上流量切到新密钥后再手工完成退役。这样开发者的应用不会因为密钥变更出现空窗期。3.2 多租户配额与计费体系API交易平台天然是多租户结构。我引入了workspace工作空间的概念一个账号可以有多个workspace每个workspace下管理多个API产品订阅和密钥。配额模型不是简单的总量而是分三层订阅级总配额、密钥级速率限制、请求级计量主表。订阅级总配额记录用户购买了多少调用量例如100万次/月。密钥级速率限制用令牌桶算法每秒不超过N个请求。请求级计量主表则是每次调用都落一条轻量记录异步写入ClickHouse用于实时统计和账单生成。这三层之间有一个扣减引擎当请求流入时先通过网关限流再判断订阅配额是否足够。扣减操作必须是原子的这里用了Redis的Lua脚本或者数据库行锁具体要看并发量。我上线前用JMeter压过单机网关每秒两千次请求时配额扣减平均耗时低于5毫秒没有出现超卖。计费方面重写了一套账单模块支持预付费和后付费两种模式。预付费是先充值到余额每次调用从余额扣减后付费是按月汇总用量生成账单。余额变动和用量单必须双写通过本地事务表加MQ最终一致。之前设计时差点用“先更新余额再下发用量单”的朴素方案后来发现极端情况下会重复扣费改成可对账的流水表才踏实。3.3 网关层限流、审计与风控网关是API交易平台的前哨。重构后我用Spring Cloud Gateway做了一层专门的API网关并把密钥校验逻辑从业务服务抽到网关的GlobalFilter里。网关只做四件事识别请求、校验签名、执行限流、记录审计日志。业务服务里不再有查密钥的代码密钥校验的耦合被彻底切断。限流策略可以针对API、密钥、IP、用户维度灵活配置。底层用Bucket4j加Redis实现分布式限流支持令牌桶和滑动窗口两种模式。配置界面在后台可以动态调整不需要重启网关。有一次压测时发现限流器和审计日志同时写数据库会把连接池打满后来审计日志改为异步批量写限流部分完全在Redis里完成数据库压力一下就下来了。风控模块是我这次着重加进去的。在线检测项包括短时间内大量401失败、非工作时段异常调用、单IP跨多账号访问、密钥调用地点突变。这些都会触发告警严重时自动暂停密钥。不能说这套规则百分百准确但它确实拦截了几次开发者误用导致的密钥泄露事故。4. 前端重构从 Vue2 到 Vue3 的实战要点前端部分我原本以为只是版本升级实际操作下来工作量和后端差不多。特别是老代码里大量使用Vue2的Options API和自定义指令迁移过程中几乎每个页面都要重新过一遍。4.1 组合式API与业务组件的拆解迁移的核心策略不是机械地把data里的字段改成ref而是对每个页面重新梳理业务逻辑再按职责拆成组合式函数。以密钥列表页为例原来的页面逻辑分散在十几个methods里互相引用。重构后我拆出了四个hookuseKeyList负责表格数据和搜索、useKeyCreate负责创建流程、useKeyAction负责启停轮换、useKeyStats负责图表数据。每个hook接收必要的参数返回一个独立的上下文页面模板只调用hook暴露出来的状态和方法。这样做的收益很直接新增一种密钥操作时不用改动模板只要扩展对应的hook。有两点需要提醒组合式API虽然灵活但不要滥用ref。对象类型的响应式数据建议直接用reactive并且配合toRefs解构。我发现很多初学者习惯把所有变量都包成ref结果模板里到处是.value看着难受排查时也容易漏掉。另一个点是watch和watchEffect的适用场景不同依赖外部数据用watch自身内部状态联动用watchEffect两者混用会造成多余的触发。4.2 动态路由与权限控制后台管理系统的权限体系这次也做了重写。Vue2时代是用完整路由表一次性注册然后根据角色过滤菜单。现在改成了登录后根据用户角色动态向路由表addRoute后端返回的菜单树和前端路由表通过路由name关联。这个改造在安全上有意义未授权页面不会被打包进访问路径即使有人强行输入URL也找不到组件。但动态路由也有坑比如刷新页面后路由表会丢失需要在应用启动时先等待用户信息接口返回再决定渲染哪些路由。如果后端是异步返回的前端要配合做一个loading状态否则会出现空白页。权限指令我也做了迁移。Vue2里自定义指令的钩子是insertedVue3改成了mounted而且指令的作用域和生命周期都有了变化。老代码里如果不改控制按钮的显示状态会失效。我写了个v-permission指令接收一个权限码数组比对当前用户的权限集合没权限就直接移除DOM节点。这里有个细节指令操作DOM要用el.parentNode.removeChild(el)而不是v-if的响应式逻辑否则不会触发更新。4.3 性能优化与构建体积控制密钥系统的后台有大量表格、图表和JSON日志展示性能优化是必须做的。组件库我选择了Element Plus并开启按需导入配合Vite的打包分包首屏JS体积从老系统的1.8MB降到600KB左右。路由懒加载是常规操作但要注意import()的动态路径不能是纯变量Vite才能正确分割chunk。遇到一个比较头疼的性能问题是调用日志的展示。一次查询可能返回上万条记录用普通表格渲染一刷新就白屏几秒。后来我把日志列表改成了虚拟滚动用的是tanstack/vue-virtual只渲染可视区域的行滚动时动态替换DOM。配合后台分页实际体验顺畅了很多。对于大日志内容默认折叠成一段摘要点击展开才显示完整JSON这招很值得推荐。5. 安全性增强把密钥当成钱来守做API交易平台安全不是可选项而是命门。密钥就是用户的资产一旦泄露不只是流量被盗更会引发信任危机。这次重构我从三个维度做安全加固存储、传输、审计。5.1 密钥存储与加密方案前面说过完整密钥不以明文入库。但API Key和密码不同密码只需要单向验证API Key需要用于签名计算和请求鉴权所以光有哈希不够还要有可逆的密文用于校验。我的方案是这样的密钥生成后明文只在创建时返回一次。数据库存两列key_digest存带盐的SHA-256摘要用于快速查找和防止撞库key_ciphertext存AES-GCM加密后的密文私钥放在独立的密钥管理服务里不放在业务数据库。访问密钥列表时后端永远只返回掩码后的部分字符例如sk_live_ab...xyz。轮换和销毁都走服务间调用不能直接改数据库记录。AES-GCM的选型是因为它自带认证标签可以防止密文被篡改。加密的随机数IV每次生成不能复用。密钥管理服务可以理解成一个独立的保险箱即使应用数据库被拖走了攻击者也拿不到加密密钥。实际部署时加密密钥存在KMS里应用启动时通过一次授权将密钥加载到内存不用明文配置文件。5.2 传输安全与防重放攻击传输层安全是底线。全站启用HTTPS打开HSTS同时关闭TLS 1.0/1.1只允许TLS 1.2以上。这个在Nginx或网关配置里都有模板五分钟能搞定但很多人会漏掉。要知道不配HSTS浏览器访问过一次HTTPS后下次还可能走HTTP中间人就有机会劫持。API请求的安全不仅仅靠HTTPS因为到了网关后面的服务间调用大多还是明文HTTP所以要在应用层做防重放。我实现了标准的签名方案请求头带X-Timestamp、X-Nonce、X-Signature。签名内容是HTTP方法、路径、请求体的SHA-256摘要、时间戳、随机串组合后用API Key对应的私钥做HMAC-SHA256。网关会校验时间戳是否在5分钟窗口内同时把Nonce存到Redis并设置过期防止同一个随机串被重复请求。有人会问网络层面已经有TLS了为什么还要做防重放因为TLS保护的是通道我防的是合法密钥在更大范围内的重放尤其是在服务间调用和日志回放场景里这是很多平台都忽略的。5.3 审计日志与异常告警审计日志的设计要兼顾完整性和性能。每次API调用都要记录请求ID、时间、调用方workspace、密钥ID、API路径、响应码、延迟、来源IP、UA、SDK版本。这些数据统一写入Kafka再异步落到ClickHouse用于对账和风控。日志里绝对不能出现完整密钥所以我加了一层脱敏过滤器在写入前用正则把sk_live_打头的字符串替换成掩码。漏掉这一步审计系统本身就是泄露源。告警规则我设了三档第一档是速率告警比如某个密钥调用量在30秒内超过阈值第二档是错误率告警例如5分钟内401比例超过30%第三档是风控告警例如调用来源城市出现漂移。告警通过钉钉、邮件和站内信同时推送。有一次真实场景里一个开发者在GitHub上误传了密钥45分钟后我们的风控就检测到大量来历不明的调用自动暂停了那个密钥等于帮客户止损了一次。6. 常见问题与排查实录重构过程中遇到很多问题有些是技术选型带来的有些是老代码的历史包袱。挑几个典型问题和排查思路记录下来给大家参考。6.1 Java 17 升级中的几个坑升级Spring Boot到3.x对应Java 17时最常遇到的是第三方库不兼容问题。比如老版本用到的一些加密库和对象映射库在Java 17的强封装模块化下会直接抛IllegalAccessError。排查方法是启动时加--add-opens参数但治标不治本真正办法是换成社区活跃的新版本库。尤其是反射操作比较多的工具建议优先看是否有JPMS兼容版本。另外Java 8时代常用的Base64.getEncoder()换成java.util.Base64没问题但一些老代码用到了com.sun.net.ssl.internal.ssl.Provider这种内部类在Java 17里已经移除要改走公开API才能过编译。还有一个容易踩的坑是JAXBJava 11开始就从JDK里移除了如果老代码还在用javax.xml.bind包需要额外引入依赖。6.2 Vue3 迁移的典型问题前端迁移中最容易出问题的是事件总线和过滤器。Vue3移除了$on、$off、$emit的全局事件系统老项目里用于跨组件通信的EventBus直接失效。我用了mitt这个轻量库替代API差不多但要注意在组件销毁时把监听事件解绑否则会造成内存泄漏。Vue2的filters在Vue3里彻底移除。以前用{{ number | format }}的地方现在要改成方法或计算属性。如果你的项目比较大可以用全局方法或者Intl.NumberFormat封装一个统一的格式化函数。自定义指令的钩子名称变化也要特别注意。除了inserted变mounted指令的binding.value在Vue3中默认是响应式的如果希望保持非响应式的静态传值需要加一个static标识。我遇到过权限按钮不显示的案例排查半天才发现是钩子没改对。6.3 线上问题速查表现象可能原因解决方案密钥校验偶尔失败网关时间与服务器时间误差过大统一用NTP密钥校验放宽时间窗口或改用单调时钟前端刷新后空白动态路由丢失应用启动前等待用户信息再挂载路由日志表格卡顿渲染上万行DOM改虚拟滚动开启分页配额扣减重复本地事务与消息不一致改为流水表加对账任务压测时数据库连接池耗尽审计日志同步写入审计日志异步批量写RabbitMQ密钥明文显示两次日志和响应中同时打印完整密钥统一脱敏过滤器禁止日志输出完整key这条速查表目前还在不断完善每次线上出问题我都会回填一条它已经成了团队排查问题时的第一入口。重构收官后在团队里做了一次复盘我最深的体会是技术栈升级只是表象真正难的是把旧的业务模型理清再在新技术上重建得更顺手。Java 17和Vue3带给我们的是更高效的工具但安全体系和交易模型的打磨靠的还是对业务细节的较真。如果让我再重构一遍灵钥系统我会更早引入契约测试让前后端在联调阶段就少吵几架。最后分享一个小经验上线前多做几轮破坏性测试把密钥存储、轮换、配额扣减这些核心链路压到极限远比写一百个单元测试管用。希望你也能在自己的项目里找到这种慢慢变踏实的感觉。