文档章节

简单API接口签名验证

_Kelin
 _Kelin
发布于 08/09 17:08
字数 1132
阅读 1166
收藏 37

前言

后端在写对外的API接口时,一般会对参数进行签名来保证接口的安全性,在设计签名算法的时候,主要考虑的是这几个问题:
1. 请求的来源是否合法
2. 请求参数是否被篡改
3. 请求的唯一性
我们的签名加密也是主要针对这几个问题来实现

设计

基于上述的几个问题,我们来通过已下步骤来实现签名加密:
1. 通过分配给APP对应的app_key和app_secret来验证身份
2. 通过将请求的所有参数按照字母先后顺序排序后拼接再MD5加密老保证请求参数不被篡改
3. 请求里携带时间戳参数老保证请求的唯一和过期,重复的请求在指定时间(可配置)内有效

实现

  1. 签名生成:

    1. 生成当前时间戳timestamp=now
    2. 按照请求参数名的字母升序排列非空请求参数(包含accessKey) stringA="AccessKey=access&home=world&name=hello&work=java&timestamp=now&nonce=random";
    3. 拼接密钥accessSecret stringSignTemp="AccessKey=access&home=world&name=hello&work=java&timestamp=now&nonce=random&accessSecret=secret";
    4. MD5并转换为大写生成签名 sign=MD5(stringSignTemp).toUpperCase();

JAVA代码如下:params是从request里面获取的所有参数map,accessSecret是加密密钥

 private String createSign(Map<String, Object> params, String accessSecret) throws UnsupportedEncodingException {
        Set<String> keysSet = params.keySet();
        Object[] keys = keysSet.toArray();
        Arrays.sort(keys);
        StringBuilder temp = new StringBuilder();
        boolean first = true;
        for (Object key : keys) {
            if (first) {
                first = false;
            } else {
                temp.append("&");
            }
            temp.append(key).append("=");
            Object value = params.get(key);
            String valueString = "";
            if (null != value) {
                valueString = String.valueOf(value);
            }
            temp.append(valueString);
        }
        temp.append("&").append(ACCESS_SECRET).append("=").append(accessSecret);
        return MD5Util.MD52(temp.toString()).toUpperCase();
    }
  1. 签名校验:

    • 参数格式校验
    • 超时校验
    • 验证签名
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        Map<String, Object> result = new HashMap<String, Object>();
        String timestamp = request.getParameter(TIMESTAMP_KEY);
        String accessKey = request.getParameter(ACCESS_KEY);
        String accessSecret = map.get(accessKey);

        if (!org.apache.commons.lang.StringUtils.isNumeric(timestamp)) {
            result.put("code", 1000);
            result.put("msg", "请求时间戳不合法");
            WebUtils.writeJsonByObj(result, response, request);
            return false;
        }

        // 检查KEY是否合理
        if (StringUtils.isEmpty(accessKey) || StringUtils.isEmpty(accessSecret)) {
            result.put("code", 1001);
            result.put("msg", "加密KEY不合法");
            WebUtils.writeJsonByObj(result, response, request);
            return false;
        }
        Long ts = Long.valueOf(timestamp);
        // 禁止超时签名
        if (System.currentTimeMillis() - ts > SIGN_EXPIRED_TIME) {
            result.put("code", 1002);
            result.put("msg", "请求超时");
            WebUtils.writeJsonByObj(result, response, request);
            return false;
        }

        if (!verificationSign(request, accessKey, accessSecret)) {
            result.put("code", 1003);
            result.put("msg", "签名错误");
            WebUtils.writeJsonByObj(result, response, request);
            return false;
        }
        return true;
    }

校验签名

 private boolean verificationSign(HttpServletRequest request, String accessKey, String accessSecret) throws UnsupportedEncodingException {
        Enumeration<?> pNames = request.getParameterNames();
        Map<String, Object> params = new HashMap<String, Object>();
        while (pNames.hasMoreElements()) {
            String pName = (String) pNames.nextElement();
            if (SIGN_KEY.equals(pName)) continue;
            Object pValue = request.getParameter(pName);
            params.put(pName, pValue);
        }
        String originSign = request.getParameter(SIGN_KEY);
        String sign = createSign(params, accessSecret);
        return sign.equals(originSign);
    }
  1. 完整代码:

