简单API接口签名验证设计

前言

后端在写对外的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. 拼接密钥accessSecretstringSignTemp="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();
    }
}

原文:https://my.oschina.net/KelinM/blog/1925209 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
API接口签名验证可以确保请求的来源是可信的,并且请求参数没有被篡改。以下是一个PHP实现API接口签名验证的示例: 1. 客户端发送请求时,将参数按照参数名进行字典排序,并将参数名和参数值用等号连接起来,每个键值对之间用&符号连接起来,得到待签名字符串。 2. 将待签名字符串和应用程序的appsecret进行拼接,然后通过哈希算法(如SHA1或MD5)计算得到签名值。 3. 将签名值作为参数(通常是sign或signature)添加到原有的请求参数中,然后发送请求。 4. 服务器接收到请求后,按照同样的方式计算签名,将计算出的签名值与请求中的签名值进行比对,如果一致则认为请求合法,否则认为请求不合法。 以下是一个简单的示例代码: ```php <?php // 定义应用程序的appsecret $appsecret = "your_app_secret"; // 获取请求参数 $params = $_POST; // 按照参数名进行字典排序 ksort($params); // 将参数名和参数值用等号连接起来,每个键值对之间用&符号连接起来,得到待签名字符串 $sign_str = ""; foreach ($params as $key => $value) { $sign_str .= $key . "=" . $value . "&"; } $sign_str .= "appsecret=" . $appsecret; // 计算签名值 $sign = md5($sign_str); // 验证签名 if ($sign != $params['sign']) { // 签名验证失败 echo "Invalid Signature"; } else { // 签名验证成功 // 执行业务逻辑 } ?> ``` 在实际应用中,为了增加安全性,可以使用更加复杂的签名算法,如RSA数字签名等。另外,还可以限制请求的时间戳和nonce值,防止重放攻击。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值