Security Architecture
ODTS-47: 安全架构——金库的锁和钥匙
目标读者:想知道系统如何认证、授权、保护数据的 BA/PM
数据来源:各项目的 SecurityConfig、Shiro 配置、Nginx 配置、application.yml 密钥配置
一句话结论:系统的安全架构从 Shiro 会话时代演进到了 JWT 无状态认证,但开发环境密钥(RSA 私钥、数据库密码、JWT secret)均提交在 Git 仓库中,存在明显安全隐忧。
认证演化:三套系统的三种登录方式
edsWeb (odts1) ofareg (odyssey) unilink (新前端)
┌─────────────────┐ ┌────────────────────┐ ┌───────────────────┐
│ Shiro + Session │ │ Spring Security │ │ SSO + JWT │
│ │ │ + JWT │ │ │
│ 登录: │ │ 登录: │ │ 登录: │
│ MD5哈希密码 │ │ BCrypt验证密码 │ │ ssoLogin() 路由守卫│
│ + Redis验证码 │ │ + RSA加密传输 │ │ 自动跳转SSO页面 │
│ + unionLogin │ │ + CICC ESC OAuth2 │ │ 失败回退本地登录 │
└─────────────────┘ └────────────────────┘ └───────────────────┘
edsWeb:Shiro + Session + unionLogin
edsWeb 使用的是 Apache Shiro,一种基于 Session 的认证框架。登录过程:
用户输入用户名密码
│
▼
前端对密码做 MD5 哈希(盐值 "imsssalt",硬编码在 Constant.java)
│
▼
AES-128/ECB 加密整个请求体(密钥 "webBookingAs@AES",硬编码在 WebAESUtil.java)
│
▼
后端调用 unionLogin 统一认证服务
│
▼
unionLogin 验证通过后,Shiro 创建 Session
│
▼
session.setAttribute("currentUser", currentUser)
两处硬编码密钥意味着:如果代码仓库被访问,加密就形同虚设。
ofareg:Spring Security + JWT + RSA
较新的 ofareg 系统使用了更现代的安全栈:
用户在登录页输入密码
│
▼
前端用 RSA 公钥加密密码(公钥在 publicKey.pem)
│
▼
后端用 RSA 私钥解密(私钥在 privateKey.pem)
│
▼
BCryptPasswordEncoder.matches() 验证密码
│
▼
Redis 验证码校验
│
▼
JWT token 返回前端(HS512 签名,7 天过期)
验证码(Captcha)存储在 Redis,JWT secret 在 application.yml 中。
SSO:三套单点登录
| SSO 系统 | 使用者 | 方式 | 端点 |
|---|---|---|---|
| CICC ESC | ofareg 申报系统 | OAuth2 Authorization Code | esc-sso/oauth2.0/authorize |
| Odyssey Fuxi Auth | edsWeb / ODTS | Token cookie | odyssey-auth-login.*.cicc.io |
| UnionLogin | edsWeb(旧) | 自定义认证 | 直接 JDBC 查 Oracle |
数字证书与电子签章 / Digital Certificates & Stamps
ODTS 使用 Cert App(数字证书服务)处理合同签署的数字证书和电子签章。这是一个独立部署的 HTTP REST 服务,统一管理用户证书的申请、签署、验证生命周期。
Cert App 提供的安全能力
| 能力 | 说明 | 实现 |
|---|---|---|
| 短信验证 | 签署前验证操作人手机号 | GET /cert/sendSMS (usage, mobile, companyId) |
| 待签名数据获取 | 取回合同原文的摘要数据 | GET /cert/getDataToSign (contractId, companyId) |
| 数字签名验证 | 验证签名结果是否有效 | 回调验证 signedData |
| 用户证书管理 | 证书申请、导入、删除 | POST /userCertManager/importUserInfo 等 |
| 合同签署记录 | 签署历史查询 | POST /userCertManager/searchContractInfo |
签署流程的安全链条
合同文档 Cert App 前端(浏览器)
│ │ │
│ ① GET /cert/sendSMS │ │
│ (usage, mobile) │ │
│◄──────── SMS验证码 ──────┤ │
│ │ │
│ ② GET /cert/getDataToSign │
│ (contractId, companyId) │ │
│◄──── DataToSign JSON ────┤ │
│ │ │
│ DataToSign { │
│ signatureId — 签署标识 │
│ dataToSign — 待签名数据(base64) │
│ certSn — 证书序列号(指定用哪张证书) │
│ digestAlg — 摘要算法(如 SHA-256) │
│ signingTime — 签名时间戳 │
│ } │ │
│ │ │
│ ③ 前端调用操作系统证书私钥签名 dataToSign │
│ │ │
│ ④ POST 回 signedData │
│ │ │
│ ⑤ Cert App 用 certSn 对应的公钥验证签名 │
│ │ │
│ ⑥ 签署完成,合同存档至 HCP │
安全要点:
- 私钥不出服务器 — 签名操作在客户端(用户浏览器)完成,Cert App 不持有私钥。
dataToSign是待签名的摘要,拿到它也无法伪造签名 - 短信验证码 — 每个签署操作前强制 SMS 验证,防止未授权操作
- 证书序列号 —
certSn字段指定签署所用的数字证书,系统确保使用正确的证书(公司证书 vs 个人证书) - 签名时间戳 —
signingTime字段记录签名时间,用于后续审计和抗抵赖
证书管理 API
| 端点 | 方法 | 用途 |
|---|---|---|
/userCertManager/getOneUserInfo | GET | 查询单条证书申请记录 |
/userCertManager/searchUserInfo | POST | 分页搜索证书申请 |
/userCertManager/searchContractInfo | POST | 分页搜索签署合同记录 |
/userCertManager/deleteOneUserInfo | POST | 删除证书申请 |
/userCertManager/importUserInfo | POST | 导入证书申请(含附件上传) |
所有请求均携带 userId(来自当前登录会话)确保操作归属可审计。
Certificate vs Authentication 的关系
| 项目 | 登录认证 (Fuxi / unionLogin) | 数字签名 (Cert App) |
|---|---|---|
| 目的 | 验证”你是谁” | 验证”你同意这份合同” |
| 技术 | 密码/JWT/Session | 非对称密钥 + 数字证书 |
| 频次 | 每次会话一次 | 每次签署一次 |
| 法律效力 | 无 | 有(《电子签名法》) |
| 过期 | Session 过期/登出 | 证书有效期(通常 1-5 年) |
授权:菜单权限 vs URL 权限
edsWeb:菜单级权限
edsWeb 的权限系统基于菜单资源字符串:
// 注解方式做权限检查
@RequiresPermissions("riskList")
public String riskList() { ... }
@RequiresPermissions("commonQuery")
public String query() { ... }
- 权限数据来源于数据库的角色-权限对应表
- Shiro 缓存到
Hashtable<String, SimpleAuthorizationInfo>——应用内存中,不是分布式缓存 - 没有颗粒度到 API 层面的权限控制
ofareg:URL 级动态权限
ofareg 的权限系统更灵活——URL 模式到角色的映射从数据库动态加载:
@Bean
public DynamicSecurityMetadataSource dynamicSecurityMetadataSource() {
// 从 UmsMenu 表加载: URL 模式 → 所需角色
return new DynamicSecurityMetadataSource();
}
也可以使用 Spring Security 注解:
@PreAuthorize("hasAuthority('pms:product:read')")
public List<Product> list() { ... }
前端权限
微前端框架中每个子应用都有一个 permission-guard.js:
// 路由守卫 — 每次导航检查 token
router.beforeEach(async (to, from, next) => {
if (!userStore.token) {
await ssoLogin() // 自动跳转 SSO
}
if (需要权限 && !已加载) {
await permissionStore.loadMenus() // 从后端加载菜单
}
})
前端还通过自定义指令控制 UI 级别的权限:
<button v-permission="'trade:create'">新建交易</button>
API 安全
CORS
// ofareg GlobalCorsConfig.java
@Bean
public CorsFilter corsFilter() {
config.addAllowedOriginPattern("*"); // 允许所有来源
config.addAllowedHeader("*"); // 允许所有请求头
config.addAllowedMethod("*"); // 允许所有 HTTP 方法
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
允许所有来源的 CORS 配置——在微前端架构中常见(因为多个子应用域名不同),但带来的安全后果是:任何外部网站都可以跨域调用这些 API。
CSRF
两个系统都显式禁用了 CSRF 防护。ofareg:
.csrf().disable() // SecurityConfig 第 63 行
安全响应头
| 项目 | HSTS | X-Frame-Options | X-Content-Type-Options | CSP |
|---|---|---|---|---|
| odyssey-main-web Nginx | ✅ | DENY | nosniff | ❌ |
| ofareg Nginx | ❌ | ❌ | ❌ | ❌ |
| agquant.com Nginx | ✅(通过 HTTPS) | ❌ | ❌ | ❌ |
限流
无。任何系统、任何 API 都没有发现限流配置。
密钥管理
已提交到 Git 的密钥(严重)
| 文件 | 密钥 | 风险 |
|---|---|---|
ofareg/.../config/privateKey.pem | RSA 私钥 | 攻击者可伪造登录请求解密密码 |
ofareg/.../application.yml | JWT secret: submission-secret | 太弱,可暴力破解后伪造任意 token |
unionLogin/config/db.properties | 数据库密码: 0oORM0Di | 数据库直接被访问 |
edsWeb/Constant.java | MD5 盐值: "imsssalt" | 密码哈希可被彩虹表破解 |
edsWeb/WebAESUtil.java | AES 密钥: "webBookingAs@AES" | 传输加密形同虚设 |
ofareg/.../application.yml | Jasypt 主密码: tongyu | 加密配置等于没加密 |
关键背景:这些仓库是内部 GitLab(git.ciccgroup.com.cn),并非公开的 GitHub。风险在于内部人员的权限过大而非外部攻击。但内部 GitLab 本身是否有足够的安全防护——这超出了代码本身能回答的范围。
这些密钥一旦泄露,业务代价是什么?
密钥不是”技术问题”,它直接等于钱和合规处罚:
1. 数据库密码 `0oORM0Di` 写在 `unionLogin/config/db.properties`
→ 任何能访问这个内部仓库的人,都能直连生产数据库
→ 可读走全部客户交易、持仓、保证金数据
→ 在 GDPR / 个人信息保护法 / 金融监管口径下,
这属于"未授权访问客户数据",可能触发监管报送 + 罚款
2. JWT secret `submission-secret` 太弱
→ 攻击者可本地暴力破解,伪造任意用户的登录 token
→ 等于绕过了整条认证链路,直接以"某交易员"身份发指令
→ 一笔伪造的交易指令如果进入簿记,后果是真实的资金损失
3. AES 密钥 `webBookingAs@AES` 写在 `WebAESUtil.java`
→ 传输层加密被逆向,报文可被解密/篡改
→ 客户门户到后端的通信不再可信
对 BA/PM 的启示:
- 安全审计里”已提交的密钥”是一个必须清零的红线项,不是”改天再说”。
- 这些密钥现在(2022+)已迁到 K8s Secret(见下节),但历史提交里仍然能翻到明文——Git 历史不会自动抹掉。真正干净的修复是:轮换所有暴露过的密钥 + 从历史里 purge。
- 内部 GitLab 不等于”安全”。权限过大的内部账号本身就是威胁面(insider risk)。
环境变量管理的密钥
生产环境的敏感配置(数据库密码、SSO 密钥)通过 Kubernetes Secret 和环境变量注入,不在代码仓库中:
# 通过 K8s 环境变量注入
env:
- name: TRADE_DB_PASSWORD
valueFrom:
secretKeyRef:
name: trade-secret
key: db-password
SSL/TLS
用户侧的 HTTPS
- agquant.com:Let’s Encrypt 证书,标准 HTTPS
- K8s 内部服务:端口 8080(HTTP),TLS 在负载均衡器/Ingress 层处理
- ofareg Nginx:仅端口 80(HTTP),TLS 由外部处理
后端内部的 SSL
- AccessApp:使用 JKS 证书在端口 51177 启用 SSL
- Oracle/PostgreSQL 连接:未加密(JDBC URL 中没有
ssl=true) - ActiveMQ:纯 TCP 连接,未加密
数据库连接加密
所有数据库连接配置中都没有 SSL 参数:
# oracle 连接
jdbc:oracle:thin:@192.168.163.195:1521/oracle # 无 SSL
# postgresql 连接
jdbc:postgresql://localhost:5432/trade_dev # 无 SSL
密码存储
| 系统 | 密码哈希算法 | 评级 |
|---|---|---|
| edsWeb(旧) | MD5 + 静态盐值 | ❌ 弱——可被彩虹表破解 |
| ofareg(新) | BCrypt | ✅ 强——带自适应盐值 |
| ofareg(传输) | RSA 加密 | ✅ 安全 |
edsWeb 的 MD5 哈希方式:
// Constant.java
public static final String IMSSSALT = "imsssalt";
// 登录时
Md5Hash password = new Md5Hash(inputPassword, "imsssalt");
userService.userUnionLogin(username, password.toString());
安全基线的演进
从 odts1 到 odyssey 再到 research,安全基线的改善是明显的:
| 维度 | odts1(2015) | odyssey ofareg(2022) | research(2025) |
|---|---|---|---|
| 密码存储 | MD5 + 静态盐 | BCrypt | BCrypt |
| Token | Session Cookie | JWT(HS512) | JWT(HS256) |
| 密码传输 | AES/ECB 硬编码 | RSA 非对称加密 | HTTPS |
| 授权 | 粗粒度菜单权限 | URL 级动态 RBAC | 接口级注解 |
| CSRF | 未防护 | 显式禁用 | 显式禁用 |
| 密钥管理 | Git 中明文字段 | Git 中明文密钥 | 环境变量 |
| HTTPS | 外部处理 | 外部处理 | Let’s Encrypt |
演进是明显的,但有些问题贯穿始终:
- CSRF 在所有系统中都未被防护
- 没有限流机制
- 没有 API 网关层面的安全统一管控
- 密码连接几乎是普遍现象
数据目录
edsWeb 安全配置:
odts1/edsWeb/WebContent/WEB-INF/spring/applicationContext.xml ← Shiro XML 配置
odts1/edsWeb/src/com/cicc/eds/security/AuthorizationRealm.java ← Shiro Realm
odts1/edsWeb/src/com/cicc/eds/model/Constant.java ← MD5 salt
odts1/edsWeb/src/com/cicc/eds/util/WebAESUtil.java ← AES key
ofareg 安全配置:
odyssey/ofareg/security/src/main/java/.../SecurityConfig.java ← Spring Security
odyssey/ofareg/security/src/main/java/.../JwtTokenUtil.java ← JWT
odyssey/ofareg/data-api/src/main/resources/config/ ← RSA 密钥对
odyssey/ofareg/data-api/src/main/resources/application.yml ← JWT secret + Jasypt
unionLogin:
odts1/unionLogin/config/db.properties ← 数据库密码
odts1/unionLogin/config/mail.properties ← 邮件密码
Nginx 安全头:
odyssey/odyssey-main-web/ci/nginx.conf ← HSTS + X-Frame-Options
odyssey/ofareg/doc/docker/nginx/conf/nginx.conf ← 无安全头
CORS:
odyssey/ofareg/data-api/src/main/java/.../GlobalCorsConfig.java ← 允许所有来源
前端路由守卫:
odyssey/unilink-parent-web/packages/console-web/src/permission/ ← 各子应用的权限守卫
SSL 证书:
odts1/accessapp-new/accessapp/config/keystore.jks ← JKS 密钥库
research/docs/deploy/nginx-agquant.com.2026-07-08.conf ← Let's Encrypt