springcloud提供开放api接口签名验证
Posted 不负前行
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springcloud提供开放api接口签名验证相关的知识,希望对你有一定的参考价值。
一、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; } }
以上是关于springcloud提供开放api接口签名验证的主要内容,如果未能解决你的问题,请参考以下文章