关于参数签名的总结

来源:互联网 发布:星星知多少的自频道 编辑:程序博客网 时间:2024/06/01 10:41

刚刚工作2~3年,我们搞java或是php(这里省略很多语言)开发年轻人,前几年开发的时候总会碰到一些和第三方对接的任务,例如第三方交易的支付宝、微信(心累,文档里面有很多排序和参数大小写错误的问题),再或是聊天软件的融云、环信,再或是地图的高德等。
他们虽然说大部分功能都提供SDK,简化接入者的工作量,让接入者只关系调用(自己只需要组织业务),不需要太过关心安全方面的问题,但是有些功能(接口)还是不可避免的使用HTTP/HTTPS(有些HTTPS的接口也需要参数签名)接口访问,这就带来了一些安全性的问题。例如防止CSRF(不知道者可以自行谷歌)、XSS等。 一开始特别不理解有些网站为什么要签名整这么麻烦做什么,接入怎么这么困难,又排序又base64最后还得加密,很不理解这样做的原因。
有一次开发接入微信的公众号支付wap版整整调试了2天,真心觉得很累心。下面这篇文章我们就来讨论和解释下其中的原因。
当然作为服务的提供者不得不考虑一些安全性的问题,其中比较关键的问题就是访问是不是我的合法用户,防止其他人伪造遍历你的接口攻击你,这是互联网安全必须考虑的因素。
好针对合法用户的判断有2个方面需要考虑
1. 判断访问是不是来源合法的地址。

这对这可以通过http 报头里面的Referer 进行验证,取到访问者的 request.getHeader("referer"); 进行访问校验是不是合法用户,当然这需要开发者先提供网站的域名,就像微信公众平台开发。

  1. 参数签名 这是我们今天讨论的重点

参数签名可以保证开发的者的信息被冒用后,信息不会被泄露和受损。原因在于接入者和提供者都会对每一次的接口访问进行签名和验证。下面我们就简述下这个过程。
可以这样理解接入者把需求访问的接口的所有必要的参数信息(一般都会有一个ACCESSKEY_ID,用于标示接入者的信息,一般都是接入者的ID或是其唯一标示,还会有随机字符串和时间戳,是为了防止被重放)按照字母顺序拼接成字符串(防止参数乱序之后,造成加密不一致的问题),URLencode(标准的HTTP传输规范),BASE64编码(将非ASCII字符的数据转换成ASCII字符,有效降低 %xx 的出现次数) 之后再和提供者提供的一个SECRET_KEY,这个SECRET_KEY只有接入者和提供者知道,进行加密或是摘要(用MD5或是SHA1摘要的比较多),然后当做一个新的参数sign发给提供者。提供者接收到这些信息后首先取到ACCESSKEY_ID 进行接入者的身份判断,取到用户的SECRET_KEY,然后取得所有的参数(去除sign字段),按照字母的顺序拼接成字符串,base64之后再和SECRET_KEY 进行和开发者相同的加密方式也得到sign 字段,和开发者传来的sign进行比较,如果相等则证明是合法的接入者的一次正常的调用请求,其中也会校验是否被重放(会记录一些改用户的访问参数信息,会验证随机字符串和时间戳短时间内是否一致,防止被重放),验证通过,正常返回业务请求的信息,如果不匹配那么久证明是有人篡改了参数信息企图攻击,就不予通过,返回错误。一般的还会有记录黑名单的功能。这样即使接入者的信息泄露了(ACCESSKEY_ID),攻击者随意修改参数也是无效的了。还是需要有技术成本的。
以上我们大致说明了参数签名的意义和开发者和提供者的流程,(有些支付还需在控制台填写特殊的字符串用作第二次匹配使用,但是思路还是一样的。)我想看到这读者一定对签名的意义和作用有了了解,明白了为什么大型的网站针对安全方面要做参数签名了。

下面直接上干货,来点实例:由于微信支付和支付宝支付网上很多资源这里就不重复写了。来个最近刚刚接入的服务
这是一个阿里云CDN 调用的例子

  • 首先确认调用的接口,这里以RefreshObjectCaches 调用为例。确认接口参数

