很多后台项目一谈接口安全,第一反应就是:不是已经上 HTTPS 了吗?还要接口加密干嘛?
这句话只对了一半。
HTTPS 解决的是传输链路加密,但企业项目里经常还有这些场景:
- 登录、密码修改、支付确认等敏感接口,希望请求体本身加密。
- 前端埋点、抓包工具、代理层日志里,不希望直接看到明文参数。
- 政务、金融、运营商项目要求"应用层加密"。
- 内部网关、日志平台、第三方代理可能记录请求响应体。
所以 Forge Admin 的接口安全不是只靠 HTTPS,而是做了一套应用层 API 加解密:
sql
前端获取 RSA 公钥
↓
前端生成 SM4/AES 会话密钥
↓
用 RSA 公钥加密会话密钥并交换
↓
后续请求/响应用会话密钥做对称加解密
↓
可选 nonce + timestamp 防重放
这一期就拆这套源码。
一、为什么不是直接用 RSA 加密所有数据?
很多人一听"加密",第一反应就是 RSA。
但真实工程里,RSA 不适合直接加密大体积请求体:
- RSA 性能比对称加密慢得多。
- RSA 有明文长度限制。
- 大量接口都用 RSA 加密响应体,会拖慢整个系统。
所以更合理的方案是:
- RSA 只用来做密钥交换。
- SM4/AES 用来加密业务数据。
这也是 HTTPS/TLS 的经典思路:非对称加密解决"安全交换密钥",对称加密解决"高效传输数据"。
Forge 也是这么做的。
二、第一步:前端获取 RSA 公钥
入口在 KeyExchangeController:
less
@GetMapping("/public-key")
@SaIgnore
public ResponseEntity<Map<String, Object>> getPublicKey() {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("data", new PublicKeyResponse(keyExchangeService.getPublicKey(), "RSA"));
result.put("msg", "success");
return ResponseEntity.ok(result);
}
接口路径是:
vbnet
GET /crypto/public-key
这个接口有三个细节:
@SaIgnore:获取公钥不需要登录。@ApiPermissionIgnore:跳过 API 权限控制。@IgnoreTenant:密钥交换是平台级能力,不受租户隔离影响。
前端拿到公钥后,生成一个随机的 SM4/AES 会话密钥,然后用 RSA 公钥加密这个会话密钥。
三、第二步:RSA 交换会话密钥
前端把加密后的会话密钥提交到:
bash
POST /crypto/exchange
核心源码:
less
@PostMapping("/exchange")
@SaIgnore
public ResponseEntity<Map<String, Object>> exchangeKey(
@RequestBody KeyExchangeRequest request,
HttpServletRequest httpRequest) {
String sessionId = getSessionId(httpRequest);
if (sessionId == null || sessionId.isEmpty()) {
return ResponseEntity.badRequest().body(result);
}
boolean success = keyExchangeService.exchangeKey(sessionId, request.getEncryptedKey());
// ... 返回结果
}
这里关键是 sessionId。Forge 取会话标识的顺序是:
- 优先取
Authorization: Bearer xxx里的 token。 - 其次取
X-Session-Id。 - 最后用 HTTP Session ID。
也就是说,会话密钥不是全局一把钥匙,而是绑定到当前会话。
真正解密和存储在 KeyExchangeService:
typescript
public boolean exchangeKey(String sessionId, String encryptedKey) {
String sessionKey = rsaKeyPairHolder.decryptByPrivateKey(encryptedKey);
sessionKeyStore.storeKey(sessionId, sessionKey);
return true;
}
RSA 私钥只负责解开"会话密钥",不会直接处理业务请求体。
四、会话密钥放哪?Redis,默认 2 小时
会话密钥如果只存在内存里,多实例部署就麻烦了:A 机器交换密钥,B 机器处理请求,B 拿不到密钥。
Forge 的 SessionKeyStore 用缓存服务存储会话密钥,底层可接 Redis:
typescript
private static final String KEY_PREFIX = "crypto:session:";
public void storeKey(String sessionId, String secretKey) {
String cacheKey = KEY_PREFIX + sessionId;
cacheService.set(cacheKey, secretKey, expireSeconds, TimeUnit.SECONDS);
}
public String getKey(String sessionId) {
String cacheKey = KEY_PREFIX + sessionId;
Object value = cacheService.get(cacheKey);
return value != null ? value.toString() : null;
}
默认过期时间来自配置:
ini
private Long sessionKeyExpire = 7200L; // 2小时
这点很实用:
- 多实例部署没问题。
- 用户登出可以清理密钥。
- 密钥过期后需要重新协商。
- 不需要把会话密钥写进数据库。
五、请求怎么自动解密?RequestBodyAdvice
很多人写接口加密,最后会变成这样:
less
@PostMapping("/login")
public Result login(@RequestBody EncryptedRequest request) {
String json = cryptoService.decrypt(request.getData());
LoginDTO dto = JSON.parseObject(json, LoginDTO.class);
// 业务逻辑...
}
这样写的问题是:每个接口都要手动解密,业务代码被加密逻辑污染。
Forge 用的是 Spring MVC 的 RequestBodyAdvice,在 Controller 参数绑定之前自动解密。
核心类是 DecryptRequestBodyAdvice:
kotlin
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
if (!properties.getEnabled() || !properties.getEnableApiCrypto()) {
return false;
}
if ("true".equalsIgnoreCase(request.getHeader("X-Inner-Call"))) {
return false;
}
ApiConfigInfo apiConfig = apiConfigManager.getApiConfig(request.getRequestURI(), request.getMethod());
if (apiConfig != null) {
return apiConfig.getNeedEncrypt();
}
return hasAnnotation(methodOrClass, ApiDecrypt.class);
}
它判断是否需要解密有两条路:
- 动态 API 配置:后台配置某个接口是否需要加密。
- 注解方式 :Controller 类或方法上标
@ApiDecrypt。
这就比单纯注解更灵活:上线后想把某个接口改成加密,不一定要改代码发布。
真正解密发生在 beforeBodyRead:
ini
EncryptedRequest request = objectMapper.readValue(encryptedBody, EncryptedRequest.class);
Encryptor encryptor = encryptorFactory.getEncryptor(algorithm);
String sessionKey = getSessionKey();
if (sessionKey != null) {
decryptedData = encryptor.decrypt(request.getData(), sessionKey);
} else {
decryptedData = encryptor.decrypt(request.getData());
}
return new DecryptedHttpInputMessage(headers,
new ByteArrayInputStream(decryptedData.getBytes(UTF_8)));
最终 Controller 拿到的仍然是正常 DTO,业务代码完全不知道请求曾经是密文。
六、响应怎么自动加密?ResponseBodyAdvice
请求能自动解密,响应也要自动加密。
Forge 用 EncryptResponseBodyAdvice 在响应写出前处理:
kotlin
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
if (!properties.getEnabled() || !properties.getEnableApiCrypto()) {
return false;
}
if (isBinaryResponseType(returnType)) {
return false;
}
if ("true".equalsIgnoreCase(request.getHeader("X-Inner-Call"))) {
return false;
}
ApiConfigInfo apiConfig = apiConfigManager.getApiConfig(request.getRequestURI(), request.getMethod());
if (apiConfig != null) {
return apiConfig.getNeedEncrypt();
}
return hasAnnotation(methodOrClass, ApiEncrypt.class);
}
几个细节值得看:
- 二进制响应不加密 :比如文件、图片、验证码,避免把
ResponseEntity<byte[]>搞坏。 - 内部调用跳过 :
X-Inner-Call: true的 FlowClient 等内部服务调用,直接明文 JSON。 - 支持动态配置和注解两种方式。
真正加密逻辑:
ini
String jsonBody = objectMapper.writeValueAsString(body);
String sessionKey = getSessionKey(request);
if (sessionKey != null) {
encryptedData = encryptor.encrypt(jsonBody, sessionKey);
} else {
encryptedData = encryptor.encrypt(jsonBody);
}
return new EncryptedResponse(encryptedData, algorithm);
响应格式会变成统一的密文对象,而不是直接返回原始 JSON。
七、算法不是写死的:SM4 / AES 可切换
Forge 的默认算法是 SM4:
ini
private String algorithm = "SM4";
但代码里用的是 EncryptorFactory:
ini
Encryptor encryptor = encryptorFactory.getEncryptor(algorithm);
这意味着算法不是写死在 Advice 里的。默认走 SM4,想换 AES 也可以通过配置或注解指定:
less
@ApiEncrypt(algorithm = "AES")
@ApiDecrypt(algorithm = "AES")
这对不同合规要求的项目很有用:有的项目要求国密 SM4,有的项目只需要 AES。
八、防重放:timestamp + nonce
加密只能保证"看不懂",不能阻止别人把同一个密文请求重复发一遍。
比如支付确认、审批通过、转账提交,如果攻击者截到一次请求后重复发送,即使看不懂内容,也可能造成重复操作。
Forge 提供了 ReplayAttackFilter 做防重放:
ini
String timestamp = httpRequest.getHeader("X-Timestamp");
String nonce = httpRequest.getHeader("X-Nonce");
long requestTime = Long.parseLong(timestamp);
long currentTime = System.currentTimeMillis();
long timeWindow = properties.getReplayTimeWindow() * 1000;
if (Math.abs(currentTime - requestTime) > timeWindow) {
sendError(response, "请求已过期");
return;
}
if (tokenCache.exists(nonce)) {
sendError(response, "重复的请求");
return;
}
tokenCache.cache(nonce, properties.getReplayTimeWindow());
防重放的逻辑很清楚:
- 请求必须带
X-Timestamp和X-Nonce。 - 时间戳必须在允许窗口内,默认 300 秒。
- nonce 只能使用一次,使用后写入缓存。
- 同一个 nonce 再来,直接拒绝。
默认配置里,防重放是关闭的,需要按项目风险打开:
ini
private Boolean enableReplayProtection = false;
private Long replayTimeWindow = 300L;
这点我反而觉得合理:不是所有内部后台都需要防重放,只有支付、审批、资金、状态流转类接口需要强保护。
九、配置开关:不是所有接口都必须加密
Forge 的加密配置集中在 CryptoProperties:
ini
private Boolean enabled = true;
private String algorithm = "SM4";
private String secretKey;
private Boolean enableDynamicKey = true;
private Long sessionKeyExpire = 7200L;
private Boolean enableApiCrypto = true;
private Boolean enableFieldCrypto = true;
private Boolean enableReplayProtection = false;
private Boolean enableDesensitize = true;
几个设计点:
enableDynamicKey:是否启用动态密钥协商。secretKey:动态密钥不可用时的降级密钥。enableApiCrypto:是否启用 API 请求/响应加解密。enableFieldCrypto:是否启用字段级加解密。enableDesensitize:是否启用字段脱敏。
这说明 Forge 的安全能力不是硬编码在业务里,而是做成了 Starter 能力,可开可关、可配置、可组合。
十、和普通"接口加密工具类"有什么区别?
很多项目也有接口加密,但只是一个工具类:
ini
String data = AesUtil.decrypt(request.getData());
这种做法只能算"能用",不算体系化。
Forge 的完整度体现在这几层:
| 层级 | Forge 做法 |
|---|---|
| 密钥交换 | RSA 公钥获取 + RSA 私钥解密会话密钥 |
| 会话密钥 | 按 token/session 维度存 Redis,默认 2 小时 |
| 请求解密 | RequestBodyAdvice 自动解密,不污染 Controller |
| 响应加密 | ResponseBodyAdvice 自动加密,统一返回密文结构 |
| 算法扩展 | EncryptorFactory 支持 SM4 / AES 可切换 |
| 动态配置 | API 配置表 + @ApiEncrypt / @ApiDecrypt 双模式 |
| 内部调用 | X-Inner-Call 跳过,避免服务间调用互相加密 |
| 防重放 | timestamp + nonce + cache,一次性请求校验 |
| 二进制保护 | 文件/图片响应自动跳过加密 |
这就是"工具类"和"框架能力"的区别。
十一、有没有短板?
有。
第一,应用层加密会增加前后端复杂度。前端必须完成公钥获取、会话密钥生成、密钥交换、请求加密、响应解密。新手调试时会比普通 JSON 接口麻烦。
第二,接口加密不是万能的。它不能替代权限校验、不能替代幂等、不能阻止业务逻辑漏洞。它只是保护传输内容和防止简单重放。
第三,动态密钥依赖缓存服务。Redis 不可用时,如果没有合理降级和监控,会影响加密接口调用。
所以我的建议是:不要全站无脑加密,只给敏感接口加密。 登录、修改密码、支付确认、审批流转、个人隐私提交,这些值得加;普通列表查询没必要都加。
十二、总结:Forge 的 API 安全强在哪?
一句话:它不是"加个 AES 工具类",而是把接口加密做成了 Spring MVC 层的基础设施。
它的亮点:
- RSA 只做密钥交换,SM4/AES 加密业务数据。
- 会话密钥绑定 token/session,并存 Redis。
- 请求体解密在 Controller 参数绑定前完成。
- 响应体加密在返回前自动完成。
- API 配置和注解两种方式都支持。
- 防重放用 timestamp + nonce 控制。
- 内部调用、二进制响应、排除路径都有跳过机制。
多租户解决"租户之间不能串",数据权限解决"租户内部谁能看什么",接口加密解决"数据传输过程不裸奔"。这三件事连起来,才是一个企业后台真正的安全底座。
下一期拆什么?
横评第 5 期可以继续拆:
1:AI 代码生成,为什么 Forge 不是让 AI 直接写一坨代码2:Flowable 工作流,流程怎么和业务绑定3:低代码 AiCrudPage,为什么协议驱动比生成代码更适合长期产品
评论区扣数字,我按票数写。
源码自取:
- Gitee:gitee.com/ForgeLab/fo...
- GitHub:github.com/yaomindong1...
- 在线演示:www.dlforgelab.com:8084/forge/login (admin / 123456)
你们项目的敏感接口怎么保护?只靠 HTTPS,还是也做应用层加密?评论区聊聊。