项目场景:
开放接口是指不需要登录凭证就允许被第三方系统调用的接口。为了防止开放接口被恶意调用,开放接口一般都需要验签才能被调用。
在Spring Boot中实现接口校验签名通常是为了保证接口请求的安全性和数据的完整性。签名校验通常涉及对请求参数的签名计算和验证,以确保请求是由可信的发送方发送,并且在传输过程中没有被篡改。下面,我将详细介绍如何在Spring Boot应用中实现接口校验签名的过程。
解决方案:
1.配置签名密钥
签名密钥是用于生成和验证签名的秘密信息,生成规则自己定义,需要把生成的密钥提供给第三方。
比如:
appId:360aa3a3ba074da6a7bb17ae55e72d26
appSecret:81343DC5-6E80-483A-A427-E3DF5FA4E5F3
/**
* 生成应用id和密钥
*/
@RequestMapping(value = "/getSecret", method = RequestMethod.GET)
public Map<String, String> getSecret() {
//生成应用id和密钥提供给第三方使用,具体生成规则自己定
String appId = UUID.randomUUID().toString().replace("-", "").toLowerCase();
String appSecret = UUID.randomUUID().toString().toUpperCase();
System.out.println("appId:"+appId);
System.out.println("appSecret:"+appSecret);
Map<String, String> map = new HashMap<>();
map.put("appId", appId);
map.put("appSecret", appSecret);
return map;
}
2.定义签名算法
一个用于生成签名,另一个用于验证签名。生成签名的方法通常将请求参数按照特定规则计算出一个签名值。常见的签名算法有HMAC-SHA1、HMAC-SHA256等。
验证签名的方法则是对接收到的请求参数进行同样的处理,并计算出一个签名值,然后与请求中携带的签名值进行比对。
package com.test.utils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class Signature {
/**
* 获取签名
* @param secretKey 密钥
* @param data 需要签名的数据
* @return 签名
*/
public static String signWithHmacSha1(String secretKey, String data) {
try {
SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(signingKey);
return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes("UTF-8")));
} catch (NoSuchAlgorithmException | InvalidKeyException | UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
/**
* 验证签名
* @param secretKey 密钥
* @param data 需要签名的数据
* @param hmac 已经签名的数据
* @return true:签名一致
*/
public static boolean verify(String secretKey, String data, String hmac) {
String calculatedHmac = signWithHmacSha1(secretKey, data);
return calculatedHmac.equals(hmac);
}
}
3.拦截器或过滤器实现
使用Spring的拦截器(Interceptor)或过滤器(Filter)来实现对接口请求的签名校验。在拦截器或过滤器中,你可以获取到请求的参数,并调用签名验证方法来校验签名的有效性。
package com.test.aop;
import com.test.utils.Signature;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 签名拦截器
*/
@Component
@Slf4j
public class SignInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
//分配的应用id
String appId = request.getHeader("appId");
//时间戳
String timestampStr = request.getHeader("timestamp");
//签名
String signature = request.getHeader("signature");
if(StringUtils.isBlank(appId) || StringUtils.isBlank(timestampStr) || StringUtils.isBlank(signature)){
response.setStatus(500);
response.getWriter().println("参数错误!");
return false;
}
//这个密钥实际应该根据appId到数据库里查出来
String appSecret = "81343DC5-6E80-483A-A427-E3DF5FA4E5F3";
//如果密钥没查到
// if(flag){
// response.setStatus(500);
// response.getWriter().println("密钥不存在!");
// }
//拼接数据
String origin = appId + "\n" + appSecret + "\n" + timestampStr;
if(!Signature.verify(appSecret, origin, signature)){
response.setStatus(500);
response.getWriter().println("签名错误!");
return false;
}
//业务的时间戳
long timestamp = Long.parseLong(timestampStr);
//当前时间戳
long currentTimestamp = System.currentTimeMillis() / 1000;
//10分钟内有效
long timeDifference = 10 * 60;
if(Math.abs(timestamp - currentTimestamp) > timeDifference){
response.setStatus(500);
response.getWriter().println("签名过期!");
return false;
}
//放行
return true;
}
}
配置拦截器
package com.test.config;
import com.test.aop.SignInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
/**
* WebMvc配置
*/
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Resource
private SignInterceptor signInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//不拦截的地址
List<String> excludedList = new ArrayList<>();
//swagger地址
excludedList.add("/swagger-ui.html");
excludedList.add("/swagger-ui.html/**");
excludedList.add("/webjars/**");
excludedList.add("/swagger/**");
excludedList.add("/doc.html");
excludedList.add("/doc.html/**");
excludedList.add("/swagger-resources/**");
excludedList.add("/v2/**");
excludedList.add("/favicon.ico");
//生成应用id和密钥接口不拦截
excludedList.add("/getSecret");
registry.addInterceptor(signInterceptor)
.addPathPatterns("/**")//拦截所有请求
.excludePathPatterns(excludedList);//排除的请求
super.addInterceptors(registry);
}
}
4.测试接口
controller定义一个测试接口
/**
* 测试签名
*/
@RequestMapping(value = "/sign", method = RequestMethod.GET)
public String sign() {
return "success";
}
模拟第三方调用:
我们需要把接口请求头所需参数和签名的方法告知第三方。
接口请求头参数如下:
参数名称 | 中文 | 参数值 |
---|---|---|
appId | 应用id | 360aa3a3ba074da6a7bb17ae55e72d26 |
timestamp | 当前时间戳,精确到秒 | 1713838208 |
signature | 签名 | bjvXebFiHi2+I93BNs+8+Tl2I7k= |
签名方法:
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class Signature {
/**
* 获取签名
* @param secretKey 密钥
* @param data 需要签名的数据
* @return 签名
*/
public static String signWithHmacSha1(String secretKey, String data) {
try {
SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(signingKey);
return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes("UTF-8")));
} catch (NoSuchAlgorithmException | InvalidKeyException | UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
}
生成签名示例:
public static void main(String[] args) {
//这里的应用id和密钥已实际分配的为准
String appId = "360aa3a3ba074da6a7bb17ae55e72d26";
String appSecret = "81343DC5-6E80-483A-A427-E3DF5FA4E5F3";
//当前时间戳,精确到秒,示例:1713838208
long timestamp = System.currentTimeMillis() / 1000;
//拼接数据:appId、appSecret、timestamp
String origin = appId + "\n" + appSecret + "\n" + timestamp;
String signature = Signature.signWithHmacSha1(appSecret, origin);
//需要加到请求头的参数
System.out.println("appId:"+appId);
System.out.println("timestamp:"+timestamp);
System.out.println("signature:"+signature);
}
当第三方知道接口请求头所需参数和签名的方法后,就可以调用接口了
curl调用:
curl -X GET \
http://localhost:8080/testservice/test/sign \
-H 'appId: 360aa3a3ba074da6a7bb17ae55e72d26' \
-H 'signature: bjvXebFiHi2+I93BNs+8+Tl2I7k=' \
-H 'timestamp: 1713843128'
前端js生成签名:
在JavaScript中,可以使用crypto-js库来实现HMAC-SHA1签名。
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js" type="text/javascript"></script>
<title>HmacSHA1 签名</title>
<script type="text/javascript">
function encryptor() {
var appId = '360aa3a3ba074da6a7bb17ae55e72d26';
var appSecret = '81343DC5-6E80-483A-A427-E3DF5FA4E5F3';
var timestamp = parseInt((new Date()).getTime()/1000);
var converttext = appId + "\n" + appSecret + "\n" + timestamp;
var plaintext = $("#plaintext").val(converttext);
var ciphetext = CryptoJS.HmacSHA1(
CryptoJS.enc.Utf8.parse(converttext),
CryptoJS.enc.Utf8.parse(appSecret)
).toString(CryptoJS.enc.Base64);
$("#ciphetext").val(ciphetext);
}
</script>
<style>
h, div {
margin: 10px 10px;
}
input[type=text] {
width: 500px;
}
input[type=button] {
margin: 0px 5px;
padding: 10px;
}
select {
width: 250px;
}
textarea {
width: 500px;
height: 150px;
}
</style>
</head>
<body>
<div>
<div>
<h>HmacSHA1 签名</h>
</div>
<div>
<input type="button" value="生成签名" onclick="encryptor()" />
</div>
<div>
<label for="plaintext">明文:</label>
<br />
<textarea id="plaintext" readonly="readonly" ></textarea>
</div>
<div>
<label for="ciphetext">签名结果:</label>
<br />
<textarea id="ciphetext" readonly="readonly" ></textarea>
</div>
</div>
</body>
</html>
总结
- 这里签名由appId + "\n" + appSecret + "\n" + timestamp,生成签名字串
- 时间戳用于保证签名的有效性,即使签名被盗用,也只能在有效时间内使用
- appId、appSecret自己定义生成规则,保存到数据库中