一、MD5参数签名的方式
我们对api查询产品接口进行优化:
1.给app分配对应的key、secret
2.Sign签名,调用API 时需要对请求参数进行签名验证,签名方式如下:
a. 按照请求参数名称将所有请求参数按照字母先后顺序排序得到:keyvaluekeyvalue...keyvalue 字符串如:将arong=1,mrong=2,crong=3 排序为:arong=1, crong=3,mrong=2 然后将参数名和参数值进行拼接得到参数字符串:arong1crong3mrong2。
b. 将secret加在参数字符串的头部后进行MD5加密 ,加密后的字符串需大写。即得到签名Sign
新api接口代码:
app调用:http://api.test.com/getproducts?key=app_key&sign=BCC7C71CF93F9CDBDB88671B701D8A35&参数1=value1&参数2=value2.......
注:secret 仅作加密使用, 为了保证数据安全请不要在请求参数中使用。
如上,优化后的请求多了key和sign参数,这样请求的时候就需要合法的key和正确签名sign才可以获取产品数据。这样就解决了身份验证和防止参数篡改问题,如果请求参数被人拿走,没事,他们永远也拿不到secret,因为secret是不传递的。再也无法伪造合法的请求。
但是...这样就够了吗?细心的同学可能会发现,如果我获取了你完整的链接,一直使用你的key和sign和一样的参数不就可以正常获取数据了...-_-!是的,仅仅是如上的优化是不够的
请求的唯一性:
为了防止别人重复使用请求参数问题,我们需要保证请求的唯一性,就是对应请求只能使用一次,这样就算别人拿走了请求的完整链接也是无效的。
唯一性的实现:在如上的请求参数中,我们加入时间戳 :timestamp(yyyyMMddHHmmss),同样,时间戳作为请求参数之一,也加入sign算法中进行加密。
新的api接口:
app调用:
http://api.test.com/getproducts?key=app_key&sign=BCC7C71CF93F9CDBDB88671B701D8A35×tamp=201603261407&参数1=value1&参数2=value2.......
如上,我们通过timestamp时间戳用来验证请求是否过期。这样就算被人拿走完整的请求链接也是无效的。
下面代码包含key screct生成,zuulfilter拦截校验代码。
package com.idoipo.common.message.user; /**
* 数字签名签名模型
* Create by liping on 2019/1/9
*/
public class SignModel { //加密key
private String appKey;
//加密密钥
private String appSecret; public String getAppKey() {
return appKey;
} public void setAppKey(String appKey) {
this.appKey = appKey;
} public String getAppSecret() {
return appSecret;
} public void setAppSecret(String appSecret) {
this.appSecret = appSecret;
} @Override
public String toString() {
return "SignModel{" +
"appKey='" + appKey + '\'' +
", appSecret='" + appSecret + '\'' +
'}';
}
}
package com.idoipo.common.util; import java.util.Stack; /**
* Create by liping on 2019/1/9
*/
public class DecimalChange {
/**
* @return
* @version 1.0.0
* @Description 10进制转N进制
*/
public static String getDecimal(Long num, int base) {
StringBuffer sb = new StringBuffer();
String all = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
String digths = all.substring(0, base);//将要转换的进制字母对应表
//只能装字符型的栈
Stack s = new Stack();
while (num != 0) {
// digths.charAt(n % base) 返回指定索引处的值
Long bb = num % base;
s.push(digths.charAt(bb.intValue()));
num = num /base;
}
while (!s.isEmpty()) {
sb.append(s.pop());
}
return sb.toString();
} }
package com.idoipo.common.util; import com.idoipo.common.exception.MD5UtilException; import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; /**
* Created by liping on 2018-08-10.
*/
public class MD5Util { public static String md5(String content) throws MD5UtilException {
StringBuffer sb = new StringBuffer();
try{
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(content.getBytes("UTF-8"));
byte[] tmpFolder = md5.digest(); for (byte aTmpFolder : tmpFolder) {
sb.append(Integer.toString((aTmpFolder & 0xff) + 0x100, 16).substring(1));
} return sb.toString();
}catch(NoSuchAlgorithmException ex){
throw new MD5UtilException("无法生成指定内容的MD5签名", ex);
}catch(UnsupportedEncodingException ex){
throw new MD5UtilException("无法生成指定内容的MD5签名", ex);
}
} }
package com.idoipo.common.util; import com.idoipo.common.message.user.SignModel; import java.util.Date;
import java.util.Random; /**
* Create by liping on 2019/1/9
*/
public class AppKeyGenerate { private final static String product = "test_";
private static SignModel signModel = new SignModel();
/**
* 随机生成产品名+时间戳+1000以内随机数+16进制表示
* @return
*/
private static String getAppKey() {
Date date = new Date();
long timestamp= date.getTime();
Random random = new Random();
int randomInt1 = random.nextInt(1000);
int randomInt2 = random.nextInt(1000);
long randNum = timestamp + randomInt1 + randomInt2;
String app_key = product + DecimalChange.getDecimal(randNum,16);
return app_key;
} /**
* 根据md5加密
*
* @return
*/
public static String appSecret(String app_key) {
String mw = product + app_key;
String app_sign = MD5Util.md5(mw).toUpperCase();// 得到以后还要用MD5加密。
return app_sign;
} public static SignModel getKeySecret() {
String appKey = getAppKey();
String appSecret = appSecret(appKey);
signModel.setAppKey(appKey);
signModel.setAppSecret(appSecret);
return signModel;
} public static void main(String[] args) {
SignModel signModel = AppKeyGenerate.getKeySecret();
System.out.println(signModel);
} }
下面是过滤器拦截所有请求,只支持post
package com.idoipo.infras.gateway.api.filters.pre; import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.idoipo.common.data.web.MVCResultMsg;
import com.idoipo.common.data.web.ResultCode;
import com.idoipo.common.util.AppKeyGenerate;
import com.idoipo.common.util.MD5Util;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap; /**
* 第三方调用参数非法检验
*/
@Component
@SuppressWarnings("unused")
public class IllegalCheckPreFilter extends ZuulFilter {
private Logger logger = LoggerFactory.getLogger(IllegalCheckPreFilter.class); @Value("${com.idoipo.requestExpire}")
private Long requestExpire; @Override
public String filterType() {
return FilterConstants.PRE_TYPE;
} @Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 4;
} @Override
public boolean shouldFilter() {
return true;
} //需要修正返回的http状态码,目前的设置无效,将setSendZuulResponse设置为false时,即可采用自定义的状态码
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
MVCResultMsg msg = new MVCResultMsg();
InputStream in;
try {
in = request.getInputStream(); String method = request.getMethod();
String interfaceMethod = request.getServletPath();
//logger.info("请求方法method={},url={}",method,interfaceMethod)
String reqBody = StreamUtils.copyToString(in, Charset.forName("UTF-8"));
if (!"POST".equals(method.toUpperCase())) {
msg.setCode(ResultCode.NOT_SUPPORT_REQUEST.getCode());
msg.setMsg(ResultCode.NOT_SUPPORT_REQUEST.getDesc());
errorMessage(ctx, msg);
return null;
} //打印请求json参数
if (!StringUtils.isEmpty(reqBody)) {
String conType = request.getHeader("content-type");
if (conType.toLowerCase().contains("application/json")) {
//默认content-type传json-->application/json
Object invokeUserObject;
JSONObject jsonObject = JSONObject.parseObject(reqBody);
Object appKey = jsonObject.get("appKey");
Object sign = jsonObject.get("sign");
Object timestamp = jsonObject.get("timestamp");
//鉴权参数为空判断
if (StringUtils.isEmpty(appKey) || StringUtils.isEmpty(sign) || StringUtils.isEmpty(timestamp)) {
msg.setCode(ResultCode.AUTHENTICATION_PARAM_MISS.getCode());
msg.setMsg(ResultCode.AUTHENTICATION_PARAM_MISS.getDesc());
errorMessage(ctx, msg);
return null;
} else {
long times = Long.valueOf(timestamp.toString());
long expireTime = times + requestExpire * 60 * 1000;
long nowDate = new Date().getTime();
//请求超过指定时间就过期,不允许调用
if (nowDate < expireTime) {
msg.setCode(ResultCode.REQUEST_REPEAT.getCode());
msg.setMsg(ResultCode.REQUEST_REPEAT.getDesc());
errorMessage(ctx, msg);
return null;
}
//对比签名,用treeMap,定义字段排序
TreeMap treeMap = new TreeMap();
treeMap.putAll(jsonObject);
Iterator iterator = treeMap.entrySet().iterator();
StringBuilder stringBuilder = new StringBuilder();
String appSecret = AppKeyGenerate.appSecret(jsonObject.get("appKey").toString());
stringBuilder.append(appSecret);
while (iterator.hasNext()) {
Map.Entry entry = (Map.Entry) iterator.next();
// 获取key
String key = (String) entry.getKey();
if (key.equals("sign")) {
continue;
}
// 获取value
String value = (String) entry.getValue();
if (StringUtils.isEmpty(value)) {
continue;
}
stringBuilder.append(key).append(value);
} if (!sign.toString().equals(signGenerate(stringBuilder))) {
msg.setCode(ResultCode.SIGN_PARAM_TAMPER.getCode());
msg.setMsg(ResultCode.SIGN_PARAM_TAMPER.getDesc());
errorMessage(ctx, msg);
} else {
ctx.setSendZuulResponse(true); //将请求往后转发
ctx.setResponseStatusCode(200);
} }
} else {
//不支持的请求类型
msg.setCode(ResultCode.NOT_SUPPORT_TRANSPORT_TYPE.getCode());
msg.setMsg(ResultCode.NOT_SUPPORT_TRANSPORT_TYPE.getDesc());
errorMessage(ctx, msg);
return null;
}
}
} catch (Exception e) {
logger.error("参数转换流异常", e);
}
return null;
} private void errorMessage(RequestContext ctx, MVCResultMsg msg) {
logger.error("MVCResultMsg={}", msg);
ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
ctx.getResponse().setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
ctx.setResponseBody(new String(JSON.toJSONString(msg, SerializerFeature.WriteMapNullValue).getBytes(), Charset.forName("utf-8")));
//将结果立即返回,不再进一步操作
ctx.setSendZuulResponse(false);
} private String signGenerate(StringBuilder stringBuilder) {
String sign = MD5Util.md5(stringBuilder.toString()).toUpperCase();
return sign;
} }