ENGINEERING NOTES · 工程笔记

管理后台的安全加固:从静态密钥到会话+MFA

先说结论:管理后台是多租户 SaaS 里价值密度最高的攻击面——商户数据、订单、退款、会员手机号全在这一侧。我们最早用一把静态 X-Management-Key 保护全部管理接口,上线前安全自查时推翻了这个自己做的设计:静态密钥的原罪不是"不够长",而是没有身份、没有失效、没有审计。本文记录从静态密钥到 Spring Security 会话 + MFA + 权限码 + CSRF 的完整加固过程,配置均为脱敏示意。

一、起点与教训:一把钥匙走天下

项目最初只有三个人碰管理端,接口保护方案简单到一句话:所有 /manage/** 请求带一个静态请求头 X-Management-Key,校验通过即放行。当时觉得够用——钥匙只在几个人手里。

自查那天 grep 了一遍仓库,这把钥匙硬编码在三处:配置文件、部署脚本、一个测试小工具。再往前翻,半年前排查线上问题时,它还被整段贴进过工作群。复盘会上同事一句话点破了要害:"这把钥匙在群里躺了半年,我们连它有没有被人捡走都回答不了。"——知晓范围早已不可枚举,而它一旦泄露就是全线失守:不能按人吊销,不能按接口收缩,换钥匙要三处代码联动发版。

更致命的是三个"没有":

  • 没有身份。 日志里所有操作都长一个样:management。出了问题,无法回答"谁干的"。
  • 没有失效。 钥匙不会过期,泄露了你也不知道——攻击者用它拉数据,和正常巡检在日志里毫无区别。
  • 没有粒度。 客服只需要看订单,拿到的却是和开发一样的全量权限,包括退款和会员手机号导出。

那天我们承认:这不是把钥匙换长一点能修的问题,是身份模型缺席。重构方案定了四层。

二、目标设计:分层身份,各挡各的攻击

手段防什么
认证层Spring Security 会话(HttpOnly Cookie)防密钥泄露即永久失守:凭证可失效、可强制下线
因素层TOTP 双因素(MFA)防弱口令、撞库、钓鱼:口令被盗还有第二道锁
授权层权限码 + 方法级注解防横向扩张:进来了也只能动被授权的那几个接口
请求层CSRF Token防跨站伪造:恶意页面借浏览器自动带 Cookie 冒充管理员

四层不是冗余,是分工。会话解决"凭证可吊销";MFA 解决"口令不代表你本人";权限码解决"账号被盗后损失有上限";CSRF 解决"浏览器会自动携带 Cookie"这个天然缺陷——管理员在登录状态下点开一个恶意页面,页面里一个隐藏表单就能以他的身份发起退款请求,Cookie 是浏览器自动带的,用户全程无感。任何一层缺席,其余三层都要替它兜底,兜不住就是事故。

三、实施要点

1. 登录的加固

三件事。失败锁定:同一账号连续 5 次失败锁定 15 分钟;同一 IP 对不同账号的失败单独计数——前者防针对单人的爆破,后者防撞库扫号。会话超时:空闲 30 分钟、绝对生命周期 12 小时,超时即服务端销毁;同一账号最多一个活跃会话,后登录踢前登录。MFA 绑定与恢复码:首次登录进入"半登录态",只能访问绑定页;扫码绑定 TOTP、验证一次通过后才发正式会话。绑定的同时生成 10 张一次性恢复码,仅在页面明文展示一次,库里只存哈希。绑定页本身也限速,TOTP 验证失败同样计入失败锁定——防止有人对着绑定接口批量撞验证码。

恢复码的保管是被严重低估的设计问题。我们的规矩:恢复码不进共享文档、不进聊天记录,管理员纸质密封保管;补发恢复码或重置他人 MFA,必须另一名管理员审批——否则"重置 MFA"本身就成了整个体系的后门。

2. 接口的收敛

权限收敛靠注解统一。所有管理端接口必须挂 @PreAuthorize,权限码按 域:资源:动作 命名,例如退款是 merchant:order:refund。CI 里加了一步静态扫描:发现没挂注解的管理端接口直接红——37 个管理接口、27 个业务域,靠 review 保证"一个不漏"是不现实的。

敏感操作做服务端二次确认:重置他人 MFA、导出会员手机号、批量退款,要求重新验证口令或 TOTP。确认必须由服务端校验,只在弹窗里问一句"确定吗"属于表演级安全。

3. 审计日志:出事后的黑匣子

审计日志回答三个问题:谁、什么时候、动了什么。记录账号、时间、接口、权限码、来源 IP 与 UA,敏感字段记改前/改后值,表只增不改——"只增不改"不是口头约定:日志表不授予 UPDATE 和 DELETE 权限,归档清理走独立角色单独审批并留痕。它不防攻击,但决定了出事之后 48 小时内能不能给商户一个交代——对 SaaS 来说,这决定了这家商户还留不留。

四、脱敏示意:过滤链怎么配

// 脱敏示意,非真实配置
http.securityMatcher("/manage/**")
    .authorizeHttpRequests(auth -> auth
        .requestMatchers("/manage/login", "/manage/mfa/bind").permitAll()
        .anyRequest().hasRole("ADMIN"))              // 半登录态另用临时角色圈在绑定页
    .sessionManagement(s -> s
        .invalidSessionUrl("/manage/login")
        .maximumSessions(1)                          // 同账号单活跃会话,后来者顶掉先来者
        .sessionFixation().newSession())             // 登录成功必须换新会话 ID
    .csrf(c -> c.csrfTokenRepository(
        CookieCsrfTokenRepository.withHttpOnlyFalse()))
    .logout(l -> l.invalidateHttpSession(true)
        .deleteCookies("JSESSIONID"));               // 登出要服务端销毁,不能只清前端状态

登录失败锁定用认证失败事件监听器累加计数实现,键分"账号"与"IP+账号"两套;恢复码与 MFA 绑定关系各一张独立表,全部走 Flyway 迁移管理(85 个迁移,至 V113),随 1241 个测试方法回归。

五、踩坑清单

  1. MFA 的可用性平衡。 第一版每次登录都要输 TOTP,运营同事一天登录四五次,第二周就有人把 6 位验证码抄在便签上贴到显示器边框。她的原话:"我不是不配合,是一天输八遍谁受得了。"安全设计把人逼向不安全的行为,等于白做。改成"可信设备 7 天免验":绑定设备发一个专用 Cookie,服务端记录设备指纹与到期时间,管理台可一键吊销全部可信设备。可用性与安全性的交点要主动设计,不能等用户替你设计。
  1. CSRF 与小程序端接口分治。 全局启用 CSRF 后,小程序端接口集体 403——小程序请求根本不回传 CSRF Token。根因是两类接口的信任模型不同:管理端 Web 用 Cookie 会话,浏览器会自动带 Cookie,所以要 CSRF Token;小程序端凭据放在请求头,第三方页面无法让浏览器自动携带,本来就不在 CSRF 威胁模型内。解法是 securityMatcher 分治:/manage/ 走会话 + CSRF,/api/ 走无状态令牌认证,两套过滤链互不沾边。最初想用一条链加白名单省事,两周后承认省下的那点事不够填坑。
  1. 二次确认只做前端等于没做。 中期 review 抓到一个导出接口:前端弹窗确认,后端直接放行。绕过页面直接发请求,"二次确认"毫无作用。敏感操作的确认必须是服务端重新校验凭证(口令或 TOTP),前端弹窗只是体验层。
  1. 审计日志自己就是敏感数据。 第一版把改前/改后值原样入库,review 时发现手机号、收款账号的明文躺在日志表里——审计日志成了第二份泄密源。敏感字段入库前脱敏或加密,日志表的查询权限单独授予,不与应用运行角色混用。

三句话总结

  1. 静态密钥的原罪不是长度,而是无身份、无失效、无审计——泄露即失守,且你永远不知道它已经泄露。
  2. 分层身份是分工不是堆料:会话管失效、MFA 挡盗号、权限码限损失、CSRF 防冒用,砍掉任何一层,其余层都要被迫兜底。
  3. 加固的终点不是"进不来",而是"进来了干不了大事、干了也查得到"——最小权限加只增不改的审计日志,比任何单一机制都可靠。

运营主体:北京位元跃迁科技有限公司。本文同步发布于本号技术专栏,可搬运至 CSDN/掘金。

本文为位元跃迁原创内容,仅代表编辑观点,不构成经营或投资建议;文中涉及的品牌与案例仅作公开信息分析示例。

LET'S TALK

把方法用进你的生意

如果你正在为门店做数字化规划,欢迎和我们聊聊。位元跃迁是微信支付合作伙伴、微信开放平台第三方平台,提供小程序定制、开发咨询与企业数字化服务。

18601279913

业务咨询 · 项目合作 · 代理商合作

了解位元跃迁的服务 →
位元跃迁业务联系微信二维码
微信咨询