- 一、定义
- 二、签名
- 三、相应工具类
- 四、测试get请求,参数写url上
- 五、post请求,参数放入body中
- 六、使用过滤器配置接口防篡改
- 一、相关工具类
- 二、测试
一、定义
在客户端与服务端请求交互的过程中,请求的数据容易被拦截并篡改,比如在支付场景中,请求支付金额为 10 元,被拦截后篡改为 100 元,由于没有防篡改校验,导致多支付了金钱,造成了用户损失。因此我们在接口设计时必须考虑防篡改校验,加签、验签就是用来解决这个问题的。划重点,敲黑板:加签、验签是用来解决防篡改问题的。
签名主要包含摘要和非对称加密两部分内容,首先对需要签名的数据进行摘要计算得到摘要值,然后通过签名者的私钥对摘要值进行非对称加密即可得到签名结果。
验签主要包含摘要、非对称解密、摘要比对三部分内容,首页对接收到的数据进行摘要计算得到验签方摘要值,然后通过签名者的公钥对摘要值进行非对称解密得到签名方摘要值,将签名方摘要值与验签方摘要值进行比对,如果相等则验签成功,否则验签失败。
二、签名1、参数排序
将需要签名的内容根据参数名称进行排序,排序规则按照第一个字符的ASCII码值递增排序(字母升序排序),如果遇到相同字符则按照第二个字符的ASCII码递增排序,以此类推。将参数内容进行排序,可以保证签名、验签双方参数内容的一致性。
为什么会产生不一致?
签名方以 Json 格式将参数内容发送给验签方,验签方需要将 Json 格式的参数内容反序列化为对象,由于验签方可能使用不同的编程语言,不同的 Json 框架,所以会导致双方的参数顺序不一致。
2、参数拼接
将排序后的参数与其对应值,组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来,此时生成的字符串为待摘要字符串。
3、摘要计算
通过摘要算法求待摘要字符串的摘要值,常用的摘要算法如MD5、SHA、HMAC等。
4、非对称加密
使用非非对称加密算法,利用客户端的私钥对摘要值进行加密,生成内容我们称之为签名。
5、发送请求
将参数内容、字符编码、签名方法(非对称加密算法)、签名发送给验签方。
验签
验签方收到请求后进行验签。
1、SHA256Util加密算法工具类:
public class SHA256Util {
public static String getSHA256String(String str) {
String encodeStr = "";
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(str.getBytes("UTF-8"));
encodeStr = byte2Hex(messageDigest.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return encodeStr;
}
private static String byte2Hex(byte[] bytes) {
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
String temp = Integer.toHexString(bytes[i] & 0xFF);
if (temp.length() == 1) {
stringBuffer.append("0");
}
stringBuffer.append(temp);
}
return stringBuffer.toString();
}
}
2、生产签名工具类
public class SignUtil {
private static String secret = "e10adc3949ba59abbe56e057f20f883f";
public static String generatorSign(Map map) {
map.remove("sign");
//排序
Map stringObjectMap = MapSortUtil.sortMapByKey(map);
//转格式
Set> entries = stringObjectMap.entrySet();
//存放StringBuilder
StringBuilder sb = new StringBuilder();
//遍历
for (Map.Entry entry : entries) {
sb.append(entry.getKey() + ":" + entry.getValue()).append("&");
}
//组装secret
sb.append("secret").append(secret);
//生产签名
return SHA256Util.getSHA256String(sb.toString());
}
public static Boolean checkSign(Map map){
String sign = (String) map.get("sign");
map.remove("sign");
//生产Sign
String signGenera = generatorSign(map);
//校验Sign
if (signGenera.equals(sign)){
return true;
}
return false;
}
}
3、Map排序工具类
public class MapSortUtil {
public static Map sortMapByKey(Map map) {
//判断是否为空
if (ObjectUtils.isEmpty(map)) {
throw new RuntimeException("输入参数为空");
}
//排序
Map sortMap = new TreeMap<>(new MyMapComparator());
sortMap.putAll(map);
return sortMap;
}
static class MyMapComparator implements Comparator {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
}
}
四、测试get请求,参数写url上
1、测试内容,这里简单测试两个appId以及name
public static void main(String[] args) {
HashMap map = new HashMap<>();
map.put("appId", 1);
map.put("name", "jowell");
String s = generatorSign(map);
System.out.println("s = " + s);
}
2、controller代码
@GetMapping("/getTest")
public String getTest(String sign,HttpServletRequest request){
HashMap map = new HashMap<>();
// 获取get中的参数
Enumeration parameterNames = request.getParameterNames();
while (parameterNames.hasMoreElements()){
//获取name
String parametename = parameterNames.nextElement();
// 获取值
String parameterValue = request.getParameter(parametename);
map.put(parametename,parameterValue);
}
//排序
Map map1 = MapSortUtil.sortMapByKey(map);
//生产签名
String sign1 = SignUtil.generatorSign(map1);
//判断签名
if (sign.equals(sign1)){
return "success";
}
return "error";
}
3、启动项目测试效果
如下图测试成功:
我们把appId内容改为2,可以可以看到请求接口失败,无论是改了内容还是改了签名,都请求不成功,这样就防止了第三方而已者篡改接口内容
测试内容同上
1、把请求参数封装为实体类
@Data
public class SignDTO {
private String appId;
private String name;
private String sign;
}
2、controller
@PostMapping("/postTest")
public String postTest(@RequestBody SignDTO signDTO) {
//JSON转对象
JSONObject jsonObject = JSONUtil.parseObj(signDTO);
//转Map
Map map = Convert.toMap(String.class, Object.class, jsonObject);
//排序
Map map1 = MapSortUtil.sortMapByKey(map);
System.out.println("map1 = " + map1);
//生成
String sign = SignUtil.generatorSign(map1);
//判断签名
if (sign.equals(signDTO.getSign())){
return "校验通过";
}
return "校验失败";
}
3、启动项目测试
六、使用过滤器配置接口防篡改 一、相关工具类但通过上面代码可以看到,代码非常冗余,每次写一次controller都得写签名校验,下面把签名验证改为统一过滤器。
1、Sign过滤器类
@Slf4j
@Component
public class SignAuthFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//ServletRequest转HttpServletRequest
HttpServletRequest req = (HttpServletRequest) servletRequest;
//获取请求路径
final String uri = req.getRequestURI().startsWith("/") ? req.getRequestURI().substring(1) : req.getRequestURI();
//对以下请求放行
if(uri.contains("user/getCaptcha")){
filterChain.doFilter(servletRequest, servletResponse);
return;
}
//转HttpServletRequest以及HttpServletResponse
HttpServletRequest request = new BodyReaderHttpServletRequestWrapper((HttpServletRequest) servletRequest);
HttpServletResponse response = (HttpServletResponse) servletResponse;
//获取请求参数工具类,包括是get或post
SortedMap allParams = HttpParamUtil.getAllParams(request);
log.info("所有请求参数:{}", allParams);
//校验签名
Boolean flag = SignUtil.checkSign(allParams);
if (flag){
filterChain.doFilter(request, response);
}else {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
JSONObject jsonObject = new JSONObject();
jsonObject.put("msg","签名不正确");
jsonObject.put("code",-1);
writer.println(jsonObject);
}
}
@Override
public void destroy() {
}
}
2、保存过滤器里面的流工具类
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
String sessionStream = getBodyString(request);
body = sessionStream.getBytes(Charset.forName("UTF-8"));
}
public String getBodyString(final ServletRequest request) {
StringBuilder sb = new StringBuilder();
try (
InputStream inputStream = cloneInputStream(request.getInputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")))
) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
public InputStream cloneInputStream(ServletInputStream inputStream) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
try {
while ((len = inputStream.read(buffer)) > -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
byteArrayOutputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
return new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
3、获取请求参数工具类,不管是get或post
public class HttpParamUtil {
public static SortedMap getAllParams(HttpServletRequest request) throws IOException {
//总的参数map
SortedMap allMap = new TreeMap<>();
//获取URL上的参数
if (StringUtils.isNotEmpty(request.getQueryString())) {
Map urlParams = getUrlParams(request);
//遍历URL上的参数
for (Map.Entry entry : urlParams.entrySet()) {
allMap.put((String) entry.getKey(), entry.getValue());
}
}
//获取Body上的参数
Map bodyParams = getBodyParams(request);
if (ObjectUtils.isNotEmpty(bodyParams)) {
//遍历Body上的参数
for (Map.Entry entry : bodyParams.entrySet()) {
allMap.put((String) entry.getKey(), entry.getValue());
}
}
return allMap;
}
private static Map getBodyParams(HttpServletRequest request) throws IOException {
//读取Body中的参数
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(request.getInputStream()));
StringBuilder sb = new StringBuilder();
String s = "";
while (null != (s = bufferedReader.readLine())) {
sb.append(s);
}
//转Map
return JSONObject.parseObject(sb.toString(), Map.class);
}
private static Map getUrlParams(HttpServletRequest request) {
String queryParam = "";
try {
//查询URL上的请求参数
queryParam = URLDecoder.decode(request.getQueryString(), "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
HashMap result = new HashMap<>();
//分隔:如//hello?a=1&b=2 分隔&
String[] split = queryParam.split("&");
//遍历
for (String s : split) {
int i = s.indexOf("=");
result.put(s.substring(0, i), s.substring(i + 1));
}
return result;
}
}
二、测试
1、controller代码,如下可以看到代码清晰了很多,只关注业务代码即可
@GetMapping("/getTest")
public String getTest(String sign, HttpServletRequest request) {
System.out.println("进入get请求,参数写url上方法");
return "getTest";
}
@PostMapping("/postTest")
public String postTest(@RequestBody SignDTO signDTO) {
System.out.println("进入post请求,参数放入body中方法");
return "postTest";
}
2、测试
如下可以看到,只要我修改了内容就验证不通过,就判定接口是被第三方恶意篡改过的
get请求测试同上,自行测试



