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&timestamp=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;
    }

}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM