Learning
VOL. VII · NO. 96 · OTC Derivatives · 19 JUL 2026

Security Architecture

OTC 衍生品 · 19 JUL 2026 · 12 min read · 1,732 words
· · ·

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 ESCofareg 申报系统OAuth2 Authorization Codeesc-sso/oauth2.0/authorize
Odyssey Fuxi AuthedsWeb / ODTSToken cookieodyssey-auth-login.*.cicc.io
UnionLoginedsWeb(旧)自定义认证直接 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                            │

安全要点:

  1. 私钥不出服务器 — 签名操作在客户端(用户浏览器)完成,Cert App 不持有私钥。dataToSign 是待签名的摘要,拿到它也无法伪造签名
  2. 短信验证码 — 每个签署操作前强制 SMS 验证,防止未授权操作
  3. 证书序列号certSn 字段指定签署所用的数字证书,系统确保使用正确的证书(公司证书 vs 个人证书)
  4. 签名时间戳signingTime 字段记录签名时间,用于后续审计和抗抵赖

证书管理 API

端点方法用途
/userCertManager/getOneUserInfoGET查询单条证书申请记录
/userCertManager/searchUserInfoPOST分页搜索证书申请
/userCertManager/searchContractInfoPOST分页搜索签署合同记录
/userCertManager/deleteOneUserInfoPOST删除证书申请
/userCertManager/importUserInfoPOST导入证书申请(含附件上传)

所有请求均携带 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 行

安全响应头

项目HSTSX-Frame-OptionsX-Content-Type-OptionsCSP
odyssey-main-web NginxDENYnosniff
ofareg Nginx
agquant.com Nginx✅(通过 HTTPS)

限流

。任何系统、任何 API 都没有发现限流配置。


密钥管理

已提交到 Git 的密钥(严重)

文件密钥风险
ofareg/.../config/privateKey.pemRSA 私钥攻击者可伪造登录请求解密密码
ofareg/.../application.ymlJWT secret: submission-secret太弱,可暴力破解后伪造任意 token
unionLogin/config/db.properties数据库密码: 0oORM0Di数据库直接被访问
edsWeb/Constant.javaMD5 盐值: "imsssalt"密码哈希可被彩虹表破解
edsWeb/WebAESUtil.javaAES 密钥: "webBookingAs@AES"传输加密形同虚设
ofareg/.../application.ymlJasypt 主密码: tongyu加密配置等于没加密

关键背景:这些仓库是内部 GitLabgit.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 + 静态盐BCryptBCrypt
TokenSession CookieJWT(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