需求背景
Springboot接口防刷机制:通过秘钥生成签名,校验请求源合法性,不同源可以设置不同的秘钥
业务场景:
- 可用于第三方业务系统回调接口,比如s2s场景下(Server端也可以利用ip白名单,不做签名校验也可以)
- 可用于一些App端接口发送请求校验(无token下)
代码演示
1. 项目目录结构:
2. 利用签名工具类:SignUtil.java
package com.md.demo.util.sign;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 签名工具类
*/
public class SignUtil {
protected static Logger logger = LoggerFactory.getLogger(SignUtil.class);
/**
* 算法实现 将参数集合按照参数名的ASCII排列 把排序后的结果按照【参数+参数值 +
* &】的方式拼接,再加上secretKey=secretKeyValue
* 拼装好的字符串按MD5(p1=v1&p2=v2&p3=v3&secretKey=secretKeyValue)进行md5加密后,转大写
*
* @param params 参数集合(必须)
* @param secretKey 秘钥(必须)
* @return
*/
public static String signByMD5(Map<String, Object> params, String secretKey) {
// 将参数集合按照参数名首字母先后顺序排列
SortedMap<String, Object> sortParamMap = SignUtil.sortMap(params);
// 把排序后的结果按照参数+参数值的方式拼接
// 拼装好的字符串按secretKey进行md5加密后,转大写
return SignUtil.createSign(sortParamMap, secretKey);
}
/**
* 把排序后的结果按照【参数+参数值 + &】的方式拼接,再加上secretKey=secretKeyValue
* 拼装好的字符串按MD5(p1=v1&p2=v2&p3=v3&secretKey=secretKeyValue)进行md5加密后,转大写
*
* @param parameters
* @param secretKey
* @return
*/
private static String createSign(Map<String, Object> parameters, String secretKey) {
StringBuffer sb = new StringBuffer();
Iterator<Entry<String, Object>> it = parameters.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Object> entry = (Map.Entry<String, Object>) it.next();
String key = (String) entry.getKey();
Object value = entry.getValue();
// 去掉带sign的项
if (null != value && !"".equals(value) && !"sign".equals(key) && !"secretKey".equals(key)) {
sb.append(key + "=" + value + "&");
}
}
sb.append("secretKey=" + secretKey);
// 注意sign转为大写
return MD5Util.encodeByMD5(sb.toString()).toUpperCase();
}
/**
* 按首字母排列
*
* @param map
* @return
*/
public static SortedMap<String, Object> sortMap(Map<String, Object> map) {
List<Map.Entry<String, Object>> infoIds = new ArrayList<Map.Entry<String, Object>>(map.entrySet());
// 排序
Collections.sort(infoIds, new Comparator<Map.Entry<String, Object>>() {
public int compare(Map.Entry<String, Object> o1, Map.Entry<String, Object> o2) {
// 按首字母比对
return (String.valueOf(o1.getKey().charAt(0))).compareTo(String.valueOf(o2.getKey().charAt(0)));
}
});
// 排序后
SortedMap<String, Object> sortmap = new TreeMap<String, Object>();
// 根据key进行排序ASCII顺序
System.out.println(infoIds.toString());
for (int i = 0; i < infoIds.size(); i++) {
String[] split = infoIds.get(i).toString().split("=");
if (split.length == 1) {
sortmap.put(split[0], null);
continue;
}
sortmap.put(split[0], split[1]);
}
return sortmap;
}
public static void main(String[] args) {
Map<String, Object> params = new HashMap<String, Object>();
params.put("name", "minbo");
params.put("age", 100);
String secretKey = "996";
String sign = SignUtil.signByMD5(params, secretKey);
System.out.println(sign);
}
}
注:详细用法讲解,见方法注释上的详细说明
3. InitRest.java文件
演示用法
package com.md.demo.rest;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.md.demo.util.JsonResult;
import com.md.demo.util.ResultCode;
import com.md.demo.util.sign.NetworkUtil;
import com.md.demo.util.sign.SignUtil;
/**
* @author Minbo
*/
@RestController
public class InitRest {
protected static Logger logger = LoggerFactory.getLogger(InitRest.class);
/**
* http://localhost:9090/hello
*
* @return
*/
@GetMapping("/hello")
public String hello() {
return "Hello greetings from spring-boot2-api-protect";
}
/**
* 利用秘钥生成签名(只有对方知,服务器知),校验请求源合法性,不同源可以设置不同的秘钥
*/
private static final String API_SECRET_KEY = "996";;
/**
* http://localhost:9090/test?name=minbo&age=100&sign=495FC6F52324AB1460C95A27803E3A4A
*
* @param name
* @param age
* @param sign 大写
* @return
*/
@GetMapping("/test")
public JsonResult test(String name, Integer age, String sign, HttpServletRequest request) {
// 1. 还可以在参数中增加一个动态随机字符参数,比如sId,每次请求时,对方都需要动态生成一个十位随机字符,防止sign值一直固化不变
// 2. 同时,服务器可以校验请求是否重复,比如可以通过redis存储已请求过的rId(可设置过期时间,以免一直存储历史的rId值),防止别人利用固定请求链接刷请求
// 3. 可以使用公网ip,限制同一个ip访问次数(也可以在nginx层做限制,这个自行网上了解了)
// // 获取公网ip
// String sIp = NetworkUtil.getIpAddress(request);
// System.out.println("sIp=" + sIp);
Map<String, Object> params = new HashMap<String, Object>();
params.put("name", name);
params.put("age", age);
String serverSign = SignUtil.signByMD5(params, API_SECRET_KEY);
if (serverSign.equals(sign)) {
return new JsonResult(ResultCode.SUCCESS, "签名通过");
}
return new JsonResult(ResultCode.SUCCESS_FAIL, "非法请求");
}
}
案例演示
访问接口:http://localhost:9090/test?name=minbo&age=100&sign=495FC6F52324AB1460C95A27803E3A4A
如果值在传输过程中有变动过,则会签名值失败:
策略逻辑
- 还可以在参数中增加一个动态随机字符参数,比如sId,每次请求时,对方都需要动态生成一个十位随机字符,防止sign值一直固化不变
- 同时,服务器可以校验请求是否重复,比如可以通过redis存储已请求过的rId,每请求一次就需要重新生成一个新的rId(可设置过期时间,以免一直存储历史的rId值),防止别人利用固定请求链接刷请求
- 可以使用公网ip,限制同一个ip访问次数(也可以在nginx层做限制,这个自行网上了解了)
完整源码下载
下一章教程
SpringBoot从入门到精通教程(二十)- 分布式锁用法(基于Redis实现)
该系列教程
至此,全部介绍就结束了
------------------------------------------------------
------------------------------------------------------
关于我(个人域名)
期望和大家一起学习,一起成长,共勉,O(∩_∩)O谢谢
欢迎交流问题,可加个人QQ 469580884,
或者,加我的群号 751925591,一起探讨交流问题
不讲虚的,只做实干家
Talk is cheap,show me the code