接口参数

  • 构造公共参数 核心逻辑
 String ACCESSKEY_ID = "testid";    String ACCESSKEY_SECRET = "testsecret";   private static final Logger LOGGER = LoggerFactory.getLogger(CDNServiceImpl.class);   private static final String ISO8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss:SSSXXX";   private static  final String HTTP_METHOD_STR  = "GET&%2F&";    public boolean refreshObjectCaches(String filePath) {        if(StringUtils.isEmpty(filePath)){            return true;        }        Map<String, String> param =new HashMap<>();        param.put("Action","RefreshObjectCaches");        param.put("ObjectPath",filePath);        param.put("ObjectType","File");        String reqpestParam = formPublicParam(param);        String url = String.format("%s?%s", "http://cdn.aliyuncs.com", reqpestParam);        String result =  HttpRequestUnit.get(url).connectTimeout(2000).readTimeout(2000).body();        System.out.println("result "+result);    }private static String formPublicParam(Map<String, String> requestParam) {        //所有的参数,这里使用TreeMap, 好处在于天然有序,默认是字母顺序        Map<String, String> params = new TreeMap<>();        params.putAll(requestParam);        //随机串        String nonce = RandomStringUtils.randomAlphanumeric(6);        //访问的URL        StringBuffer buffer = new StringBuffer();        for (Map.Entry<String, String> param : params.entrySet()) {            buffer.append(param.getKey() + "=" + param.getValue() + "&");        }        buffer.append("Format=JSON").append("&");        params.put("Format", "JSON");        buffer.append("Version=").append("2014-11-11").append("&");        params.put("Version", "2014-11-11");        buffer.append("AccessKeyId=").append(ACCESSKEY_ID).append("&");        params.put("AccessKeyId", ACCESSKEY_ID);        buffer.append("SignatureMethod=").append("HMAC-SHA1").append("&");        params.put("SignatureMethod", "HMAC-SHA1");        String timeStamp = DateUtils.formatDate(new Date(),ISO8601_PATTERN);        buffer.append("TimeStamp=").append(timeStamp).append("&");        params.put("TimeStamp", timeStamp);        buffer.append("SignatureVersion=1.0").append("&");        params.put("SignatureVersion", "1.0");        buffer.append("SignatureNonce=").append(nonce).append("&");        params.put("SignatureNonce", nonce);        String sign ="";        try {            sign = getSign(params);        } catch (Exception e) {            LOGGER.error(e.getMessage(),e);        }        //最后拼接签名        buffer.append("Signature=").append(sign);        return buffer.toString();    } private static String getSign(Map<String, String> params) throws Exception {        StringBuilder builder = new StringBuilder();        int size = 1;        for (Map.Entry<String, String> entry : params.entrySet()) {            //对每个参数和值进行encode,对字符进行转义           String key = URLEncoder.encode(entry.getKey(), "UTF-8");           String value = URLEncoder.encode(entry.getValue(), "UTF-8");            builder.append(key + "=" + value);            if (size != params.size()) {                builder.append("&");            }            size++;        }        String stringToSign = HTTP_METHOD_STR+URLEncoder.encode(builder.toString(), "UTF-8");        byte[] bytes =HmacSHA1Encrypt(stringToSign, ACCESSKEY_SECRET + "&");        BASE64Encoder encoder = new BASE64Encoder();        return encoder.encode(bytes);    } public static byte[] HmacSHA1Encrypt(String encryptText, String encryptKey) throws Exception {        Mac mac = Mac.getInstance("HmacSHA1");        SecretKey secretKey = new SecretKeySpec(encryptKey.getBytes(), "HmacSHA1");        mac.init(secretKey);        byte[] text = encryptText.getBytes();        byte[] bytes = mac.doFinal(text);        return bytes;    }

以上是调用的核心代码,提供者接到后其实流程也差不多的。本文的目的在于总结下这几年调用第三方服务的一些心得和一些积累。为了让和我一样的新手们能对安全有些认识。最后感谢大家能看到最后。如果有什么不对或是说的不明的地方还请大家多多指出。

1 0
原创粉丝点击