主题
管理后台(captcha-admin)
8082 端口的 WAR 单页后台:Spring Security 做认证 + URL 级按钮权限,
SafeCheckInterceptor补无操作退出与密码到期,FreeMarker.ftl出页面,所有写操作直接落到nc_*/sys_*表并即时影响captcha-server的判定。
1. 运行形态与请求入口
captcha-admin 是标准 Spring MVC WAR,无独立端口配置(容器 8082)。决定"哪些 URL 会进 Spring"的是 captcha-admin/src/main/webapp/WEB-INF/web.xml:75-90:DispatcherServlet 只映射 *.html、*.json、/sso/*、/health/* 四类模式,/css/**、/js/**、/images/** 交给容器 DefaultServlet 直出。因此后台所有接口自然以 .json 结尾、所有页面以 .html 结尾,这是全仓最稳定的 URL 约定。
| 关注点 | 配置 | 位置 |
|---|---|---|
| Servlet 映射 | *.html、*.json、/sso/*、/health/* | captcha-admin/src/main/webapp/WEB-INF/web.xml:75-90 |
| 会话 | session-timeout=3600(秒),tracking-mode=COOKIE | captcha-admin/src/main/webapp/WEB-INF/web.xml:92-95 |
| Filter 顺序 | 编码 → 特殊字符 → 前缀重定向 → Security | captcha-admin/src/main/webapp/WEB-INF/web.xml:10-55 |
| Mvc 拦截器 | IpBlackInterceptor、SafeCheckInterceptor | captcha-admin/src/main/resources/springmvc.xml:30-46 |
| 上传上限 | maxUploadSize=104857600(100 MB) | captcha-admin/src/main/resources/springmvc.xml:79-81 |
/sso/* 的映射带一条特殊语义:注释明确请求会剥离 /sso 前缀再匹配后端 Controller(captcha-admin/src/main/webapp/WEB-INF/web.xml:86-89),改 SSO 相关路径时容易踩。
2. 安全体系详解
2.1 认证 Provider
http 元素 auto-config="false"、use-expressions="true",入口点由 AuthenticationProcessingFilterEntryPoint 构造参数写死 /login.html(captcha-admin/src/main/resources/spring-security.xml:10、:227-230)。认证管理器只挂一个 Provider:
xml
<authentication-manager>
<authentication-provider ref="passportAuthenticationProvider"/>
</authentication-manager>
<b:bean id="passportAuthenticationProvider"
class="com.netease.is.nc.admin.security.PassportAuthenticationProvider">
<b:property name="userDetailsService" ref="sysUserDetails"/>
<b:property name="sysConfLoader" ref="sysConfLoader"/>
<b:property name="accountOperateInfoService" ref="accountOperateInfoService"/>
</b:bean>(captcha-admin/src/main/resources/spring-security.xml:232-250)
三个注入点各管一件事:sysUserDetails(CensorAccountDetailsServiceImpl)从 sys_user / sys_role 装载账号与 authority;sysConfLoader 提供密码策略与失败锁定参数(admin.security.safe.config 单键 JSON);accountOperateInfoService 记录最近操作时间与最近改密时间,供失败计数与到期判定使用。
登录页装配:
| 配置项 | 值 | 位置 |
|---|---|---|
login-page | /login.html | captcha-admin/src/main/resources/spring-security.xml:215 |
| 成功后 | NAAuthenticationSuccessHandler,强制跳默认目标 | :216、:236-238 |
| 失败后 | NAAuthenticationFailureHandler,失败计数 + 回登录页 | :217、:240-243 |
| 认证详情 | CustomAuthenticationDetailsSource(携带后台验证码) | :218 |
| 登出 | /logout,invalidate-session="true",成功回 /login.html | :219-220 |
后台登录验证码不在 Security 内部生成,而是走 CustomCaptchaAuthenticationDetails + admin.security.captcha 包(NECaptchaVerifier),即"人机校验在认证详情里完成",因此登录页必须能拿到验证码接口。
2.2 授权:URL 级即按钮级
后台的"按钮级权限"不是前端概念,而是每个 JSON 接口在 spring-security.xml 里声明所需 authority(captcha-admin/src/main/resources/spring-security.xml:13 注释即写明)。规则顺序严格由前到后匹配,具体 URL 必须写在宽泛规则之前(同文件 :209-212 注释)。
authority 命名遵循 模块_对象_动作:CAPTCHA_MANAGE_VIEW/CREATE/EDIT/DELETE/ONLINE/OFFLINE、WHITELIST_USER_IP_*、STRATEGY_FREQUENCY_*、APPROVAL_*、ADMIN_ACCOUNT_*、FRONTEND_VERSION_*、IMAGE_MANAGE_*、MODEL_*。典型声明:
xml
<intercept-url pattern="/app/paginate.json" access="hasAuthority('CAPTCHA_MANAGE_VIEW')"/>
<intercept-url pattern="/app/save.json"
access="hasAnyAuthority('CAPTCHA_MANAGE_CREATE','CAPTCHA_MANAGE_EDIT')"/>
<intercept-url pattern="/app/online.json" access="hasAuthority('CAPTCHA_MANAGE_ONLINE')"/>
<intercept-url pattern="/app/offline.json" access="hasAuthority('CAPTCHA_MANAGE_OFFLINE')"/>(captcha-admin/src/main/resources/spring-security.xml:80-85)
几个刻意设计:
- 详情页跨模块复用列表接口:
/app/query.json、/sys/managedip/query.json、/freq/query.json都额外授予APPROVAL_*_VIEW_DETAIL,让审批详情页能在不放大CAPTCHA_MANAGE_VIEW的前提下展示单据(:81、:104、:137)。 - 下拉候选接口放开:
/app/queryIdsByName.json、/freq/querySelectOptions.json、/audit/record/startAudit.json等为permitAll(:89、:139、:174-178),只靠"登录后/**=hasAuthority('USER')"兜底。 - 特例覆盖:
/api/**是permitAll(:196),监控告警保存接口必须单独提前声明MONITOR_SYSTEM_SERVICE_WARNING(:182),否则任何人都能改告警策略。
2.3 会话与安全策略
SessionManager(captcha-admin/src/main/resources/spring-security.xml:256-258)与 SafeCheckInterceptor 承担三件事,全部在 MVC 拦截器阶段、Security 之后执行:
| 能力 | 实现 | 返回码 |
|---|---|---|
| 无操作超时退出 | safeConfig.needQuit(info.getLastOperationTime()),超时即 session.invalidate() + 清 SecurityContext | StatusMsg.IDLE_LOGOUT = 1005 |
| 登录态密码到期 | safeConfig.needUpdatePassword(...),不改密则拒绝后续请求 | StatusMsg.NEED_UPDATE_PASSWORD = 1004 |
| 每次成功请求刷新最近操作时间 | accountOperateInfoService.updateOperationTime(account) | — |
(captcha-admin/src/main/java/com/netease/is/nc/admin/interceptor/SafeCheckInterceptor.java:48-77)
拒绝方式按请求类型分流:.json 或 X-Requested-With: XMLHttpRequest 返回 JSON 状态码,页面请求则 sendRedirect(contextPath + "/login.html")(同文件 :105-124)。前端 captcha-admin/src/main/webapp/js/global.js 对 401/403/1004/1005 做全局兜底跳转,两端口径必须一起改。
SafeCheckInterceptor 的排除清单写在 springmvc.xml:40-43:/login、/login.html、/logout、/sys/user/modifyPassword.json,与 spring-security.xml:22 的 permitAll 对应。
安全策略本身不落在 properties,而是 sys_conf 里 admin.security.safe.config 一个 JSON 键(SysConfLoader.SAFE_CONFIG_KEY,captcha-base/src/main/java/com/netease/is/nc/base/sys/conf/SysConfLoader.java:111),读取统一走 SysConfLoader.getSafeConfig()(同文件 :118-129),未配置时返回"不做限制"的默认对象——这解释了"为什么新环境默认不超时"。
2.4 权限实时刷新
Spring Security 默认把 authority 缓存在 Session 里,管理员在用户登录期间改权限会出现"按钮出现了但接口 403"。PermissionRefreshFilter 在 FilterSecurityInterceptor 之前执行,每次请求重新按用户名从 DB 载权限并重建 Authentication(captcha-admin/src/main/resources/spring-security.xml:224;实现见 captcha-admin/src/main/java/com/netease/is/nc/admin/security/PermissionRefreshFilter.java:32-60)。代价是每请求一次 DB 查询,属于有意的性能取舍。
前端按钮显隐由 captcha-admin/src/main/webapp/js/permission-helper.js 完成:登录后拉一次 /sys/role/permission/getUserPermissions.json(:127),提供 hasPermission / hasAnyPermission / hasAllPermissions(:149、:190、:204)。这是纯体验层,后端声明才是强制力。
2.5 已知薄弱点
| 项 | 现状 | 影响 |
|---|---|---|
| CSRF | <csrf disabled="true"/>(spring-security.xml:221,注释"后续有时间再研究") | 后台写接口无 CSRF 令牌,依赖 Cookie + 内网限制 |
/api/** | permitAll(:196) | 新增 /api/ 前缀接口默认无鉴权,必须显式加 intercept-url |
/health/** | permitAll(:187)且 DispatcherServlet 有映射(web.xml:82-85) | 无 handler,必然 404,见第 8 节 |
/sys/user/modifyPassword.json | permitAll(:22) | 自助改密接口自身校验原密码,不能当普通接口改造 |
3. 功能模块清单
| 域 | Controller(类级路径) | 主要页面 | 主表 | authority 前缀 |
|---|---|---|---|---|
| 产品 | ProductController(/product)、ProductRecordController(/product/record) | product/product.html、product/record.html | nc_product、nc_product_record | CAPTCHA_PRODUCT_* |
| 应用/验证码 | AppController(/app) | app/app.html | nc_app | CAPTCHA_MANAGE_* |
| 名单-IP | ManagedIpController(/sys/managedip) | sys/managedip.ftl | nc_managed_ip | WHITELIST_USER_IP_* |
| 名单-用户 | NamelistUserController(/namelist/user) | namelist/user.ftl | nc_namelist_user | WHITELIST_USER_* |
| 名单-指纹 | ManagedFingerPrintController(/namelist/fingerPrint) | namelist/managedFingerPrint | nc_managed_finger_print | WHITELIST_DEVICE_* |
| 高频规则 | FrequencyController(/freq)、FrequencyConditionController(/freq/condition)、FrequencyMetaController(/freq/meta) | freq/freq.html 等 | nc_freq、nc_freq_condition、nc_freq_meta | STRATEGY_FREQUENCY_* / STRATEGY_METADATA_* |
| 审核流 | AuditWorkflowController(/audit/workflow)、AuditRecordController(/audit/record) | audit/*.html | nc_audit_workflow、nc_audit_record | APPROVAL_* |
| 行为日志 | CaptchaInfoController(/captchaInfo) | log/captchaInfo.html | Elasticsearch captcha_info_yyyy-MM-dd | 走 /** 的 USER |
| 统计报表 | RequestStatController(/stat)、RequestStatChartController(/stat/chart)、DeviceStatChartController(/stat/device)、CaptchaStatController(/captcha/stat)、CaptchaIpStatChartController(/captcha/ip/stat) | stat/*.html | nc_request_stat、nc_request_stat_daily、nc_captcha_stat_daily、nc_ip_stat、nc_location_request_stat、nc_sdk_collect | — |
| 底图 | ImageSetController(/image/set)、ImageController(/image) | image/set.html、image/output.html | nc_image_set、nc_image、nc_image_output、nc_icon、nc_lsh_feature | IMAGE_MANAGE_* |
| 模型 | ModelManagerController(/model/manage)、ModelConfController(/model/conf) | model/*.html | nc_model、nc_model_conf | MODEL_* |
| 前端版本 | VersionController(/sys/version)、VersionProductController(/sys/version/product) | version/*.html | nc_version、nc_version_product、nc_browser_versions | FRONTEND_VERSION_* / FRONTEND_GRAY_* |
| 许可 | LicenceController(/licence)、LicenseController(/license) | licence/licence.html | nc_licence、nc_license | 无专属 authority(LICENCE 仅用于删除) |
| 系统管理 | SysUserController、SysRoleController、SysRolePermissionController、SysRoleProductController、SysPermissionController、SysConfController、SafeConfigController、SysOpRecordController | sys/*.html | sys_user、sys_role、sys_role_permission、sys_role_product、sys_permission、sys_conf、sys_op_record | ADMIN_* |
| 监控/仪表盘 | PrometheusMetricsController(/api/prometheus)、DashboardController(/dash) | monitor/*.html、index.ftl | Prometheus / 聚合查询 | MONITOR_SYSTEM_SERVICE_WARNING |
两处易混:CaptchaInfoController 的类级路径是 /captchaInfo,但页面入口是 /log/captchaInfo.html(springmvc.xml:142);LicenceController 与 LicenseController 仅差一个字母,对应两张不同的表与两套校验逻辑(详见 07-license-system.md)。
3.1 审核流机制
审核不改变各业务 Controller 的写法,而是通过注解 + AOP 横切:保存类方法标注 @CheckAudit,AuditAspect.aroundcheckAudit 在调用前拦截并创建/校验审核单,命中"需审批"配置时把写操作转成待办,审批通过后由 AuditRecordController 侧的 dealWithAudit 回调执行业务(captcha-admin/src/main/java/com/netease/is/nc/admin/audit/aspect/AuditAspect.java:127)。这也解释了为什么 spring-security.xml:174-178 里 startAudit / dealWithAudit / rollBack 是 permitAll——它们由业务侧与审批回调调用,鉴权在业务方法内部完成。
4. FreeMarker 模板组织与视图解析
两条解析链,缺一不可:
| 组件 | 配置 | 位置 |
|---|---|---|
freemarkerConfig | templateLoaderPath=/WEB-INF/view/、UTF-8、template_update_delay=0、locale=zh_CN | captcha-admin/src/main/resources/springmvc.xml:48-66 |
viewResolver | FreeMarkerViewResolver,suffix=.ftl,contentType=text/html;charset=UTF-8 | captcha-admin/src/main/resources/springmvc.xml:72-77 |
| 全局模板变量 | options(selectOptionDirective)、monitorConfig、basePath(BasePathDirective) | springmvc.xml:59-69 |
| 静态页面控制器 | 约 50 条 <mvc:view-controller>,URL → 视图名 | springmvc.xml:97-156 |
页面目录约定(captcha-admin/src/main/webapp/WEB-INF/view/):template/base.ftl 是骨架,page.header.ftl / page.nav.top.ftl / page.nav.left.ftl / page.footer.ftl / page.scripts.ftl 是布局片,water-mark.ftl、page.show.detail.ftl 是横切组件;业务页按域分目录(app/、audit/、freq/、image/、log/、model/、namelist/、product/、stat/、sys/、version/、sso/、licence/、monitor/)。
新增一个页面需要同时改三处:WEB-INF/view/ 下放 .ftl、springmvc.xml 加 <mvc:view-controller>、spring-security.xml 给该页调用的每个 .json 接口声明 authority。少第三处就是"页面能开、按钮能点、接口 403"的经典故障。
5. 数据表归属速查
| 表族 | 表 | 写入方 |
|---|---|---|
| 产品/应用 | nc_product、nc_product_record、nc_app | admin |
| 名单 | nc_managed_ip、nc_namelist_user、nc_managed_finger_print | admin(同时写 Redis 名单缓存供 server 判定) |
| 策略 | nc_freq、nc_freq_condition、nc_freq_meta、nc_model、nc_model_conf、nc_click_conf | admin |
| 审核 | nc_audit_workflow、nc_audit_record | admin |
| 素材/版本 | nc_image_set、nc_image、nc_image_output、nc_icon、nc_lsh_feature、nc_version、nc_version_product、nc_browser_versions | admin + schedule |
| 统计 | nc_request_stat、nc_request_stat_daily、nc_captcha_stat_daily、nc_ip_stat、nc_location_request_stat、nc_sdk_collect | schedule / server |
| 许可 | nc_licence、nc_license | admin |
| 系统 | sys_user、sys_role、sys_role_permission、sys_role_product、sys_permission、sys_conf、sys_op_record | admin |
sys_conf 是动态配置源,SysConfLoader 用单线程 scheduleAtFixedRate 每 30 秒全量 reload(captcha-base/src/main/java/com/netease/is/nc/base/sys/conf/SysConfLoader.java:44、:91-99)。因此后台改配置后最多 30 秒生效、且不分模块——server / admin / schedule 里的所有应用实例都按各自节奏轮询。
6. 子路径部署适配
客户环境统一域名 + Kong 按 path 路由,后台没有独立域名与 nginx,前缀随环境变化(集成/UAT/生产三套)。落地方式见 docs/验证码后台路径适配方案-子路径部署与动态前缀.md 与 docs/captcha-admin-前缀适配改动清单.md,要点三条:
- 页面基址不再用
request.contextPath,改为basePath():优先请求头X-Forwarded-Prefix,为空退化contextPath,由AdminPathUtils校验合法性(须/开头、不以/结尾、仅A-Za-z0-9._~-/)。 - 模板只改 3 行:
template/base.ftl、login.ftl、sso/relay.ftl的<#assign ctx=basePath() />,其余${ctx}/<@js>/<@css>/window.__CTX自动继承。 - 302 也要带前缀:
PrefixRedirectFilter在 Security 之前补齐sendRedirect与Location头(captcha-admin/src/main/webapp/WEB-INF/web.xml:39-46),否则登录跳转会丢前缀。
后端部署侧由 start.sh 依据环境变量 CONTEXT_PATH 生成 Tomcat context 描述符;为空则保持 ROOT 部署,不影响其他客户。Kong 侧推荐 strip_path: false(路径透传)+ 后端 contextPath 等于该环境前缀,避免依赖 Kong 版本是否下发 X-Forwarded-Prefix。
7. 探针与健康检查(重要)
/health/* 只有配置、没有 handler,必然 404。 依据两处:spring-security.xml:187 把 /health/** 设为 permitAll,web.xml:82-85 把 /health/online、/health/check、/health/status、/health/offline 映射进 DispatcherServlet,但全仓没有任何 Controller 处理这些路径,Spring MVC 找不到 handler 即返回 404。容器探针必须改用真实可达的页面:
| 模块 | 可用探针 |
|---|---|
captcha-admin | GET /login.html(NormalController,springmvc.xml:85-93) |
captcha-server | GET /metrics(web.xml:51-58 Prometheus MetricsServlet) |
captcha-image | GET / |
captcha-schedule | 无端口,用 exec 探针 |
8. 运维踩坑清单
| 现象 | 根因 | 处置 |
|---|---|---|
| 页面能开、接口 403 | 新接口没在 spring-security.xml 声明 authority | 补 intercept-url,注意写在 /** 之前 |
| 按钮出现了但接口 403 | 修改权限后旧 Session 未刷新 | 已由 PermissionRefreshFilter 覆盖;若仍复现,检查该 Filter 是否被移除 |
| 登录页无样式 / 跳转丢前缀 | ctx 取了空的 contextPath | 走 basePath() + PrefixRedirectFilter |
请求莫名返回 black list ip | IpBlackInterceptor 命中 sys_conf 的 IP 黑名单 | captcha-admin/src/main/java/com/netease/is/nc/admin/interceptor/IpBlackInterceptor.java:27-43 |
| 请求返回"含非法字符" | SpecialCharCheckFilter,仅对 admin.special.char.check.conf 标注的接口生效 | 见 web.xml:24-36 |
| 改完配置要等一会才生效 | SysConfLoader 30s 轮询 | 等 30 秒或重启(重启会重新 reload) |
| 上传底图 413 | maxUploadSize 100 MB 上限 | springmvc.xml:79-81 |