一、验签核心概念与解决的问题
1. 验签核心概念
接口验签(接口签名验证)是服务端对客户端请求的合法性、完整性进行校验的安全机制 ,核心基于加密算法+密钥验证实现:客户端通过约定算法对请求参数生成唯一签名,服务端用相同算法重新生成签名并与客户端签名对比,一致则判定请求合法,反之则拒绝请求。
本次实现基于HmacSHA256不可逆加密算法 ,结合时间戳+随机数+Redis原子操作,是生产环境主流的接口安全防护方案,客户端与服务端需共用相同的签名生成逻辑,保证算法一致性。
2. 验签解决的核心问题
在分布式/跨系统接口调用场景中,未做验签的接口会面临三大核心安全风险,验签机制可彻底解决这些问题:
- 参数被篡改:黑客拦截请求后修改参数(如修改订单金额、用户ID),导致业务异常;
- 非法请求冒充:黑客伪造请求调用接口,无需合法身份即可执行业务操作(如伪造进件请求、恶意调用OCR接口);
- 重放攻击:黑客拦截合法请求后,多次重复提交(如重复提交支付请求、重复进件),导致数据重复、业务异常;
- 附加价值:精准管控接口访问,仅允许合法客户端、在有效时间内、单次调用指定核心接口,降低系统安全风险。
二、验签核心能力与设计原则
1. 核心能力
- 签名验证:验证客户端身份合法性,仅持有合法密钥的客户端可生成有效签名;
- 参数防篡改:参数任意修改会导致签名不一致,服务端直接拒绝;
- 防重放攻击:双重防护(时间戳限制有效窗口+Redis原子操作保证请求唯一),杜绝重复请求;
- 环境隔离:开发/测试环境跳过验签,提升联调效率,生产环境强制全量校验;
- 多请求适配:兼容POST(JSON请求体)、GET/POST(URL拼接参数)等所有HTTP请求方式。
2. 设计原则
- 框架适配:基于BladeX内置工具类(BladeRedis/SpringUtil/StringUtils),无额外依赖,开箱即用;
- 可复用性:代码模块化、参数可配置,复制到任意BladeX项目仅需少量修改即可使用;
- 快速失败:按「环境→基础→防重放→核心签名」顺序校验,一步失败直接终止,无冗余执行,提升性能;
- 无侵入扩展:仅新增拦截器与配置,不修改BladeX框架原生代码,不影响原有功能(安全/跨域/分页等);
- 日志规范:全流程分级日志记录,包含请求地址、校验步骤、失败原因,便于问题排查;
- 异常统一:自定义专属验签异常,适配全局异常处理器,返回统一错误格式。
三、核心加密工具类 - ApiSecurity(客户端/服务端共用)
作用 :验签算法核心,封装HmacSHA256签名生成/验证逻辑,客户端和服务端必须完全一致 (包名、代码、配置无任何差异),是签名一致性的基础。
包路径 :com.xxx.common.security(建议客户端/服务端包名统一,便于复用)
核心原理:参数标准化排序→固定格式拼接→HmacSHA256密钥加密→Base64编码,生成唯一、不可逆、可传输的签名。
代码实现(详尽注释+示例说明)
java
package com.xxx.common.security;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.TreeMap;
/**
* 签名核心工具类(客户端/服务端共用,必须完全一致,任何修改需两端同步)
* 基于HmacSHA256实现不可逆签名,核心能力:参数防篡改 + 客户端身份合法性验证
* 设计原则:算法统一、参数标准化、加密不可逆、传输友好
* @author 验签模块
*/
@Slf4j
public class ApiSecurity {
// ===================== 基础配置(生产环境建议移至Nacos/配置中心,多环境隔离)=====================
/** 应用公钥:标识客户端身份,可随请求传输,建议为每个客户端分配独立APP_KEY */
public static final String APP_KEY = "oqukikZ2SulpgSNd";
/** 应用私钥:签名核心密钥,客户端+服务端私密保存,绝对禁止网络传输!泄露则会被冒充 */
public static final String APP_SECRET = "681FINjnSkK5D0Nl8PtcAa8abi55VedO";
/** 签名加密算法:HmacSHA256(基于密钥的哈希消息认证码,不可逆、抗篡改、唯一性) */
private static final String HMAC_ALGORITHM = "HmacSHA256";
// ==================================================================================================
/**
* 生成签名(客户端/服务端共用核心方法)
* 严格执行固定流程,保证客户端/服务端签名一致性
* @param params 所有请求参数(必须包含appKey+timestamp+nonce+业务参数,不可缺省)
* @return Base64编码的签名字符串(可直接网络传输,无乱码、无损耗)
* @throws RuntimeException 签名生成失败(算法不支持、密钥异常等系统级错误)
*
* 【示例】:
* 入参params:{appKey:"oqukikZ2SulpgSNd", timestamp:1735689600, nonce:"a1b2c3d4", informationId:"1988180641392365569"}
* 步骤1排序后:appKey、informationId、nonce、timestamp
* 步骤2拼接后:appKey=oqukikZ2SulpgSNd&informationId=1988180641392365569&nonce=a1b2c3d4×tamp=1735689600
* 步骤3HmacSHA256加密:基于APP_SECRET生成二进制哈希值
* 步骤4Base64编码:生成最终签名字符串(如:f2L7R9t5U8x0S3b6N7m2K1p9Q8w4E5r0T2g7H3j1F6d8G9s2A4z7X5c3V8b1N6m9K0)
*/
public static String generateSignature(Map<String, Object> params) {
try {
// 步骤1:参数按Key自然排序(TreeMap)- 解决Map无序问题,保证客户端/服务端拼接顺序完全一致
// 【核心前提】:排序是签名一致的基础,无序Map会导致相同参数拼接出不同字符串,签名必然不一致
TreeMap<String, Object> sortedParams = new TreeMap<>(params);
StringBuilder paramSb = new StringBuilder();
// 步骤2:固定格式拼接参数字符串 - key=value&,过滤空值避免解析不一致
// 【规范约定】:统一拼接规则,无额外分隔符,过滤空值防止客户端传空导致服务端解析异常
sortedParams.forEach((key, value) -> {
if (value != null && !"".equals(value.toString().trim())) {
paramSb.append(key).append("=").append(value).append("&");
}
});
// 移除最后一个多余的&符号,得到标准参数字符串
String paramStr = paramSb.substring(0, paramSb.length() - 1);
log.debug("签名原始参数字符串:{}", paramStr);
// 步骤3:HmacSHA256加密 - 基于APP_SECRET生成二进制哈希值
// 【加密核心】:不可逆加密,参数/密钥任意修改,哈希值立即变化,实现参数防篡改
// 【示例】:若黑客修改informationId为"123456",拼接后的字符串改变,加密后哈希值完全不同
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec secretKeySpec = new SecretKeySpec(
APP_SECRET.getBytes(StandardCharsets.UTF_8),
HMAC_ALGORITHM
);
mac.init(secretKeySpec);
byte[] encryptBytes = mac.doFinal(paramStr.getBytes(StandardCharsets.UTF_8));
// 步骤4:Base64编码 - 二进制转可见字符串,适配网络传输
// 【传输适配】:加密后的二进制包含不可见字符,Base64编码为字母/数字组合,避免传输乱码、转义错误
return Base64.getEncoder().encodeToString(encryptBytes);
} catch (Exception e) {
log.error("签名生成失败,原因:", e);
// 签名生成失败为系统级异常,直接抛出运行时异常中断请求
throw new RuntimeException("签名生成失败,系统异常", e);
}
}
/**
* 验证签名(服务端专用)
* 核心原理:服务端用相同参数+相同密钥重新生成签名,与客户端传入签名对比,一致则合法
* @param params 服务端解析的完整请求参数(与客户端传入参数完全一致)
* @param clientSign 客户端请求头中传入的签名字符串
* @return true=验签通过,false=验签失败(签名为空/不一致)
*
* 【示例】:
* 客户端传入sign:f2L7R9t5U8x0S3b6N7m2K1p9Q8w4E5r0T2g7H3j1F6d8G9s2A4z7X5c3V8b1N6m9K0
* 服务端用相同参数生成serverSign:f2L7R9t5U8x0S3b6N7m2K1p9Q8w4E5r0T2g7H3j1F6d8G9s2A4z7X5c3V8b1N6m9K0 → 对比一致,验签通过
* 若参数被篡改,服务端生成serverSign:a3M8S0u6V9y1D4c7B8n3L2o0P9x5F6s1G3h8J4k2E7e9H0s3Z5a7X6d4W9c2M7n0K → 对比不一致,验签失败
*/
public static boolean verifySignature(Map<String, Object> params, String clientSign) {
// 快速校验:签名为空直接返回失败
if (clientSign == null || clientSign.trim().isEmpty()) {
log.warn("验签失败,客户端传入签名为空");
return false;
}
// 服务端重新生成签名(与客户端完全相同的逻辑)
String serverSign = generateSignature(params);
log.debug("服务端生成签名:{},客户端传入签名:{}", serverSign, clientSign);
// 严格字符串对比,保证签名完全一致
boolean verifyResult = serverSign.equals(clientSign);
if (!verifyResult) {
log.warn("验签失败,服务端与客户端签名不一致,参数可能被篡改或身份非法");
}
return verifyResult;
}
}
关键说明
- 日志分级:debug级日志记录签名原始串/对比结果(开发调试用),error/warn级记录异常/失败信息(生产环境可查看);
- 空值过滤 :拼接参数时过滤空值,避免客户端传空参数(如
userName: "")导致服务端解析不一致; - 编码规范 :使用
StandardCharsets.UTF_8替代硬编码字符串,避免不同环境编码不一致问题; - 客户端要求 :直接复制此类到客户端项目,不得做任何修改,保证与服务端算法完全一致。
四、自定义验签拦截器 - InterfaceSignatureVerification(服务端核心执行层)
作用 :SpringMVC前置拦截器(preHandle),请求到达Controller前执行全流程验签逻辑 ,按「快速失败」原则执行,任何一步校验失败直接抛异常拒绝请求,无冗余执行。
包路径 :com.xxx.common.interceptor(BladeX框架常规拦截器包路径,符合项目规范)
核心依赖:BladeX内置工具类(无额外引入,开箱即用),复用项目原有参数解析工具类,保持逻辑统一。
代码实现(详尽注释+示例说明)
java
package com.xxx.common.interceptor;
import com.xxx.common.security.ApiSecurity;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.utils.SpringUtil;
import org.springblade.core.tool.utils.StringUtils;
import org.springblade.redis.cache.BladeRedis;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/**
* BladeX框架自定义验签拦截器(服务端核心执行层)
* 执行时机:请求到达Controller之前(preHandle)
* 校验流程:环境隔离→基础校验→防重放校验→核心签名验证(快速失败,一步失败直接终止)
* 核心依赖:BladeX内置工具类(无额外引入,开箱即用)
* @author 验签模块
*/
@Slf4j
@Component // 交给Spring容器管理,保证依赖可注入,拦截器注册时直接new即可
public class InterfaceSignatureVerification implements HandlerInterceptor {
// ===================== 防重放配置参数(生产环境建议移至application.yml,@Value注入)=====================
/** 时间戳有效窗口:单位秒,建议3-5分钟(300秒),兼顾防重放和客户端网络延迟/时间微小偏差 */
private static final long TIMESTAMP_VALID_WINDOW = 300;
/** Redis中nonce过期时间:单位秒,必须≥时间戳有效窗口,避免Redis缓存堆积,建议比有效窗口多一倍 */
private static final long NONCE_EXPIRE_SECONDS = 600;
/** Redis Key前缀:遵循BladeX框架命名规范「blade:业务模块:功能:子功能:」,避免与其他业务Key冲突 */
private static final String NONCE_REDIS_KEY_PREFIX = "blade:interface:sign:nonce:";
// ==========================================================================================================
/**
* 前置拦截核心方法:执行全流程验签逻辑
* @return true=所有校验通过,放行请求;false=拒绝请求
* @throws ForbiddenException 所有验签失败场景,统一抛出该异常(适配全局异常处理器)
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestUri = request.getRequestURI();
log.info("开始执行接口验签,请求地址:{}", requestUri);
// 1. 从请求头获取客户端传入的签名(约定:签名必须放在请求头的sign字段中)
String clientSign = request.getHeader("sign");
// ===================== 步骤1:环境隔离校验 - dev/test环境跳过验签,提升开发联调效率 =====================
Environment environment = SpringUtil.getBean(Environment.class);
if (environment.acceptsProfiles(Profiles.of("dev", "test"))) {
log.info("当前为开发/测试环境,跳过验签,直接放行请求:{}", requestUri);
return true;
}
log.info("当前为生产环境,执行全量验签逻辑,请求地址:{}", requestUri);
// ===================== 步骤2:基础参数校验 - 快速拦截无签名/无参数的非法请求 =====================
log.info("开始基础参数校验");
// 2.1 校验签名非空
if (StringUtils.isBlank(clientSign)) {
log.error("基础参数校验失败,请求头缺少签名参数sign,请求地址:{}", requestUri);
throw new ForbiddenException("请求头缺少签名参数sign,禁止访问");
}
// 2.2 解析请求参数(复用项目原有工具类,兼容JSON请求体/URL拼接参数)
// 解析规则:先解析POST JSON请求体,解析失败则解析GET/POST URL参数,保证参数完整获取
// 【示例】:
// POST请求(JSON体):{"appKey":"xxx","timestamp":1735689600,"nonce":"a1b2c3d4","informationId":"123"} → 正常解析
// GET请求(URL参数):/blade-api/information/applyInfomation?appKey=xxx×tamp=1735689600 → 正常解析
Map<String, Object> requestParams = DataConvertHelper.getRequestBodyParams(request);
if (StringHelper.IsEmptyOrNull(requestParams)) {
requestParams = DataConvertHelper.getRequestParams(request);
}
// 2.3 校验参数非空
if (StringHelper.IsEmptyOrNull(requestParams)) {
log.error("基础参数校验失败,请求无有效参数,请求地址:{}", requestUri);
throw new ForbiddenException("请求无有效参数,禁止访问");
}
log.info("基础参数校验通过,解析到请求参数:{},请求地址:{}", requestParams, requestUri);
// ===================== 步骤3:防重放校验1 - timestamp时间戳时效校验(限制请求有效时间窗口) =====================
log.info("开始防重放校验 - 时间戳时效校验");
Object timestampObj = requestParams.get("timestamp");
// 3.1 校验timestamp参数存在
if (timestampObj == null) {
log.error("防重放校验失败,缺少timestamp参数,请求地址:{}", requestUri);
throw new ForbiddenException("缺少防重放参数timestamp,禁止访问");
}
long requestTimestamp;
try {
// 3.2 校验timestamp格式:必须为秒级数字(与客户端约定统一格式,避免毫秒/秒混淆)
// 【示例】:合法值:1735689600(2025-01-01 00:00:00);非法值:"2025-01-01"、1735689600000(毫秒)
requestTimestamp = Long.parseLong(timestampObj.toString().trim());
} catch (NumberFormatException e) {
log.error("防重放校验失败,timestamp格式错误,必须为秒级数字,参数值:{},请求地址:{}", timestampObj, requestUri, e);
throw new ForbiddenException("timestamp参数格式错误,必须为秒级数字,禁止访问");
}
// 3.3 校验timestamp时效:仅判断「请求时间不能太早」,不判断「请求时间晚于当前时间」
// 【兼容设计】:避免客户端时区错误、系统时间微小偏差(如快10秒)导致合法请求失败,仅限制有效窗口
// 【示例】:
// 服务端当前时间:1735692900(2025-01-01 01:05:00),有效窗口300秒(5分钟)
// 请求时间:1735689600(2025-01-01 00:00:00)→ 间隔3300秒>300秒 → 请求过期,验签失败
// 请求时间:1735692600(2025-01-01 01:00:00)→ 间隔300秒=300秒 → 有效,验签通过
long currentServerTimestamp = System.currentTimeMillis() / 1000;
if (currentServerTimestamp - requestTimestamp > TIMESTAMP_VALID_WINDOW) {
log.error("防重放校验失败,请求已过期,有效窗口{}秒,请求时间:{},服务端当前时间:{},请求地址:{}",
TIMESTAMP_VALID_WINDOW, requestTimestamp, currentServerTimestamp, requestUri);
throw new ForbiddenException("请求已过期,有效时间窗口" + TIMESTAMP_VALID_WINDOW + "秒,禁止访问");
}
log.info("时间戳时效校验通过,请求在有效窗口内,请求地址:{}", requestUri);
// ===================== 步骤4:防重放校验2 - nonce唯一性校验(BladeRedis原子操作,防止重复请求) =====================
log.info("开始防重放校验 - nonce唯一性校验");
Object nonceObj = requestParams.get("nonce");
// 4.1 校验nonce参数存在且非空(nonce:全局唯一随机数,建议客户端用UUID生成,保证永不重复)
if (nonceObj == null || StringUtils.isBlank(nonceObj.toString().trim())) {
log.error("防重放校验失败,缺少nonce参数,请求地址:{}", requestUri);
throw new ForbiddenException("缺少防重放参数nonce,禁止访问");
}
String nonce = nonceObj.toString().trim();
String redisNonceKey = NONCE_REDIS_KEY_PREFIX + nonce;
// 4.2 BladeRedis原子操作:setIfAbsent(底层封装Redis SET NX EX指令)
// 【核心原理】:原子性执行「Key不存在则设置值+过期时间」,首次请求返回true,重复请求返回false
// 【高并发适配】:单原子操作,无并发问题,即使多个请求同时传入相同nonce,仅一个能设置成功
// 【示例】:
// 首次请求nonce:a1b2c3d4 → RedisKey:blade:interface:sign:nonce:a1b2c3d4 → 不存在,设置成功返回true
// 重复请求相同nonce:a1b2c3d4 → RedisKey已存在,返回false → 验签失败,拒绝请求
boolean isNonceFirstUse = BladeRedis.setIfAbsent(redisNonceKey, "1", NONCE_EXPIRE_SECONDS);
// 4.3 校验nonce是否为首次使用
if (!isNonceFirstUse) {
log.error("防重放校验失败,请求重复提交,nonce已被使用,RedisKey:{},请求地址:{}", redisNonceKey, requestUri);
throw new ForbiddenException("请求重复提交,禁止访问");
}
log.info("nonce唯一性校验通过,RedisKey:{},已设置过期时间{}秒,请求地址:{}",
redisNonceKey, NONCE_EXPIRE_SECONDS, requestUri);
// ===================== 步骤5:核心签名验证 - 验证参数防篡改+客户端身份合法性 =====================
log.info("开始核心签名验证,请求地址:{}", requestUri);
boolean isSignValid = ApiSecurity.verifySignature(requestParams, clientSign);
if (!isSignValid) {
log.error("核心签名验证失败,参数可能被篡改或客户端身份非法,请求地址:{}", requestUri);
throw new ForbiddenException("签名验证失败,参数可能被篡改或身份非法,禁止访问");
}
log.info("核心签名验证通过,参数未被篡改,客户端身份合法,请求地址:{}", requestUri);
// ===================== 所有校验通过,放行请求 =====================
log.info("接口验签全流程通过,放行请求,请求地址:{}", requestUri);
return true;
}
/**
* 自定义验签失败异常(专属异常,便于全局异常处理器精准捕获)
* 异常类型:运行时异常,无需显式捕获,由Spring全局异常处理器统一处理
* 适配建议:若项目已使用BladeX框架的BladeException,可直接替换为该异常(保持框架异常体系统一)
* 【性能优化】:关闭堆栈填充(suppressed=false, writableStackTrace=false),验签失败为业务异常,无需完整堆栈
*/
public static class ForbiddenException extends RuntimeException {
public ForbiddenException(String message) {
super(message, null, false, false);
}
}
/**
* 项目原有工具类引用(无需重复实现,直接指向项目中已存在的工具类)
* 作用:避免代码冗余,保持项目参数解析逻辑的统一性
*/
static class DataConvertHelper {
/**
* 解析POST请求体参数(JSON格式)
* @param request HttpServletRequest
* @return 解析后的参数Map
*/
public static Map<String, Object> getRequestBodyParams(HttpServletRequest request) {
return com.xxx.common.utils.DataConvertHelper.getRequestBodyParams(request);
}
/**
* 解析URL拼接参数(GET/POST都支持)
* @param request HttpServletRequest
* @return 解析后的参数Map
*/
public static Map<String, Object> getRequestParams(HttpServletRequest request) {
return com.xxx.common.utils.DataConvertHelper.getRequestParams(request);
}
}
/**
* 项目原有字符串/集合工具类引用(无需重复实现)
*/
static class StringHelper {
/**
* 判断Map是否为空(null/无元素都视为空)
* @param map 待判断Map
* @return true=空,false=非空
*/
public static boolean IsEmptyOrNull(Map<String, Object> map) {
return com.xxx.common.utils.StringHelper.IsEmptyOrNull(map);
}
}
}
关键设计亮点
- 快速失败原则:按「环境→基础→防重放→核心」顺序校验,一步失败直接抛异常,不执行后续冗余逻辑(如无签名则直接拒绝,无需解析参数),提升验签效率;
- Redis原子操作 :基于BladeRedis
setIfAbsent实现nonce唯一性校验,底层封装RedisSET NX EX指令,无并发问题,完美解决高并发下的重复请求问题; - 兼容设计 :
- 时间戳仅校验「不早于有效窗口」,兼容客户端时区错误、系统时间微小偏差(如快10秒);
- 参数解析兼容JSON请求体/URL拼接参数,适配POST/GET等所有HTTP请求方式;
- 专属异常设计 :自定义
ForbiddenException,精准标识验签失败场景,关闭堆栈填充提升性能,便于全局异常处理器统一处理; - 代码复用:直接引用项目原有参数解析工具类,避免代码冗余,保持项目逻辑统一;
- Spring容器管理 :添加
@Component注解,拦截器注册时直接new即可,Spring自动注入容器实例,无需手动管理依赖。
五、BladeX框架配置类 - 验签拦截器注册(精准管控验签接口)
作用 :在BladeX核心配置类BladeConfiguration中,注册自定义验签拦截器,并精准指定需要验签的接口路径 ,实现「按需验签」------ 仅核心业务接口(如进件、OCR)参与验签,框架原生接口、非核心接口不触发验签,降低系统性能损耗。
修改原则 :无侵入式修改 ,仅在原有addInterceptors方法中新增验签拦截器注册代码,其余所有原有配置(安全/跨域/分页/Redis等)完全保留,不影响框架原生功能。
包路径 :项目原有BladeConfiguration路径(如com.xxx.config)
代码实现(仅展示验签相关修改,其余原有代码完全保留)
java
package com.xxx.config;
import com.xxx.common.interceptor.InterfaceSignatureVerification;
import org.springblade.core.launch.constant.AppConstant;
import org.springblade.core.secure.config.SecureRegistry;
import org.springblade.core.tool.utils.StringPool;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* BladeX框架核心配置类(仅展示验签相关修改,其余原有配置完全保留)
* 验签相关修改:仅在addInterceptors方法中注册验签拦截器,精准指定验签接口
* 设计原则:无侵入式扩展,不修改框架原生配置,不影响原有功能(安全/跨域/分页/Redis等)
* @author Chill(原有作者)/ 验签模块(新增)
*/
@Configuration(proxyBeanMethods = false)
public class BladeConfiguration implements WebMvcConfigurer {
// 原有注入属性、Bean配置(secureRegistry/redisTemplate/mybatisPlusInterceptor等)完全保留 ↓
// 原有重写方法(addCorsMappings/configurePathMatch/addArgumentResolvers等)完全保留 ↓
@Value("${xxl.job.accessToken}")
String accessToken;
// ===================== 原有Bean配置,完全保留 =====================
@Bean
public SecureRegistry secureRegistry() {
return new SecureRegistry()
.enabled()
.strictTokenEnabled()
.strictHeaderEnabled()
.skipUrls(
"/blade-auth/**",
"/blade-system/tenant/info",
"/blade-flow/process/resource-view",
"/blade-flow/process/diagram-view",
"/blade-flow/manager/check-upload",
"/doc.html",
"/swagger-ui.html",
"/static/**",
"/webjars/**",
"/swagger-resources/**",
"/druid/**"
)
.authEnabled()
.addAuthPattern(HttpMethod.ALL, "/blade-chat/message/**", "hasAuth()")
.addAuthPattern(HttpMethod.POST, "/blade-desk/dashboard/upload", "hasTimeAuth(9, 17)")
.addAuthPattern(HttpMethod.POST, "/blade-desk/dashboard/submit", "hasAnyRole('administrator', 'admin', 'user')")
.basicEnabled()
.addBasicPattern(HttpMethod.POST, "/blade-desk/dashboard/info", "blade", "blade")
.signDisabled()
.addSignPattern(HttpMethod.POST, "/blade-desk/dashboard/sign", "sha1");
}
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
return redisTemplate;
}
// ==================================================================
/**
* 拦截器注册(原有代码+验签拦截器注册,无其他修改)
* 核心:注册自定义验签拦截器,仅对指定核心业务接口生效,实现精准验签
* 【示例】:仅对进件、OCR相关接口验签,其他接口(如/blade-auth/**、/blade-tripartite/**)不触发
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 1. 原有xxl-job拦截器注册(完全保留,无修改)
registry.addInterceptor(new JobInterceptor(accessToken))
.addPathPatterns("/job/**")
.addPathPatterns("**/job/**")
.excludePathPatterns("/blade-auth/**");
// 2. 原有三方接口拦截器注册(完全保留,无修改)
registry.addInterceptor(new TripartiteInterceptor())
.addPathPatterns("/blade-api/tripartite/**");
// ================================= 新增:验签拦截器注册(核心修改)=================================
// 注册自定义验签拦截器,精准指定需要验签的核心业务接口,非指定接口不触发验签
registry.addInterceptor(new InterfaceSignatureVerification())
// 进件相关核心接口(验签)
.addPathPatterns("/blade-api/information/applyInformationFirst")
.addPathPatterns("/blade-api/information/applyInfomation")
.addPathPatterns("/blade-api/information/AutoRuleValidation")
// OCR识别相关核心接口(验签)
.addPathPatterns("/blade-api/upload/upload-images-with-ocr-H5");
// =================================================================================================
}
// 原有其他重写方法(addCorsMappings/configurePathMatch等)完全保留 ↓
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/cors/**")
.allowedOriginPatterns("*")
.allowedHeaders("*")
.allowedMethods("*")
.maxAge(3600)
.allowCredentials(true);
}
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix(StringPool.SLASH + AppConstant.APPLICATION_AUTH_NAME,
c -> c.isAnnotationPresent(RestController.class) && (
OAuth2TokenEndPoint.class.equals(c) || OAuth2SocialEndpoint.class.equals(c) || Oauth2SmsEndpoint.class.equals(c))
);
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new FlowableNodeResolver());
}
}
注册关键说明
- 精准接口匹配 :通过
addPathPatterns指定仅核心业务接口 参与验签,框架原生接口(如/blade-auth/**、/doc.html)、非核心业务接口不触发验签,降低性能损耗; - 无侵入式扩展:仅新增3行核心代码,原有所有拦截器、Bean配置、重写方法完全保留,不影响项目原有功能;
- 拦截器执行顺序 :SpringMVC拦截器按注册顺序执行,验签拦截器在原有拦截器(xxl-job/三方接口)之后执行,不影响原有拦截器逻辑;
- 便捷注册 :因
InterfaceSignatureVerification添加了@Component注解,直接new InterfaceSignatureVerification()即可,Spring会自动注入容器中的实例,无需手动注入依赖。
六、Redis基础配置(BladeX框架原生,仅需配置文件)
作用 :为BladeX内置的BladeRedis工具类提供Redis连接信息,无需编写任何代码 ,仅在配置文件中配置即可,BladeRedis会自动加载配置,开箱即用。
核心要求:Redis服务已启动,配置信息正确,保证服务端能正常连接Redis(用于nonce唯一性校验,实现防重放攻击)。
配置文件(application.yml / 分环境配置application-prod.yml)
yaml
# 配置文件路径:src/main/resources/application.yml(全局)/ application-prod.yml(生产环境专属)
# BladeX框架Redis原生配置,BladeRedis工具类自动加载,无需额外配置代码
spring:
redis:
# Redis服务地址(生产环境建议配置集群/哨兵地址,如:192.168.1.100:6379,192.168.1.101:6379)
host: 127.0.0.1
# Redis服务端口
port: 6379
# Redis密码(无密码则留空,生产环境建议设置复杂密码,避免暴力破解)
password:
# 数据库索引:建议单独分配一个数据库(如1),避免验签的nonce键与框架其他业务(缓存/会话)Key冲突
database: 1
# 连接超时时间:单位毫秒,建议3000ms
timeout: 3000
# Lettuce连接池配置(BladeX默认使用Lettuce,高性能,支持连接池,避免频繁创建/销毁连接)
lettuce:
pool:
max-active: 200 # 最大连接数:根据业务并发量调整(如并发100则设为200)
max-idle: 20 # 最大空闲连接:保持一定空闲连接,提升响应速度
min-idle: 5 # 最小空闲连接:避免连接池空了之后频繁创建连接
max-wait: -1 # 最大等待时间:-1表示无限制,建议根据业务调整(如1000ms)
配置优化建议
- 分环境配置 :dev/test环境使用本地Redis(
host:127.0.0.1),prod环境使用生产Redis集群/哨兵,通过application-{env}.yml实现多环境隔离; - 单独数据库 :为验签模块分配独立的Redis数据库(如
database:1),避免与框架其他业务Key冲突,便于运维排查(如单独清理验签相关Key); - 生产高可用:prod环境必须配置Redis集群/哨兵模式,保证Redis服务高可用,避免单点故障导致验签失败;
- 连接池调优 :根据项目实际并发量调整
max-active/max-idle,如高并发接口(OCR/进件)可将max-active调至300-500,避免连接池溢出。
七、一次完整验签请求的全流程(客户端→服务端,带示例)
结合以上所有代码,从客户端发起请求到服务端执行业务逻辑的完整验签流程共14步,每步附带具体示例,清晰展示验签的工作原理、执行细节和安全防护效果。
前置约定(客户端必须遵守)
- 客户端与服务端使用完全相同的
ApiSecurity类(包名、代码、配置无差异); - 请求参数必须包含:
appKey(公钥,如oqukikZ2SulpgSNd)+timestamp(秒级时间戳,如1735689600)+nonce(UUID唯一随机数,如a1b2c3d4-5678-90ef-ghij-klmnopqrstuv)+ 业务参数; - 生成的签名必须放在HTTP请求头的
sign字段中; - 每次发起新请求,必须生成新的nonce (保证唯一),
timestamp取当前系统秒级时间。
【客户端侧:4步,生成签名并发起请求(带示例)】
以进件接口 /blade-api/information/applyInfomation 为例,客户端为Java项目:
-
构造完整请求参数 :
javaMap<String, Object> params = new HashMap<>(); params.put("appKey", "oqukikZ2SulpgSNd"); // 公钥,与服务端一致 params.put("timestamp", 1735689600L); // 秒级时间戳(2025-01-01 00:00:00) params.put("nonce", "a1b2c3d4-5678-90ef-ghij-klmnopqrstuv"); // UUID唯一随机数 params.put("informationId", "1988180641392365569"); // 业务参数:进件ID params.put("userName", "张三"); // 业务参数:用户名 -
生成签名 :调用
ApiSecurity.generateSignature(params),按「排序→拼接→加密→编码」生成签名:- 排序后参数:appKey、informationId、nonce、timestamp、userName;
- 拼接后字符串:
appKey=oqukikZ2SulpgSNd&informationId=1988180641392365569&nonce=a1b2c3d4-5678-90ef-ghij-klmnopqrstuv×tamp=1735689600&userName=张三; - 最终生成签名:
f2L7R9t5U8x0S3b6N7m2K1p9Q8w4E5r0T2g7H3j1F6d8G9s2A4z7X5c3V8b1N6m9K0;
-
设置请求头 :将签名放入请求头,同时设置Content-Type:
sign: f2L7R9t5U8x0S3b6N7m2K1p9Q8w4E5r0T2g7H3j1F6d8G9s2A4z7X5c3V8b1N6m9K0 Content-Type: application/json;charset=UTF-8 -
发起HTTP POST请求 :将参数转为JSON放入请求体,请求服务端进件接口:
- 请求地址:
http://{服务端地址}/blade-api/information/applyInfomation; - 请求体:
{"appKey":"oqukikZ2SulpgSNd","timestamp":1735689600,"nonce":"a1b2c3d4-5678-90ef-ghij-klmnopqrstuv","informationId":"1988180641392365569","userName":"张三"}。
- 请求地址:
【服务端侧:10步,拦截器全流程验签,放行则执行业务(带示例)】
服务端为BladeX框架,请求地址匹配验签拦截器配置,触发全流程校验:
- 请求到达BladeX框架:请求先经过框架原生过滤器(跨域过滤器、令牌过滤器、权限过滤器),因是验签接口,进入下一步;
- 验签拦截器触发 :SpringMVC根据
BladeConfiguration配置,将请求转发到InterfaceSignatureVerification.preHandle方法(Controller执行前); - 环境隔离校验 :判断当前环境为
prod(生产),执行全量验签逻辑; - 基础参数校验 :
- 从请求头获取
sign:f2L7R9t5U8x0S3b6N7m2K1p9Q8w4E5r0T2g7H3j1F6d8G9s2A4z7X5c3V8b1N6m9K0(非空,通过); - 解析请求体参数:与客户端传入参数完全一致(非空,通过);
- 从请求头获取
- 时间戳时效校验 :
- 从参数中获取
timestamp:1735689600(存在且为秒级数字,通过); - 服务端当前时间:
1735689700(2025-01-01 00:01:40),间隔100秒<300秒有效窗口(通过);
- 从参数中获取
- nonce唯一性校验 :
- 从参数中获取
nonce:a1b2c3d4-5678-90ef-ghij-klmnopqrstuv(存在非空,通过); - 拼接RedisKey:
blade:interface:sign:nonce:a1b2c3d4-5678-90ef-ghij-klmnopqrstuv; - 调用BladeRedis
setIfAbsent:Key不存在,设置成功返回true(通过),并为Key设置600秒过期时间;
- 从参数中获取
- 核心签名验证 :
- 服务端调用
ApiSecurity.verifySignature(params, clientSign),用相同参数重新生成签名:f2L7R9t5U8x0S3b6N7m2K1p9Q8w4E5r0T2g7H3j1F6d8G9s2A4z7X5c3V8b1N6m9K0; - 服务端签名与客户端签名完全一致,返回
true(通过);
- 服务端调用
- 所有校验通过 :
preHandle返回true,放行请求; - 执行业务逻辑 :请求到达
InformationController.applyInfomation方法,执行进件相关业务操作(如数据入库、规则校验); - 返回响应 :业务执行完成,服务端返回统一格式JSON响应(如
{"code":200,"msg":"进件成功","data":{"informationId":"1988180641392365569"}}),一次验签请求完成。
八、核心代码复用步骤(复制即用,仅4步)
将以上代码按包路径复制到你的BladeX项目中,仅需4步自定义修改,无需额外开发,直接投入使用,适配所有BladeX项目:
步骤1:修改包名
将所有代码中的com.xxx替换为你的项目实际包名 (如com.abc.project、com.company.bladex),保证包结构符合项目规范。
步骤2:指向项目原有工具类
修改InterfaceSignatureVerification中DataConvertHelper和StringHelper内部类的方法体,将包名替换为你项目中实际的工具类包名 (如com.abc.project.common.utils.DataConvertHelper),保证参数解析逻辑与项目统一。
步骤3:调整验签接口路径
在BladeConfiguration的addInterceptors方法中,根据你的实际业务需求 ,增/删addPathPatterns中的接口路径,仅对核心业务接口开启验签(非核心接口无需添加)。
步骤4:配置Redis连接信息
在application.yml(或分环境配置文件application-prod.yml)中,配置你的Redis实际连接信息(host/port/password/database),保证服务端能正常连接Redis(用于防重放校验)。
九、生产环境高级优化建议(可选,提升安全性/可维护性)
以上代码为生产可用基础版本,若需适配高安全、高可用的生产环境,可根据实际需求进行以下优化,提升代码的安全性、可维护性和性能:
优化1:所有参数配置化(推荐)
将ApiSecurity中的APP_KEY/APP_SECRET和拦截器中的TIMESTAMP_VALID_WINDOW/NONCE_EXPIRE_SECONDS移至配置文件,通过@Value注入,支持多环境隔离,无需修改代码即可调整参数:
yaml
# application.yml 新增验签专属配置
interface:
sign:
app-key: oqukikZ2SulpgSNd # 应用公钥
app-secret: 681FINjnSkK5D0Nl8PtcAa8abi55VedO # 应用私钥
timestamp-valid-window: 300 # 时间戳有效窗口(秒)
nonce-expire-seconds: 600 # nonce过期时间(秒)
nonce-redis-prefix: blade:interface:sign:nonce: # RedisKey前缀
注入示例 (ApiSecurity添加@Component交给Spring管理):
java
@Component
public class ApiSecurity {
@Value("${interface.sign.app-key}")
private String appKey;
@Value("${interface.sign.app-secret}")
private String appSecret;
// 其余代码不变,将原有常量替换为成员变量
}
优化2:多客户端隔离支持
为每个客户端分配独立的appKey+appSecret,避免单个密钥泄露影响所有客户端,服务端通过appKey动态获取对应密钥:
- 数据库新建
t_interface_client表,存储app_key/app_secret/client_name/status等信息; - 服务端新增
ClientService,根据参数中的appKey查询数据库,获取对应的appSecret(并校验客户端状态是否有效); - 修改
ApiSecurity.generateSignature方法,将全局APP_SECRET改为入参,传入查询到的客户端专属密钥。
优化3:适配BladeX全局异常处理器
将自定义的ForbiddenException替换为BladeX框架原生的BladeException,实现异常响应格式统一,无需单独处理验签异常:
java
// 导入BladeX异常类
import org.springblade.core.tool.api.BladeException;
// 替换原有抛异常代码
throw new BladeException(403, "签名验证失败,参数可能被篡改或身份非法");
BladeX的全局异常处理器会自动将BladeException转换为统一的JSON响应:
json
{
"code": 403,
"msg": "签名验证失败,参数可能被篡改或身份非法",
"data": null
}
优化4:APP_SECRET加密存储
生产环境避免将APP_SECRET明文配置在配置文件中,通过AES对称加密将密钥加密后配置,服务端启动时解密,提升安全性:
- 离线使用AES加密工具加密
APP_SECRET,得到密文; - 将密文配置在配置文件中,加密密钥通过环境变量/启动参数传入服务端(不写入配置文件);
- 服务端启动时,通过AES算法解密密文,得到原始
APP_SECRET。
优化5:日志精细化管理
- 生产环境关闭
debug级日志(避免泄露签名原始串、参数等敏感信息),仅保留info/error/warn级日志; - 将验签日志写入独立的日志文件 (如
interface-sign.log),通过日志配置(logback/log4j2)实现,便于运维排查; - 对频繁的验签失败日志(如同一IP多次验签失败)做限流处理,避免日志刷屏。
优化6:接口限流与IP白名单
结合BladeX框架的限流功能,对验签接口做双层防护,抵御暴力破解和恶意请求:
- 接口限流 :对验签接口按
appKey/IP做请求频率限制(如每分钟最多100次请求); - IP白名单:将合法客户端的IP加入白名单,仅允许白名单内的IP访问验签接口。
十、总结
本次提供的BladeX框架验签实现方案,是纯核心、可直接复用、功能完整、安全可靠的生产级方案,具备以下核心优势:
- 功能全覆盖:签名验证+参数防篡改+防重放攻击,彻底解决接口调用的三大核心安全风险;
- 框架深度适配:基于BladeX内置工具类,无额外依赖,零侵入式扩展,不影响原有功能;
- 复制即用:仅需4步简单修改,即可在任意BladeX项目中使用,大幅提升开发效率;
- 高性能高可用:快速失败原则、Redis原子操作、异常堆栈优化,保证验签流程高性能,适配高并发场景;
- 可维护性强:代码模块化、参数可配置、日志规范、异常统一,便于后续扩展和运维排查。
该方案可直接作为BladeX框架项目的标准接口安全防护模块,适配进件、支付、OCR、数据提交等所有核心业务接口的安全需求,兼顾安全性、易用性和性能,是分布式/跨系统接口调用的最优解。