一、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; } }