一个 Java 1.8 + Shiro 的老项目,要给 AI Agent 开放接口。东西不难 —— 一个 Filter、几个接口、一页前端。但这些活实际是在 Claude Code 里一天干完的:Claude Code 先用各种技巧把跑通了,Cursor 评价了一句「架构不对」,于是 Claude Code 就按 Shiro 多 Realm 推倒重来。三个版本、六个坑都挤在一个下午。用两个 AI 互相监督,比押宝在单一对话里要省心得多。
背景
上一篇文章里,我宣布了一个实验:用 CLI + Skills 模式,把 Athena 数据工厂改造成 AI 可操作的平台。1000 块钱预算,100% Vibe Coding,邀请大家云监工。
这篇文章就是云监工系列的第一份进展报告。
Athena 是公司数据工厂平台,Java 1.8 + Spring Boot + Shiro + Vue 2,跑了五年。改造的第一步是给后端加 API Key 认证:用户在前端页面申请 Key,CLI 带着 Key 发请求,后端识别后放行。
东西不复杂。但真的做起来,事情就不一样了。
V1:一个下午写完,一个坑一个坑地填
第一回合:Java 版本不兼容
Key 的生成很简单——athena_ 前缀 + 随机字符串。我们写了:
| StringapiKey="athena_" |
提交,构建,报错。java.util.HexFormat 是 Java 17 才有的,项目是 Java 1.8。
换成 javax.xml.bind.DatatypeConverter.printHexBinary(bytes)。修掉。
第二回合:Filter 根本没被调用
后端全套写完——Entity、Mapper、Service、Controller、Filter。部署,curl 测试:
看日志,Filter 没有任何输出。没被调用。
原因:Filter 用 @Component 自动注册,但项目通过 ShiroFilterFactoryBean 管理 Filter 链,把 @Component 注册的 Filter 盖掉了。
修:在 ShiroConfig 里用 FilterRegistrationBean 显式注册,order=1 保证跑在最前面。
第三回合:依赖注入失效
Filter 跑起来了,但 athenaApiKeyService 和 sysUserService 全是 null。
我们 FilterRegistrationBean 的 Bean 方法里 new ApiKeyAuthFilter() 创建实例,@Autowired 字段不会自动注入——自己 new 的对象没走 Spring 容器。
修:改为 setter 注入,在 @Bean 方法的参数里让 Spring 注入依赖,再手动 set 进去。
第四回合:Key 查到了,用户找到了,但还是 401
日志长这样:
Filter 内部全部成功。但请求继续沿链路走,到了 Shiro 的 OAuth2Filter。OAuth2Filter 发现请求里没有它认识的那个 token 参数,直接返回 401——把我们刚完成的认证给盖了。
两个认证体系在一条链里互不认账。 这是最头疼的坑。
我们当时的修法:API Key 认证成功后,劫持 request 往 Header 里注入一个假的 token 参数,让 OAuth2Filter 能读到。同时写 sys_user_token 表,让 OAuth2Realm 能校验通过。还得判断用户是否已有 web session,避免覆盖已有的登录态。
终于通了。
第五回合:前端的 webpack 缓存
前端部署后报 Cannot find module——文件在正确位置,但 webpack 的动态 require 上下文模块没包含新建的文件。重启 dev server 解决。
V2:同事一句话,推倒重来
ps:此处的同事,就是Cursor;
V1 跑通后,我们觉得任务完成了。但同事看了代码,说了一句话:
"更稳妥的架构是用 Shiro 的多 Realm / 多认证方式,让 API Key 成为一等公民,而不是伪造 OAuth2 的 token 头。"
醍醐灌顶。我们的 V1 方案本质上是个补丁——在 Servlet Filter 层截胡请求,然后骗过后面的 Shiro Filter。功能对了,但架构姿势错了。
Shiro 本身就支持多认证方式。正确的做法是在 Realm 层让 API Key 和 OAuth2 Token 平起平坐,而不是在 Filter 层伪造。
V1 vs V2 架构差异
V1 — 补丁方案:绕过 Shiro,在外部截胡
V2 — 多 Realm 方案:融入 Shiro,让 API Key 成为一等公民
四个新文件,两个改动:
• 新增 ApiKeyToken(Shiro AuthenticationToken)——API Key 作为一等凭证
• 新增 ApiKeyAuthenticatingFilter(Shiro AuthenticatingFilter)——80 行,排在 oauth2 前面
• 修改 OAuth2Realm——supports() 和 doGetAuthenticationInfo() 同时处理两种 Token
• 修改 OAuth2Filter——onAccessDenied 开头加一行:如果 Subject 已认证就直接放行
• 删除 ApiKeyAuthFilter(132 行)+ FilterRegistrationBean
第六回合:又漏了一步
V2 部署,500。日志:
Realm 认证成功了,Filter 也通过了,但 Controller 的 @CurrentInstUser InstUser user 是 null。
OAuth2Filter 的 onLoginSuccess 会把用户信息注入到 request attribute,我们的 ApiKeyAuthenticatingFilter.onLoginSuccess 没写这步。加一行 request.setAttribute(PassportConstants.CURRENT_INST_USER, instUser),解决。
跑通
| # 正确的 Key → 正常返回 |
前端页面:创建 Key(仅一次完整展示)、查看列表(脱敏 Key)、吊销(软删除)。权限严格隔离:只能管理自己的 Key。
回头看
从"加个 Filter"到最终落地,经历了三个版本:
@Component | ||
FilterRegistrationBean | ||
最大的教训不是具体的技术坑,而是一个原则:在已有的认证体系上叠加新方式,不要绕过去,要融进去。
用 Servlet Filter 绕过 Shiro、伪造 token——能做,但它是补丁。用 Shiro 自己的多 Realm 机制、让 API Key 和 OAuth2 平起平坐——这才是正确的扩展方式。
同事那句话本质上是:"别在别人的体系外面搭棚子,到体系里面去,用体系自己的规则玩游戏。"
下期预告
API Key 认证只是第一步。接下来要把 Athena 的 100 多个 REST API 逐条封装成 CLI 子命令——instance list、task rerun、instance kill……这才是体力活。桥搭好了,该铺路了。
下期见。
往期回顾
• 《搞事情:1000 块钱,0 行手写代码,我要把一套数据治理平台 AI 化了》 —— 云监工系列序章
• 《当 SaaS 们都学会了 AI,下一步是什么?》 —— 为什么要做 AI 可操作化
• 《lark-cli:企业 SaaS 走向 AI 化时,值得对照的一份范本》 —— 参考架构怎么设计的
• 《让 AI 操作老系统:只需要 CLI 和 SKILLS》 —— 我的具体方案
不写代码的程序员。
夜雨聆风