这里通过拦截器来实现接口拦截,可自行替换

package com.mlcs.mop.common.web.interceptor;

import com.mlcs.core.conf.ZKClient;
import com.mlcs.mop.common.web.util.MD5Util;
import com.mlcs.mop.common.web.util.WebUtils;
import org.apache.zookeeper.KeeperException;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Author: Kelin
 * Date:  2018/5/16
 * Description:
 */
@SuppressWarnings("SuspiciousMethodCalls")
public class SimpleApiSignInterceptor extends HandlerInterceptorAdapter {

    // 签名超时时长,默认时间为5分钟,ms
    private static final int SIGN_EXPIRED_TIME = 5 * 60 * 1000;

    private static final String API_SIGN_KEY_CONFIG_PATH = "/mop/common/system/api_sign_key_mapping.properties";

    private static final String SIGN_KEY = "sign";

    private static final String TIMESTAMP_KEY = "timestamp";

    private static final String ACCESS_KEY = "accessKey";

    private static final String ACCESS_SECRET = "accessSecret";

    private static Map<String, String> map = new ConcurrentHashMap<String, String>();


    static {
        // 从zk加载key映射到内存里面
        try {
            String data = ZKClient.get().getStringData(API_SIGN_KEY_CONFIG_PATH);
            Properties properties = new Properties();
            properties.load(new StringReader(data));
            for (Object key : properties.keySet()) {
                map.put(String.valueOf(key), properties.getProperty(String.valueOf(key)));
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        Map<String, Object> result = new HashMap<String, Object>();
        String timestamp = request.getParameter(TIMESTAMP_KEY);
        String accessKey = request.getParameter(ACCESS_KEY);
        String accessSecret = map.get(accessKey);

        if (!org.apache.commons.lang.StringUtils.isNumeric(timestamp)) {
            result.put("code", 1000);
            result.put("msg", "请求时间戳不合法");
            WebUtils.writeJsonByObj(result, response, request);
            return false;
        }

        // 检查KEY是否合理
        if (StringUtils.isEmpty(accessKey) || StringUtils.isEmpty(accessSecret)) {
            result.put("code", 1001);
            result.put("msg", "加密KEY不合法");
            WebUtils.writeJsonByObj(result, response, request);
            return false;
        }
        Long ts = Long.valueOf(timestamp);
        // 禁止超时签名
        if (System.currentTimeMillis() - ts > SIGN_EXPIRED_TIME) {
            result.put("code", 1002);
            result.put("msg", "请求超时");
            WebUtils.writeJsonByObj(result, response, request);
            return false;
        }

        if (!verificationSign(request, accessKey, accessSecret)) {
            result.put("code", 1003);
            result.put("msg", "签名错误");
            WebUtils.writeJsonByObj(result, response, request);
            return false;
        }
        return true;
    }

    private boolean verificationSign(HttpServletRequest request, String accessKey, String accessSecret) throws UnsupportedEncodingException {
        Enumeration<?> pNames = request.getParameterNames();
        Map<String, Object> params = new HashMap<String, Object>();
        while (pNames.hasMoreElements()) {
            String pName = (String) pNames.nextElement();
            if (SIGN_KEY.equals(pName)) continue;
            Object pValue = request.getParameter(pName);
            params.put(pName, pValue);
        }
        String originSign = request.getParameter(SIGN_KEY);
        String sign = createSign(params, accessSecret);
        return sign.equals(originSign);
    }

    private String createSign(Map<String, Object> params, String accessSecret) throws UnsupportedEncodingException {
        Set<String> keysSet = params.keySet();
        Object[] keys = keysSet.toArray();
        Arrays.sort(keys);
        StringBuilder temp = new StringBuilder();
        boolean first = true;
        for (Object key : keys) {
            if (first) {
                first = false;
            } else {
                temp.append("&");
            }
            temp.append(key).append("=");
            Object value = params.get(key);
            String valueString = "";
            if (null != value) {
                valueString = String.valueOf(value);
            }
            temp.append(valueString);
        }
        temp.append("&").append(ACCESS_SECRET).append("=").append(accessSecret);
        return MD5Util.MD52(temp.toString()).toUpperCase();
    }
}

© 著作权归作者所有

共有 人打赏支持
_Kelin
粉丝 29
博文 9
码字总数 17240
作品 0
浦东
私信 提问
加载中

评论(16)

_Kelin
_Kelin

引用来自“吕兵阳”的评论

引用来自“_Kelin”的评论

引用来自“吕兵阳”的评论

引用来自“_Kelin”的评论

引用来自“吕兵阳”的评论

如果请求体是个body呢?例如json如何做比较好?
逻辑也是差不多的,先通过MD5之类的生成签名,如果是JSON传递的话,可以将整个json base64编码,固定key传递,服务端再解析

json顺序你如何保持?如果是嵌套json的话?
现在项目里有使用的传递json,其json key的顺序也是固定的,按照几个固定的key顺序排好,当然这些key的名字也是固定的,如果是复杂的嵌套json的话,我的想法是,参与加密的json key也是固定的,按照约定的顺序排,嵌套的内部同理

你想的简单了,一般项目中都是通过序列化工具转json不同平台,不同类库的设计,序列化后顺序是不同的。并没有你想象的那么理想。
:scream:恩恩,确实是有这种情况,不过我认为涉及到加密这种情况,复杂的JSON并不适合,而且在项目里面我也是不太喜欢这种JSON传递的,这种我认为可读性不太好,个人认为哈
吕兵阳
吕兵阳

引用来自“_Kelin”的评论

引用来自“吕兵阳”的评论

引用来自“_Kelin”的评论

引用来自“吕兵阳”的评论

如果请求体是个body呢?例如json如何做比较好?
逻辑也是差不多的,先通过MD5之类的生成签名,如果是JSON传递的话,可以将整个json base64编码,固定key传递,服务端再解析

json顺序你如何保持?如果是嵌套json的话?
现在项目里有使用的传递json,其json key的顺序也是固定的,按照几个固定的key顺序排好,当然这些key的名字也是固定的,如果是复杂的嵌套json的话,我的想法是,参与加密的json key也是固定的,按照约定的顺序排,嵌套的内部同理

你想的简单了,一般项目中都是通过序列化工具转json不同平台,不同类库的设计,序列化后顺序是不同的。并没有你想象的那么理想。
_Kelin
_Kelin

引用来自“吕兵阳”的评论

引用来自“_Kelin”的评论

引用来自“吕兵阳”的评论

如果请求体是个body呢?例如json如何做比较好?
逻辑也是差不多的,先通过MD5之类的生成签名,如果是JSON传递的话,可以将整个json base64编码,固定key传递,服务端再解析

json顺序你如何保持?如果是嵌套json的话?
现在项目里有使用的传递json,其json key的顺序也是固定的,按照几个固定的key顺序排好,当然这些key的名字也是固定的,如果是复杂的嵌套json的话,我的想法是,参与加密的json key也是固定的,按照约定的顺序排,嵌套的内部同理
吕兵阳
吕兵阳

引用来自“_Kelin”的评论

引用来自“吕兵阳”的评论

如果请求体是个body呢?例如json如何做比较好?
逻辑也是差不多的,先通过MD5之类的生成签名,如果是JSON传递的话,可以将整个json base64编码,固定key传递,服务端再解析

json顺序你如何保持?如果是嵌套json的话?
_Kelin
_Kelin

引用来自“吕兵阳”的评论

如果请求体是个body呢?例如json如何做比较好?
逻辑也是差不多的,先通过MD5之类的生成签名,如果是JSON传递的话,可以将整个json base64编码,固定key传递,服务端再解析
吕兵阳
吕兵阳
如果请求体是个body呢?例如json如何做比较好?
吕兵阳
吕兵阳
挺好的。
罗祥
罗祥

引用来自“罗祥”的评论

一些工具类咋没提供呢

引用来自“_Kelin”的评论

。。。这里面就是些工具类了,觉得跟接口签名没啥关系,就没放了。。。
把工具类贴出来呗。要来就来整套的啊。
罗祥
罗祥
把工具类贴出来呗。
_Kelin
_Kelin

引用来自“罗祥”的评论

一些工具类咋没提供呢
。。。这里面就是些工具类了,觉得跟接口签名没啥关系,就没放了。。。
Web API系列(二)接口安全和参数校验

  以前简单介绍过web api 的设计,但是还是有很多朋友问我,如何合理的设计和实现web api。比如,接口安全,异常处理,统一数据返回等问题。所以有必要系统的总结总结 web api 的设计和实现...

章为忠
2016/12/20
0
0
baigo SSO v3.0 全新发布,不再兼容老版本

重要信息,v3.0 起不再兼容老版本,请开发者尽快转换原有程序。 -------------------v3.0------------------- 2018-04-20 Bootstrap 升级至 4.0.0 全面升级优化前端界面 2018-04-10 进一步优...

baigoStudio
05/08
0
0
使用JWT做RESTful API的身份验证-Go语言实现

使用JWT做RESTful API的身份验证-Go语言实现 在 使用Golang和MongoDB构建 RESTful API已经实现了一个简单的 RESTful API应用,但是对于有些API接口需要授权之后才能访问,在这篇文章中就用 ...

CoderMiner
07/17
0
0
ToughRADIUS API 发布,开源计费系统扩展能力增强

ToughRADIUS API 发布了。 ToughRADIUS API要解决的三个问题: 业务功能扩展:ToughRADIUS V2版本提供了一个精简的管理系统,但是对于一些客户比较复杂的业务需求会显得不够用,利用API可以实...

jamiesun
2016/03/03
1K
0
App开放接口api安全性—Token签名sign的设计与实现

App开放接口api安全性—Token签名sign的设计与实现 api签名 / sign签名 / token 前言 在app开放接口api的设计中,避免不了的就是安全性问题,因为大多数接口涉及到用户的个人信息以及一些敏感...

蜗牛奔跑
2017/11/01
0
0

没有更多内容

加载失败,请刷新页面

加载更多

用any-loader封装jQuery的XHR —— 随便写着玩系列

哎,都说没人用JQuery啦,叫你别写这个。 其实我也是好高骛远使用过npm上某个和某个很出名的XHR库,嗯,认识我的人都知道我喜欢喷JQ,以前天天喷,见面第一句,你还用JQ,赶紧丢了吧。但我也...

曾建凯
今天
3
0
聊聊storm的AggregateProcessor的execute及finishBatch方法

序 本文主要研究一下storm的AggregateProcessor的execute及finishBatch方法 实例 TridentTopology topology = new TridentTopology(); topology.newStream("spout1", spout......

go4it
今天
3
0
大数据教程(7.5)hadoop中内置rpc框架的使用教程

博主上一篇博客分享了hadoop客户端java API的使用,本章节带领小伙伴们一起来体验下hadoop的内置rpc框架。首先,由于hadoop的内置rpc框架的设计目的是为了内部的组件提供rpc访问的功能,并不...

em_aaron
今天
5
0
CentOS7+git+github创建Python开发环境

1.准备CentOS7 (1)下载VMware Workstation https://pan.baidu.com/s/1miFU8mk (2)下载CentOS7镜像 https://mirrors.aliyun.com/centos/ (3)安装CentOS7系统 http://blog.51cto.com/fengyuns......

枫叶云
昨天
3
0
利用ibeetl 实现selectpicker 的三级联动

1. js 直接写在html页面上面,ibeetl 就可以动态地利用后台传上来的model List ,不需要每次点击都要ajax请求后台 2. 使用selectpicker 的时候,除了对selecct option的动态处理后,还需要 $("#...

donald121
昨天
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部