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

}

 








以上是关于springcloud提供开放api接口签名验证的主要内容,如果未能解决你的问题,请参考以下文章

开放api接口签名验证

开放api接口签名验证

开放 API 接口签名验证,让你的接口从此不再裸奔

开放API接口签名验证,让你的接口从此不再裸奔

TOKEN+签名验证

App开放接口api安全性—Token签名sign的设计与实现