记录一个对外接口签名问题

场景:

一,本公司一个项目支付中心,需要和另外一个系统oracle进行交互

流程是:支付中心推送数据到oracle系统,oracle系统再推送返回结果到支付中心

前者是:调用oracle接口,webservice实现

后者是:支付中心对外暴露一个restful接口,供oracle调用

因为项目发布到公网,别人如果拿到url可参数就可以直接调用接口,存在风险

怎么解决:就是调用者需要签名,校验权限

方式1,

/**
 * 校验 Authorization
 * 前后端都生成
 *
 * @param request
 * @return
 */
private static final String BASICAUTH = "AN_JI:AJ_1113";

public static Boolean checkAuthorization(HttpServletRequest request) {
    String header = request.getHeader("Authorization");
    byte[] encodeBase = Base64.encodeBase64(BASICAUTH.getBytes());
    String authorization = new String(encodeBase);
    authorization = "Basic " + authorization;
    return authorization.equals(header);
}

具体实现为:和调用方(前端)约定一个值,BASICAUTH,第三方和我方约定对该值进行base64编码处理,第三方调用时把编码后的值传过来,放到请求头中,我方取到该值进行校验,有且一致,则调用,否则没有权限。

缺点:

试想如果黑客拿到这个编码后的值,直接传到后台,我方可以调用通过,就可以随便调用我们的接口了。

场景:

二,开放平台,三个角色,平台,服务提供方,第三方调用者

服务方把接口放到平台,第三方调用者接入平台,然后通过平台调用服务提供方接口。还是类似上面的问题,需要签名。

先考虑一下,存在什么风险

1,服务提供方接口放到平台,参数是固定的,如果别人改了参数,调用接口,这样就不行了。

2,第三方不改接口参数,如果这个调用者没有接入平台,他是不能调的,即需要身份验证

再看怎么实现:流程如下

1,第三方调用方和平台约定签名规则,如下

2,第三方调用方选择平台中已上线的接口,生成签名sign和请求url

3,第三方通过url(这个url中包含签名sign,就是校验位),调用平台接口

4,平台验签,是由网关做的,就是同样的签名规则,再生成一个签名,比较url中第三方传过来的签名,一直则验签通过

5,身份验证,是校验url中,应用appId存在

参数说明:

开放平台API请求参数分为两种:系统参数和业务参数

系统参数:访问开放平台必须要传的参数,包括应用appId,接口名称method、应用签名sign和时间戳timestamp

业务参数:是由接口提供商指定,调用方访问接口时必须传接口指定的参数,详情参考接口列表

签名规则:

签名sign是通过应用在平台的私钥和请求参数根据一定算法生成的签名值,主要是防止参数在传输过程中被篡改, 同时对调用方的身份进行校验

签名生成步骤:

    第1步:

      将除签名sign外的所有请求参数按参数名首字母进行升序排序,其中参数包括系统参数和业务参数

    第2步:

      将第1步得到的排序结果,依次按照"keyvalue"的形式拼接成待加密的串

    第3步

      对第2步得到的新串进行HMAC加密并转化为大写的十六进制签名串

签名示例如下:

假设请求为:/anji-open/open-api/request?appId=testApp&method=testApp&sign=8D45C66B6E1E773614E5866541EAF78D×tamp=111&name=hello&password=123456&age=12

1、关键信息:
应用私钥:29bca37bf0174ea287a770cd0d4ff83c
请求参数:appId=testApp&method=testApp&sign=8D45C66B6E1E773614E5866541EAF78D×tamp=111&name=hello&password=123456&age=12

2、将除"sign"外所有的参数进行排序:age、appId、method、name、password、timestamp;
依次按照"keyvalue"形式拼接的结果:age12appIdtestAppmethodtestAppnamehellopassword123456timestamp111

3、将得到的新串进行HMAC加密并转化为大写的十六进制签名串:8D45C66B6E1E773614E5866541EAF78D

实现:

**
 * 验证签名,防止数据被篡改
 * @author lr
 * @since 2019-05-29 17:02
 */
public class SignUtils {

    private final static String SIGN_PARAM="sign";

    /**
     * 生产签名
     * @param params
     * @param secret
     * @return
     * @throws IOException
     */
    public static String generateSign(Map params, String secret) throws IOException {
        // 第一步:检查参数是否已经排序
        String[] keys = params.keySet().toArray(new String[0]);
        Arrays.sort(keys);

        // 第二步:把所有参数名和参数值串在一起
        StringBuilder query = new StringBuilder();
        for (String key : keys) {
            //跳过参数"sign",sign不参与签名
            if(StringUtils.equals(SIGN_PARAM,key)) {
                continue;
            }
            String value = params.get(key);
            if (StringUtils.isNotBlank(value)) {
                query.append(key).append(value);
            }
        }

        // 第三步:使用HMAC加密
        byte[] bytes = encryptHMAC(query.toString(), secret);

        // 第四步:把二进制转化为大写的十六进制(正确签名应该为32大写字符串,此方法需要时使用)
        return byte2hex(bytes);
    }

    /**
     * 加密
     * @param data
     * @param secret
     * @return
     * @throws IOException
     */
    public static byte[] encryptHMAC(String data, String secret) throws IOException {
        byte[] bytes = null;
        try {
            SecretKey secretKey = new SecretKeySpec(secret.getBytes(ApiConstants.CHARSET_UTF8), "HmacMD5");
            Mac mac = Mac.getInstance(secretKey.getAlgorithm());
            mac.init(secretKey);
            bytes = mac.doFinal(data.getBytes(ApiConstants.CHARSET_UTF8));
        } catch (GeneralSecurityException gse) {
            throw new IOException(gse.toString());
        }
        return bytes;
    }

    /**
     * 转16进制
     * @param bytes
     * @return
     */
    public static String byte2hex(byte[] bytes) {
        StringBuilder sign = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(bytes[i] & 0xFF);
            if (hex.length() == 1) {
                sign.append("0");
            }
            sign.append(hex.toUpperCase());
        }
        return sign.toString();
    }

    public static void main(String[] args) {
        Map map = new HashMap<>(16);
        map.put("appId","testApp");
        map.put("method","testPost");
        map.put("timestamp","111");
        try {
            String s = generateSign(map, "e113e43bb76a437d94848a2bbbb45b0b");
            System.out.println(s);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

三,公积金项目

流程:

调公积金接口,需要验证签名,签名规则MD5 apiKey

系统入参:apiKey timestamp sign
盐:appSecret
 

你可能感兴趣的:(#,签名与安全,记录一个对外接口签名问题)