用户需求
为海南师范高考录取查询接口增加 RSA 非对称验签机制,防止第三方脚本乱刷。调用方为另一个 PHP 服务,使用 RSA 私钥对请求参数签名,Java 后端使用配置的公钥验签,验签不通过则直接拒绝。
产品概述
在现有 HainnuController.query 接口上增加一层服务端到服务端的请求合法性校验。只有持有对应 RSA 私钥的 PHP 调用方才能生成后端可验签通过的请求,从而拦截无密钥的第三方盲刷请求。
核心功能
- DTO 新增
sign字段,由调用方随请求体传入 Base64 编码的 RSA 签名值 - 后端从配置加载 RSA 公钥(X.509 / Base64 格式),使用
SHA256withRSA算法验签 - 验签内容固定为
ksh与sfzh按约定顺序拼接的字符串(如ksh=xxx&sfzh=yyy) - 验签失败直接返回
AjaxResult.error,不执行后续 CSRF 获取、验证码识别与录取查询逻辑 - 提供 RSA 密钥对生成说明,公钥写入 Java 侧配置,私钥交付 PHP 调用方
技术栈
- 运行环境:Java 8 + Spring Boot 2.7.18
- 加密实现:JDK 标准库
java.security(Signature、KeyFactory、X509EncodedKeySpec),无需新增 Maven 依赖 - 签名算法:
SHA256withRSA,密钥长度 RSA 2048,便于与 PHP OpenSSL 互通 - 公钥格式:X.509 编码的 Base64 字符串,配置于
application.properties,避免硬编码
实现方案
总体策略
在 HainnuController.query 方法开头插入一段验签逻辑:从 DTO 取出 ksh、sfzh、sign,按固定规则拼接原文,使用注入的公钥做 SHA256withRSA 验签。验签通过才继续后续 OCR 与查询流程;失败立即返回错误。新增一个 RsaVerifyUtil 工具类封装公钥加载与验签,新增一个 RsaPublicKeyConfig 配置类(或直接在现有 config 目录下新增)读取配置公钥并暴露 PublicKey Bean。改动局部、聚焦,不引入拦截器或全局改动。
关键技术决策
- 仅验签不加密:RSA 验签开销低(一次签名校验约毫秒级),适合作为轻量鉴权;不直接加密业务数据,避免 RSA 长度限制与性能损耗。
- 复用 JDK 标准库 :项目 pom 无 crypto 依赖,
java.security已满足需求,避免引入 BouncyCastle 等额外依赖,降低复杂度与体积。 - 拼接原文规则固定 :约定
ksh=值&sfzh=值(URL 风格)作为待签原文,PHP 侧按相同规则构造,避免字段顺序歧义导致验签失败。 - 公钥配置化 :公钥放
application.properties,便于后续轮换密钥或环境隔离,不写死在代码里。
性能与可靠性
- 验签在方法最前置执行,失败即返回,避免无谓的 OCR(腾讯云 OCR 有调用成本)与下游 HTTP 请求,直接拦截非法流量。
- 单次 RSA 验签时间复杂度 O(1)(固定密钥规模),无性能瓶颈;公钥在应用启动时加载为
PublicKey对象缓存,避免每次请求重复解析。 - 验签异常(如
sign为空、Base64 非法、算法异常)统一捕获并返回错误,不让异常穿透到全局异常处理造成敏感信息泄露。
实现注意事项
- 复用现有
AjaxResult.error返回结构,保持接口响应格式一致。 sign字段加@NotBlank校验,缺失时由 validation 直接拦截。- 拼接原文顺序、分隔符需与 PHP 调用方严格一致,建议在代码注释与交付文档中说明。
- 公钥字符串换行与 PEM 头尾需正确处理(配置中存纯 Base64,去掉
-----BEGIN/END-----头尾)。 - 不改动现有
ksh/sfzh校验逻辑与 OCR/查询流程,仅前置拦截。
架构设计
组件关系
#mermaid-svg-2CCREeaTpncaGjaK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2CCREeaTpncaGjaK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2CCREeaTpncaGjaK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2CCREeaTpncaGjaK .error-icon{fill:#552222;}#mermaid-svg-2CCREeaTpncaGjaK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2CCREeaTpncaGjaK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2CCREeaTpncaGjaK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2CCREeaTpncaGjaK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2CCREeaTpncaGjaK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2CCREeaTpncaGjaK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2CCREeaTpncaGjaK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2CCREeaTpncaGjaK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2CCREeaTpncaGjaK .marker.cross{stroke:#333333;}#mermaid-svg-2CCREeaTpncaGjaK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2CCREeaTpncaGjaK p{margin:0;}#mermaid-svg-2CCREeaTpncaGjaK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2CCREeaTpncaGjaK .cluster-label text{fill:#333;}#mermaid-svg-2CCREeaTpncaGjaK .cluster-label span{color:#333;}#mermaid-svg-2CCREeaTpncaGjaK .cluster-label span p{background-color:transparent;}#mermaid-svg-2CCREeaTpncaGjaK .label text,#mermaid-svg-2CCREeaTpncaGjaK span{fill:#333;color:#333;}#mermaid-svg-2CCREeaTpncaGjaK .node rect,#mermaid-svg-2CCREeaTpncaGjaK .node circle,#mermaid-svg-2CCREeaTpncaGjaK .node ellipse,#mermaid-svg-2CCREeaTpncaGjaK .node polygon,#mermaid-svg-2CCREeaTpncaGjaK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2CCREeaTpncaGjaK .rough-node .label text,#mermaid-svg-2CCREeaTpncaGjaK .node .label text,#mermaid-svg-2CCREeaTpncaGjaK .image-shape .label,#mermaid-svg-2CCREeaTpncaGjaK .icon-shape .label{text-anchor:middle;}#mermaid-svg-2CCREeaTpncaGjaK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2CCREeaTpncaGjaK .rough-node .label,#mermaid-svg-2CCREeaTpncaGjaK .node .label,#mermaid-svg-2CCREeaTpncaGjaK .image-shape .label,#mermaid-svg-2CCREeaTpncaGjaK .icon-shape .label{text-align:center;}#mermaid-svg-2CCREeaTpncaGjaK .node.clickable{cursor:pointer;}#mermaid-svg-2CCREeaTpncaGjaK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2CCREeaTpncaGjaK .arrowheadPath{fill:#333333;}#mermaid-svg-2CCREeaTpncaGjaK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2CCREeaTpncaGjaK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2CCREeaTpncaGjaK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2CCREeaTpncaGjaK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2CCREeaTpncaGjaK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2CCREeaTpncaGjaK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2CCREeaTpncaGjaK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2CCREeaTpncaGjaK .cluster text{fill:#333;}#mermaid-svg-2CCREeaTpncaGjaK .cluster span{color:#333;}#mermaid-svg-2CCREeaTpncaGjaK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2CCREeaTpncaGjaK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2CCREeaTpncaGjaK rect.text{fill:none;stroke-width:0;}#mermaid-svg-2CCREeaTpncaGjaK .icon-shape,#mermaid-svg-2CCREeaTpncaGjaK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2CCREeaTpncaGjaK .icon-shape p,#mermaid-svg-2CCREeaTpncaGjaK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2CCREeaTpncaGjaK .icon-shape .label rect,#mermaid-svg-2CCREeaTpncaGjaK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2CCREeaTpncaGjaK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2CCREeaTpncaGjaK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2CCREeaTpncaGjaK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ksh,sfzh,sign
验签通过
验签失败
PHP 调用方
HainnuController.query
RsaVerifyUtil.verify
RsaPublicKeyConfig PublicKey Bean
原有 OCR + 查询流程
AjaxResult.error 直接返回
架构无新增分层,仅新增工具类与配置类,控制器内部调用,符合现有轻量结构。
目录结构
src/main/java/com/yunlei/
├── config/
│ └── RsaPublicKeyConfig.java # [NEW] 读取 application.properties 中的 RSA 公钥 Base64,启动时解析为 PublicKey Bean 并缓存。提供 getPublicKey() 供验签使用。
├── utils/
│ └── RsaVerifyUtil.java # [NEW] RSA 验签工具。封装 SHA256withRSA 验签方法 verify(plainText, base64Sign, publicKey),处理 Base64 解码与异常归一化。
├── domain/dto/
│ └── HainnuGaoKaoAdmissionQueryDto.java # [MODIFY] 新增 @NotBlank String sign 字段,作为调用方传入的 Base64 签名值。
├── controller/
│ └── HainnuController.java # [MODIFY] 在 query 方法开头注入 RsaVerifyUtil 与 PublicKey,按约定拼接 ksh/sfzh 并验签;失败返回 AjaxResult.error,通过才继续原有逻辑。
└── resources/
└── application.properties # [MODIFY] 新增 rsa.public-key= 配置项,存放 X.509 Base64 公钥(去头尾)。
关键代码结构
java
// RsaVerifyUtil.java 核心接口签名
public class RsaVerifyUtil {
public static boolean verify(String plainText, String base64Signature, PublicKey publicKey) throws SignatureException;
}
// RsaPublicKeyConfig.java 核心接口签名
@Configuration
public class RsaPublicKeyConfig {
@Bean
public PublicKey rsaPublicKey(@Value("${rsa.public-key}") String base64Key) throws GeneralSecurityException;
}