1、场景
由于项目需要开发第三方接口给多个供应商,为保证Api接口的安全性,遂采用Api接口签名验证。
2、接口防御措施
- 请求发起时间得在限制范围内
- 请求的用户是否真实存在
- 是否存在重复请求
- 请求参数是否被篡改
3、签名认证逻辑
1、服务端生成一对 a***essKey/secretKey密钥对,将 a***essKey公开给客户端,将 secretKey 保密。
2、客户端使用 secretKey和一些请求参数(如时间戳、请求内容等),使用 MD5 算法生成签名。
3、客户端将 a***essKey、签名和请求参数一起发送给服务端。
4、服务端使用 和收到的请求参数,使用 MD5 算法生成签名。
5、服务端比较客户端发来的签名和自己生成的签名是否相同,如果相同,则认为请求是可信的,否则认为请求是不可信的。
secretKey不进行网络传输,只用于本地MD5运算
4、签名算法规则
计算步骤
用于计算签名的参数在不同接口之间会有差异,但算法过程固定如下4个步骤。
将<key, value>请求参数对按key进行字典升序排序,得到有序的参数对列表N
将列表N中的参数对按URL键值对的格式拼接成字符串,得到字符串T(如:key1=value1&key2=value2),URL键值拼接过程value部分需要URL编码,URL编码算法用大写字母,例如%E8,而不是小写%e8
将应用密钥以app_key为键名,组成URL键值拼接到字符串T末尾,得到字符串S(如:key1=value1&key2=value2&app_key=密钥)
对字符串S进行MD5运算,将得到的MD5值所有字符转换成大写,得到接口请求签名
注意事项
不同接口要求的参数对不一样,计算签名使用的参数对也不一样
参数名区分大小写,参数值为空不参与签名
URL键值拼接过程value部分需要URL编码
5、代码示例
1、sign工具类
java">public class SignUtil {
/**
* 签名算法
* 1. 计算步骤
* 用于计算签名的参数在不同接口之间会有差异,但算法过程固定如下4个步骤。
* 将<key, value>请求参数对按key进行字典升序排序,得到有序的参数对列表N
* 将列表N中的参数对按URL键值对的格式拼接成字符串,得到字符串T(如:key1=value1&key2=value2),URL键值拼接过程value部分需要URL编码,URL编码算法用大写字母,例如%E8,而不是小写%e8
* 将应用密钥以app_key为键名,组成URL键值拼接到字符串T末尾,得到字符串S(如:key1=value1&key2=value2&app_key=密钥)
* 对字符串S进行MD5运算,将得到的MD5值所有字符转换成大写,得到接口请求签名
* 2. 注意事项
* 不同接口要求的参数对不一样,计算签名使用的参数对也不一样
* 参数名区分大小写,参数值为空不参与签名
* URL键值拼接过程value部分需要URL编码
* @return 签名字符串
*/
private static String getSign(Map<String, Object> map, String secretKey) {
List<Map.Entry<String, Object>> infoIds = new ArrayList<>(map.entrySet());
Collections.sort(infoIds, new ***parator<Map.Entry<String, Object>>() {
public int ***pare(Map.Entry<String, Object> arg0, Map.Entry<String, Object> arg1) {
return (arg0.getKey()).***pareTo(arg1.getKey());
}
});
StringBuffer sb = new StringBuffer();
for (Map.Entry<String, Object> m : infoIds) {
if(null == m.getValue() || StringUtils.isNotBlank(m.getValue().toString())){
sb.append(m.getKey()).append("=").append(URLUtil.encodeAll(m.getValue().toString())).append("&");
}
}
sb.append("secret-key=").append(secretKey);
return MD5.create().digestHex(sb.toString()).toUpperCase();
}
//获取随机值
private static String getNonceStr(int length){
//生成随机字符串
String str="zxcvbnmlkjhgfdsaqwertyuiopQWERTYUIOPASDFGHJKLZXCVBNM1234567890";
Random random=new Random();
StringBuffer randomStr=new StringBuffer();
// 设置生成字符串的长度,用于循环
for(int i=0; i<length; ++i){
//从62个的数字或字母中选择
int number=random.nextInt(62);
//将产生的数字通过length次承载到sb中
randomStr.append(str.charAt(number));
}
return randomStr.toString();
}
//签名验证方法
public static boolean signValidate(Map<String, Object> map,String secretKey,String sign){
String mySign = getSign(map,secretKey);
return mySign.equals(sign);
}
}
2、定义拦截器
@Configuration
public class SignInterceptorConfig implements WebMv***onfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(signInterceptor())
.addPathPatterns("/openapi/**");//只拦截openapi前缀的接口
}
//交给spring管理 SignInterceptor bean
//不然下边 private OpenApiApplyMapper applyMapper;注入为null
@Bean
public SignInterceptor signInterceptor(){
return new SignInterceptor();
}
}
3、生成a***essKey、secretKey 工具类
public class KeyGenerator {
private static final int KEY_LENGTH = 32; // 指定生成的key长度为32字节
public static String generateA***essKey() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[KEY_LENGTH / 2]; // 生成的字节数要除以2
random.nextBytes(bytes);
return Base64.getEncoder().encodeToString(bytes).replace("/", "").replace("+", "").substring(0, 20);
}
public static String generateSecretKey() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[KEY_LENGTH];
random.nextBytes(bytes);
return Base64.getEncoder().encodeToString(bytes).replace("/", "").replace("+", "").substring(0, 40);
}
}
4、signInterceptor类
public class SignInterceptor implements HandlerInterceptor {
private static final String A***ESSKEY = "a***ess-key";//调用者身份唯一标识
private static final String TIMESTAMP = "time-stamp";//时间戳
private static final String SIGN = "sign";//签名
private static final String NONCE = "nonce";//随机值
@Resource
private OpenApiApplyMapper applyMapper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(checkSign(request, response)){//签名认证
return HandlerInterceptor.super.preHandle(request, response, handler);
}
return false;
}
/**
* 验证签名
* @param request
* @param response
* @return
* @throws Exception
*/
private boolean checkSign(HttpServletRequest request,HttpServletResponse response)throws Exception {
response.setContentType("application/json");
response.setCharacterEncoding("utf8");
String ip = IPUtils.getIpAddr(request);
FzyLogUtil.infoSafe("开放接口", "访问时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL());
String a***essKey = request.getHeader(A***ESSKEY);
String timestamp = request.getHeader(TIMESTAMP);
String nonce = request.getHeader(NONCE);
String sign = request.getHeader(SIGN);
if (!StringUtils.isNotBlank(a***essKey)) {
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("a***essKey无效")));
FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:a***essKey无效");
return false;
}
if (StringUtils.isBlank(sign)) {
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("签名无效")));
FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:签名无效");
return false;
}
OpenApiDetailDO openApiDetailDO = applyMapper.selectOneByA***essKey(a***essKey);
if (openApiDetailDO == null) {
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("a***essKey不存在")));
FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:a***essKey不存在");
return false;
}
if (StringUtils.isNotBlank(openApiDetailDO.getBlackList())) {
for (String bIp : openApiDetailDO.getBlackList().split(",")) {
if (bIp.equals(ip)) {//黑名单
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("拒绝请求")));
FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:黑名单拒绝请求");
return false;
}
}
}
if (StringUtils.isNotBlank(openApiDetailDO.getWhiteList())) {
boolean flag = false;
for (String bIp : openApiDetailDO.getWhiteList().split(",")) {
if (bIp.equals(ip)) {//白名单
flag = true;
break;
}
}
if(!flag){
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("拒绝请求")));
FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:白名单未符合拒绝请求");
return false;
}
}
if ("0".equals(openApiDetailDO.getInvokeStatus() + "")) {
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("访问权限已被冻结")));
FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:访问权限已被冻结");
return false;
}
if (!"1".equals(openApiDetailDO.getApiStatus() + "")) {
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("接口异常,暂停访问")));
FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:接口异常,暂停访问");
return false;
}
if (!StringUtils.isNotBlank(timestamp)) {
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("时间戳无效")));
FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:时间戳无效");
return false;
} else if (openApiDetailDO.getTimeOut() != null) {
if (System.currentTimeMillis() - Long.valueOf(timestamp) > openApiDetailDO.getTimeOut() * 1000) {
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("请求已过期")));
FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:请求已过期");
return false;
}
;
}
Map<String, Object> hashMap = new HashMap<>();
String queryStrings = request.getQueryString();//获取url后边拼接的参数
if (queryStrings != null) {
for (String queryString : queryStrings.split("&")) {
String[] param = queryString.split("=");
if (param.length == 2) {
hashMap.put(param[0], param[1]);
}
}
}
hashMap.put(A***ESSKEY, a***essKey);
hashMap.put(TIMESTAMP, timestamp);
if (StringUtils.isNotBlank(nonce)) {
hashMap.put(NONCE, nonce);
}
String secretKey = openApiDetailDO.getSecretKey();
String body = new RequestWrapper(request).getBody();
if (StringUtils.isNotBlank(body)) {
Map<String, Object> map = JSON.parseObject(body);
if (map != null) {
hashMap.putAll(map);
}
}
if (!SignUtil.signValidate(hashMap, secretKey, sign)) {//认证失败
response.getWriter().write(JSON.toJSONString(ResultUtil.fail("认证失败")));
FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:认证失败");
return false;
}
return true;
}
}
5、SignInterceptor 获取body里参数后,接口的controller会获取不到body的参数了,会报错
通过过滤器解决
@***ponent
@WebFilter(filterName = "HttpServletRequestFilter", urlPatterns = "/")
@Order(10000)
public class HttpServletRequestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String contentType = request.getContentType();
String method = "multipart/form-data";
if (contentType != null && contentType.contains(method)) {
// 将转化后的 request 放入过滤链中
request = new StandardServletMultipartResolver().resolveMultipart(request);
}
request = new RequestWrapper((HttpServletRequest) servletRequest);
//获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新request对象中
// 在chain.doFiler方法中传递新的request对象
if(null == request) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
filterChain.doFilter(request, servletResponse);
}
}
@Override
public void destroy() {
}
}