Spring Boot接口签名校验设计与实现
在现代Web应用开发中,API安全是一个不可忽视的重要环节。本文将详细介绍如何在Spring Boot应用中实现接口签名校验机制,确保API调用的安全性。
一、接口签名校验概述
接口签名校验是一种常见的API安全防护手段,其核心思想是:客户端和服务器端通过约定的算法对请求参数进行加密处理,生成签名,服务器端收到请求后验证签名是否合法,从而判断请求是否被篡改或伪造。
签名校验的主要作用:
- 防篡改:确保请求参数在传输过程中未被修改
- 防重放:通过时间戳防止请求被重复使用
- 身份验证:验证调用方的合法性
二、签名校验流程设计
在我们的实现中,签名校验流程如下:
- 客户端准备请求参数
- 客户端按照约定规则生成签名
- 客户端将签名和必要参数放入HTTP头
- 服务端拦截器校验签名有效性
- 服务端根据校验结果决定是否继续处理请求
三、核心代码实现
1. 拦截器配置
首先,我们需要配置一个拦截器来拦截需要进行签名校验的请求:
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate AppValidInterceptor appValidInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(appValidInterceptor).addPathPatterns("/test123"); // 指定需要拦截的路径}
}
2. 签名校验拦截器
这是签名校验的核心实现:
@Slf4j
@Component
public class AppValidInterceptor implements HandlerInterceptor {@Resourceprivate MessageSource messageSource;@Resourceprivate EncryptionUtils encryptionUtils;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {// 从请求头获取签名相关参数String sign = request.getHeader(AppConstant.REQUEST_HEADER_SIGN);String appVersion = request.getHeader(AppConstant.REQUEST_HEADER_VERSION);String timestampsStr = request.getHeader(AppConstant.REQUEST_HEADER_TIMESTAMPS);long timestamps = Long.parseLong(timestampsStr);String uri = request.getRequestURI().replace("//", "/");log.info("请求URI:{}", uri);// 检查是否需要签名校验if(uri.contains("test123")) {// 基本参数校验if(StrUtil.isBlank(appVersion)){log.error("请求被拒绝:{}", uri);throw new ServiceException(getMessage("default_req_reject_message"));}// 版本号处理int version = Integer.parseInt(appVersion.replace(".", ""));// 针对低版本的特殊处理if(version <= 450){// 时间戳校验(15秒内有效)if(Math.abs(System.currentTimeMillis() / 1000 - timestamps) > 15) {log.error("签名错误,时间戳大于15S,系统时间戳:{},客户端时间戳:{}",System.currentTimeMillis()/1000, timestamps);throw new ServiceException(getMessage("default_req_reject_message"));}// 签名校验String expectedContent = appVersion + "." + uri + "/" + timestamps;if(!Objects.equals(encryptionUtils.decrypt(sign), expectedContent)) {log.error("签名错误,sign解析失败,解析值:{}", encryptionUtils.decrypt(sign));throw new ServiceException(getMessage("default_req_reject_message"));}}}return true;}private String getMessage(String code) {return messageSource.getMessage(code, null, LocaleContextHolder.getLocale());}
}
3. 加密工具类
我们使用RSA非对称加密算法进行签名验证:
@Component
public class EncryptionUtils {@Value("classpath:keytool/public_key.pem")private Resource publicKeyResource;@Value("classpath:keytool/private_key.pem")private Resource privateKeyResource;private PublicKey publicKey;private PrivateKey privateKey;@PostConstructpublic void init() throws Exception {publicKey = loadPublicKey();privateKey = loadPrivateKey();}// 加载公钥private PublicKey loadPublicKey() throws Exception {InputStream inputStream = publicKeyResource.getInputStream();String publicKeyStr = IoUtil.read(inputStream, StandardCharsets.UTF_8);byte[] publicBytes = Base64.getDecoder().decode(publicKeyStr);KeyFactory keyFactory = KeyFactory.getInstance("RSA");X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes);return keyFactory.generatePublic(keySpec);}// 加载私钥private PrivateKey loadPrivateKey() throws Exception {InputStream inputStream = privateKeyResource.getInputStream();String privateKeyStr = IoUtil.read(inputStream, StandardCharsets.UTF_8);byte[] privateBytes = Base64.getDecoder().decode(privateKeyStr);KeyFactory keyFactory = KeyFactory.getInstance("RSA");PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateBytes);return keyFactory.generatePrivate(keySpec);}// RSA加密public String encrypt(String plaintext) {try {Cipher cipher = Cipher.getInstance("RSA");cipher.init(Cipher.ENCRYPT_MODE, publicKey);byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));return Base64.getEncoder().encodeToString(encryptedBytes);} catch (Exception e) {log.error("加密失败", e);return null;}}// RSA解密public String decrypt(String ciphertext) {try {Cipher cipher = Cipher.getInstance("RSA");cipher.init(Cipher.DECRYPT_MODE, privateKey);byte[] encryptedBytes = Base64.getDecoder().decode(ciphertext);byte[] decryptedBytes = cipher.doFinal(encryptedBytes);return new String(decryptedBytes, StandardCharsets.UTF_8);} catch (Exception e) {log.error("解密失败", e);return null;}}
}
4. 常量定义
public interface AppConstant {// 请求头字段名String REQUEST_HEADER_SIGN = "sign";String REQUEST_HEADER_VERSION = "appVersion";String REQUEST_HEADER_TIMESTAMPS = "timestamps";// 不需要校验的URI列表List<String> ignoreUri = Arrays.asList("/platform/time","/device/solarFlow/today/energy","/device/smartPlug/energy");// 设备相关不需要校验的APIList<String> deviceIgnoreApi = Collections.singletonList("/productModule/device/mobile/bind");
}
四、签名生成与校验流程详解
客户端签名生成步骤:
- 准备参数:
- 获取当前时间戳(秒级):
long timestamp = System.currentTimeMillis() / 1000
- 获取应用版本号:如"1.0.0"
- 获取请求URI:如"/api/test"
- 获取当前时间戳(秒级):
- 拼接签名字符串:
String signContent = appVersion + "." + uri + "/" + timestamp;
- 使用私钥加密生成签名:
String sign = encryptWithPrivateKey(signContent);
- 设置HTTP头:
- sign: 生成的签名
- appVersion: 应用版本号
- timestamps: 时间戳
服务端校验流程:
- 拦截请求:通过拦截器拦截指定路径的请求
- 获取请求头:提取sign、appVersion和timestamps
- 基本校验:
- 检查必要参数是否存在
- 检查时间戳是否在有效期内(防止重放攻击)
- 签名验证:
- 使用私钥解密sign得到原始字符串
- 按照相同规则拼接预期字符串
- 比较解密结果与预期字符串是否一致
- 结果处理:
- 验证通过:放行请求
- 验证失败:返回错误信息
五、安全性增强建议
- HTTPS传输:确保所有API请求都通过HTTPS传输,防止中间人攻击
- 动态密钥:可以考虑定期更换密钥,增强安全性
- 请求限流:对频繁失败的请求进行限流,防止暴力破解
- 签名算法升级:支持多种签名算法,便于后期升级
- 详细日志:记录详细的校验日志,便于问题排查和安全审计
六、拓展实现
1. 支持多种签名算法
我们可以扩展加密工具类,支持多种算法:
public enum SignAlgorithm {RSA("RSA"),AES("AES"),HMAC_SHA256("HmacSHA256");private String algorithmName;SignAlgorithm(String algorithmName) {this.algorithmName = algorithmName;}public String getAlgorithmName() {return algorithmName;}
}// 在EncryptionUtils中添加方法
public String sign(String content, SignAlgorithm algorithm, String secret) {switch (algorithm) {case RSA:return rsaSign(content);case HMAC_SHA256:return hmacSha256(content, secret);// 其他算法...default:throw new IllegalArgumentException("不支持的签名算法");}
}
2. 更灵活的白名单配置
改进常量类,支持从配置文件读取白名单:
@Configuration
@ConfigurationProperties(prefix = "api.security")
public class ApiSecurityProperties {private List<String> ignoreUris = new ArrayList<>();private List<String> deviceIgnoreApis = new ArrayList<>();// getters and setters
}
然后在拦截器中注入使用:
@Resource
private ApiSecurityProperties apiSecurityProperties;// 使用方式
if (apiSecurityProperties.getIgnoreUris().contains(uri)) {return true;
}
七、总结
本文详细介绍了Spring Boot中接口签名校验的实现方案,包括:
- 拦截器配置与实现
- RSA非对称加密的应用
- 签名生成与验证流程
- 安全性增强建议
- 可扩展性改进方案
通过合理的签名校验机制,可以有效提升API接口的安全性,防止常见的安全威胁。在实际项目中,可以根据具体需求调整签名算法、有效期等参数,在安全性和用户体验之间取得平衡。
完整的示例代码已在上文中给出,读者可以直接参考实现或根据实际需求进行修改。希望本文对您的API安全实践有所帮助!