array(2) { ["docs"]=> array(10) { [0]=> array(10) { ["id"]=> string(3) "428" ["text"]=> string(77) "Visual Studio 2017 单独启动MSDN帮助(Microsoft Help Viewer)的方法" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(8) "DonetRen" ["tagsname"]=> string(55) "Visual Studio 2017|MSDN帮助|C#程序|.NET|Help Viewer" ["tagsid"]=> string(23) "[401,402,403,"300",404]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511400964" ["_id"]=> string(3) "428" } [1]=> array(10) { ["id"]=> string(3) "427" ["text"]=> string(42) "npm -v;报错 cannot find module "wrapp"" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(4) "zzty" ["tagsname"]=> string(50) "node.js|npm|cannot find module "wrapp“|node" ["tagsid"]=> string(19) "[398,"239",399,400]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511400760" ["_id"]=> string(3) "427" } [2]=> array(10) { ["id"]=> string(3) "426" ["text"]=> string(54) "说说css中pt、px、em、rem都扮演了什么角色" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(12) "zhengqiaoyin" ["tagsname"]=> string(0) "" ["tagsid"]=> string(2) "[]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511400640" ["_id"]=> string(3) "426" } [3]=> array(10) { ["id"]=> string(3) "425" ["text"]=> string(83) "深入学习JS执行--创建执行上下文(变量对象,作用域链,this)" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(7) "Ry-yuan" ["tagsname"]=> string(33) "Javascript|Javascript执行过程" ["tagsid"]=> string(13) "["169","191"]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511399901" ["_id"]=> string(3) "425" } [4]=> array(10) { ["id"]=> string(3) "424" ["text"]=> string(30) "C# 排序技术研究与对比" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(9) "vveiliang" ["tagsname"]=> string(0) "" ["tagsid"]=> string(2) "[]" ["catesname"]=> string(8) ".Net Dev" ["catesid"]=> string(5) "[199]" ["createtime"]=> string(10) "1511399150" ["_id"]=> string(3) "424" } [5]=> array(10) { ["id"]=> string(3) "423" ["text"]=> string(72) "【算法】小白的算法笔记:快速排序算法的编码和优化" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(9) "penghuwan" ["tagsname"]=> string(6) "算法" ["tagsid"]=> string(7) "["344"]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511398109" ["_id"]=> string(3) "423" } [6]=> array(10) { ["id"]=> string(3) "422" ["text"]=> string(64) "JavaScript数据可视化编程学习(二)Flotr2,雷达图" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(7) "chengxs" ["tagsname"]=> string(28) "数据可视化|前端学习" ["tagsid"]=> string(9) "[396,397]" ["catesname"]=> string(18) "前端基本知识" ["catesid"]=> string(5) "[198]" ["createtime"]=> string(10) "1511397800" ["_id"]=> string(3) "422" } [7]=> array(10) { ["id"]=> string(3) "421" ["text"]=> string(36) "C#表达式目录树(Expression)" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(4) "wwym" ["tagsname"]=> string(0) "" ["tagsid"]=> string(2) "[]" ["catesname"]=> string(4) ".NET" ["catesid"]=> string(7) "["119"]" ["createtime"]=> string(10) "1511397474" ["_id"]=> string(3) "421" } [8]=> array(10) { ["id"]=> string(3) "420" ["text"]=> string(47) "数据结构 队列_队列实例:事件处理" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(7) "idreamo" ["tagsname"]=> string(40) "C语言|数据结构|队列|事件处理" ["tagsid"]=> string(23) "["246","247","248",395]" ["catesname"]=> string(12) "数据结构" ["catesid"]=> string(7) "["133"]" ["createtime"]=> string(10) "1511397279" ["_id"]=> string(3) "420" } [9]=> array(10) { ["id"]=> string(3) "419" ["text"]=> string(47) "久等了,博客园官方Android客户端发布" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(3) "cmt" ["tagsname"]=> string(0) "" ["tagsid"]=> string(2) "[]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511396549" ["_id"]=> string(3) "419" } } ["count"]=> int(200) } 222 接口鉴权签名实践 - 爱码网

工作当中不免要与其他的公司进行打交道,比如调用对方的接口完成某项操作,或者提供接口给对方调用,这些接口可能使用者有很多公司,为了保证接口的安全性,需要设计一些方式来对接口进行保护,常见的保护措施有 IP 白名单与接口签名。
IP 白名单这种方式就不多说很简单,判断接口调用者 IP 是否在设定的白名单 IP 之中即可。但是 IP 白名单这种方式有个弊端就是维护白名单 IP 列表成了体力活,调用方增加服务器或者减少服务器就要更新白名单,对于接口提供方不是很友好,最好使用签名的方式一劳永逸,因此本文主要讲讲常用的接口签名方式,主要应用与服务端与服务端之间的接口交互。
因为生成签名过程中使用到了 appSecret,因此这种方式最好不要用于客户端与服务端之间的接口加密,appSecret 写死在 APP 中,逆向技术获取不是太困难的事情,当然非要使用,appSecret 最好通过登录时动态生成,写死的方式一向是不推荐的做法。

本文首发个人技术博客:http://nullpointer.pw/interface-authentication.html

接口签名作用

接口签名解决了如下这些问题:

  • 防止接口非法调用
  • 防止接口参数被篡改
  • 防止接口过期参数请求
  • 防止接口请求重放

接口签名方式

一般来说,接口签名方式主要是这样的,所有的接口都需要传递这几个公共参数appKeysigntimestampnonce,sign 的计算规则为

  1. 拼接接口的所有参数,参数名按照 ASCII 码从小到大排序(字典序),拼接的格式如 k1=v1&k2=v2&k3=v3 得到 params
  2. Base64(HMAC_SHA1(params, appSecret)),得到 sign 值
  3. sign 加到参数中,发送请求目标接口
    以上就是生成签名的过程。

验证签名的过程与之相同,就是从请求中提取所有的参数(除去 sign),然后同样的方式生成签名,然后将签名结果与请求中的 sign 参数进行比较,如果一致则验签成功否则失败。为了保证接口参数的时效性,一般会在验签之前校验 timestamp 参数是否超时,比如与当前时间相差 10 分钟则直接提示验签失败。另外为了防止请求重放,即相同参数不可重复请求,可以通过将 nonce 参数进行缓存,比如防止到 Redis 当中,设置 10 分钟的有效期,如果 nonce 存在与缓存中则提示验签失败,这样便通过 nonce 配合 timestamp 实现了请求重放。

话不多少,开始实践一下~

为了方便演示,就再 main 方法中实现签名与验签的逻辑。
在日常开发中,可以集成为 springboot starter,通过拦截器进行统一校验。如何自定义 springboot starter,可以参考我上一篇文章:实用主义之自定义 SpringBootStarter

依赖添加

因为加密算法使用到了开源工具包 commons-codec,先添加这个依赖

<dependency>
  <groupId>commons-codec</groupId>
  <artifactId>commons-codec</artifactId>
  <version>1.14</version>
</dependency>

生成签名与验签

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.codec.digest.HmacUtils;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;

import static java.time.temporal.ChronoUnit.SECONDS;

/**
 * @author WeJan
 * @since 2020-05-31
 */
public class AuthTest {

    public static void main(String[] args) {
        System.out.println("********************************* 签名 *********************************");
//        long timestamp = LocalDateTime.now().plusMinutes(-10).toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
        long timestamp = System.currentTimeMillis();
        String appKey = "testtest";
        String appSecret = "123456";

        Map<String, String> map = new HashMap<>();
        map.put("appKey", appKey);
        map.put("k1", "k1");
        map.put("k2", "k2");
        map.put("timestamp", String.valueOf(timestamp));
        String outSignData = getSignData(map);
        byte[] hmac = new HmacUtils(HmacAlgorithms.HMAC_SHA_1, appSecret).hmac(outSignData);
        String sign = new String(Base64.encodeBase64(hmac));
        map.put("sign", sign);
        System.out.println("outSign: " + sign);
        System.out.println("outSignData: " + outSignData);
        String outParams = JSONObject.toJSONString(map);
        System.out.println("outParams: " + outParams);

        System.out.println("\n\n********************************* 验签 *********************************");
        Map<String, String> inMap = JSONObject.parseObject(outParams, new TypeReference<Map<String, String>>() {
        });
        // 校验请求是否过期
        String inTimeStamp = inMap.getOrDefault("timestamp", "0");
        LocalDateTime inTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(inTimeStamp)), ZoneOffset.ofHours(8));
        Duration duration = Duration.between(inTime, LocalDateTime.now());
        long seconds = duration.get(SECONDS);
        System.out.println("seconds: " + seconds);
        if (seconds > 10 * 60) {
            System.out.println("请求超时");
            return;
        }
        String inSignData = getSignData(inMap);
        System.out.println("inSignData: " + inSignData);
        byte[] inHmac = new HmacUtils(HmacAlgorithms.HMAC_SHA_1, appSecret).hmac(inSignData);
        String sign2 = new String(Base64.encodeBase64(inHmac));
        System.out.println("sign2: " + sign2);
        System.out.println("验签结果: " + sign.equals(sign2));
    }

    public static String getSignData(Map<String, String> params) {
        StringBuilder content = new StringBuilder();
        // key 自然排序
        List<String> keys = new ArrayList<>(params.keySet());
        Collections.sort(keys);

        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            if ("sign".equals(key)) {
                continue;
            }
            String value = params.get(key);
            if (value != null) {
                content.append(i == 0 ? "" : "&").append(key).append("=").append(value);
            } else {
                content.append(i == 0 ? "" : "&").append(key).append("=");
            }
        }

        return content.toString();
    }
}

运行结果:

********************************* 签名 *********************************
outSign: eADUCnVqcArdexcdMVcHkPJQyXU=
outSignData: appKey=testtest&k1=k1&k2=k2&timestamp=1590996917340
outParams: {"k1":"k1","k2":"k2","sign":"eADUCnVqcArdexcdMVcHkPJQyXU=","appKey":"testtest","timestamp":"1590996917340"}

********************************* 验签 *********************************
seconds: 0
inSignData: appKey=testtest&k1=k1&k2=k2&timestamp=1590996917340
sign2: eADUCnVqcArdexcdMVcHkPJQyXU=
验签结果: true

相关文章: