在现代Web应用开发中,API安全是一个不可忽视的重要环节。本文将详细介绍如何在Spring Boot应用中实现接口签名校验机制,确保API调用的安全性。
一、接口签名校验概述
接口签名校验是一种常见的API安全防护手段,其核心思想是:客户端和服务器端通过约定的算法对请求参数进行加密处理,生成签名,服务器端收到请求后验证签名是否合法,从而判断请求是否被篡改或伪造。
签名校验的主要作用:
- 防篡改:确保请求参数在传输过程中未被修改
- 防重放:通过时间戳防止请求被重复使用
- 身份验证:验证调用方的合法性
二、签名校验流程设计
在我们的实现中,签名校验流程如下:
- 客户端准备请求参数
- 客户端按照约定规则生成签名
- 客户端将签名和必要参数放入HTTP头
- 服务端拦截器校验签名有效性
- 服务端根据校验结果决定是否继续处理请求
三、核心代码实现
1. 拦截器配置
首先,我们需要配置一个拦截器来拦截需要进行签名校验的请求:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private AppValidInterceptor appValidInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(appValidInterceptor)
.addPathPatterns("/test123"); // 指定需要拦截的路径
}
}
2. 签名校验拦截器
这是签名校验的核心实现:
@Slf4j
@Component
public class AppValidInterceptor implements HandlerInterceptor {
@Resource
private MessageSource messageSource;
@Resource
private EncryptionUtils encryptionUtils;
@Override
public 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;
@PostConstruct
public 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"
);
// 设备相关不需要校验的API
List<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安全实践有所帮助!