API签名认证算法全解析

API 签名认证算法

一. 概述

1. 概念

如果我们自己开发了一套 API 并且将这套 API 开放给他人使用的话, 就必须保证 API 调用的合法性:

API 签名认证算法就是一种用于验证 API 请求的合法性和完整性的安全机制.

2. 作用

给接口使用 API 签名认证算法, 可以增强 API 的安全性, 防止未经授权的用户访问, 防止恶意用户篡改请求数据.

二. 认证算法演变进程

(1) ak + sk 认证
① 基本流程:

一般服务端会给用户签发一个 ak 和一个 sk (其中 ak accessKey 表示用户标识, sk secretKey 表示密钥).

请求方向服务端发起请求的时候携带 ak 和 sk, 服务端收到 ak 和 sk 之后, 根据 ak 从数据库中查出 sk 并作对比, 如果相同则认证通过.

② 遇到的问题:

如果请求在传递过程中被拦截, sk 就会被其他人获取到, 造成密钥泄漏, 进而服务器有可能收到攻击.

③ 如何解决:

严禁 sk 明文传输, sk 必须和其它参数进行加密签名之后才能传输

(2) 签名认证
① 基本流程:

sk + 用户参数 进行加密签名 (一般使用 SHA256 (不可逆) ), 得到签名 sign.

请求方发送 ak, 用户参数 和 签名 (3个参数). 服务端接收到之后, 根据 ak 从数据库中查出 sk. 然后将 sk + 用户参数 再次进行加密签名, 将得到的签名和请求方发来的签名对比, 如果相同则认证通过.

这样的过程就全程没有暴露 sk 并且也可以得到正确校验.

② 遇到的问题:

使用签名认证有可能会遇到 ++重放攻击++ : 如果别人拦截请求, 获取到请求头中的签名, 那么他就可以复制签名并重新发送请求, 从而达到攻击服务器的目的.

③ 如何解决:

(1) 添加随机数 nonce: 这个随机数只能用一次, 每请求携带的随机数是不一样的 (服务端要保存用过的随机数).

使用 Redis 存储已经处理过的随机数. 当接收到新请求时, 先检查该请求中的随机数是否已经存在:

  • ++如果存在++ --> 说明处理过当前请求 (该请求可能是重放攻击, 拒绝处理).
  • ++如果不存在++ --> 将该随机数存储下来, 并设置一个过期时间 (通常与时间戳的有效时间一致), 以便后续清理过期数据.

(2) 添加 timestamp 时间戳: 请求方在请求中携带时间戳 timestamp, 服务端接收到请求后 检查时间戳与当前系统时间的差值是否在允许范围内 (如 5 分钟).

!TIP

:

  1. 在过期时间内, 服务端的随机数一定是仍然存在的. 所以如果重放攻击的话一定会被拒绝.
  2. 在过期时间外, 服务端保存的随机数也过期了, 但是 服务端检验时间戳得到的时间差值一定不在允许范围内, 重放攻击还是会被拒绝.

三. 实现步骤

对 sk 进行签名添加随机数, 时间戳 分别解决了 sk 泄露问题重放攻击问题, 最终得到的算法就没有任何的安全问题了, 具体实现步骤如下:

  1. 生成密钥对: 给每个用户生成唯一的密钥对 (accessKey 和 secretKey), 并保存到数据库中. 仅用户本人可查看自己的 ak 和 sk.
  2. 请求方生成签名: 请求方 (客户端) 对 secretKey 和 请求参数 进行签名. 签名的内容包括 请求参数 (body), 时间戳 (timestamp), 随机数 (nonce) 等. 签名加密算法有 MD5, SHA256 等
  3. 请求方发送请求: 请求方将 请求参数, 签名, 用户标识 (ak) 一起发送给服务端. 通常会把签名等元信息放到请求头参数中传递. 注意千万不要传递 secretKey.
  4. 服务端验证签名: 在 API 网关中, 通过请求头获取到相关参数, 再从数据库中查到该用户对应的 ak 和 sk, 并使用相同的签名算法生成签名, 和请求中的签名进行对比. 如果签名一致, 则服务端 (API 提供者) 可以信任请求方, 请求方可以进行后续操作.

!NOTE

: API 签名认证是一个很灵活的设计, 具体有哪些参数视情况而定. 通常情况有6个参数:

  • 参数1: ak (accessKey)

  • 参数2: sk (secretkey)

  • 参数3: 用户参数 (body)

  • 参数4: 随机数 (nonce)

  • 参数5: 时间戳 (timestamp)

  • 参数6: 签名 (sign)

共6个参数, 其中 sk 不能直接传递到服务端, 其中 (sk + 用户参数 + 随机数 + 时间戳) 通过签名加密算法生成签名.

所以最后向服务端传递的参数有: ++ak, 用户参数, 随机数, 时间戳, 签名++ 5个参数 (以请求头的方式传递).

四. 代码示例

(1) 请求方 (客户端 Client)
java 复制代码
import cn.hutool.core.util.RandomUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.wang.wangapiclientsdk.model.User;

import java.util.HashMap;
import java.util.Map;

import static com.wang.wangapiclientsdk.utils.SignUtil.genSign;


/**
 * @author yandong
 */
public class WangApiClient {
    private String accessKey;
    private String secretKey;

    public WangApiClient(String accessKey, String secretKey) {
        this.accessKey = accessKey;
        this.secretKey = secretKey;
    }

    /**
     * 向服务端发送请求 (并携带API签名认证)
     * @param user
     * @return
     */
    public String getUsernameByPost(User user) {
        String userJsonStr = JSONUtil.toJsonStr(user);
        HttpResponse response = HttpRequest.post("http://localhost:8080/name/postJson")
                .addHeaders(getHeaderMap(userJsonStr))
                .body(userJsonStr)
                .execute();
        System.out.println(response.getStatus());
        String result = response.body();
        System.out.println(result);

        return result;
    }

    /**
     * 构造请求头
     * @param body 用户参数
     * @return 请求头
     */
    private Map<String, String> getHeaderMap(String body) {
        Map<String, String> headerMap = new HashMap<>();
        headerMap.put("accessKey", accessKey); // ak (注意: sk一定不能发送给后端)
        headerMap.put("body", body); // 用户参数
        headerMap.put("nonce", RandomUtil.randomNumbers(4)); // 随机数
        headerMap.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000)); //时间戳
        headerMap.put("sign", genSign(body, secretKey)); // 签名

        return headerMap;
    }
}
(2) API 提供方 (服务端 Server)
java 复制代码
import cn.hutool.json.JSONUtil;
import com.wang.wangapiclientsdk.model.User;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import static com.wang.wangapiclientsdk.utils.SignUtil.genSign;

@RestController
@RequestMapping("/name")
public class NameController {

    @PostMapping("/postJson")
    public String getUsernameByPost(@RequestBody User user, HttpServletRequest request) {
        String accessKey = request.getHeader("accessKey");
        String body = request.getHeader("body");
        String nonce = request.getHeader("nonce");
        String timestamp = request.getHeader("timestamp");
        String sign = request.getHeader("sign");


        /*
        (1) 校验 ak
        TODO 实际情况是从数据库中查出 ak
         */
        if (!StringUtils.hasText(accessKey)) {
            throw new RuntimeException("accessKey不能为空");
        }

        /*
        (2) 校验body  确保header中的body与实际请求体一致, 防篡改
         */
        if (!StringUtils.hasText(body)) {
            throw new RuntimeException("body不能为空");
        }
        try {
            // 将请求体User对象序列化为JSON, 与header中的body对比
            String requestBodyJson = JSONUtil.toJsonStr(user);
            if (!body.equals(requestBodyJson)) {
                throw new RuntimeException("请求体与body不一致,可能被篡改");
            }
        } catch (RuntimeException e) {
            throw new RuntimeException("请求体序列化失败", e);
        }

        /*
        (3) 校验nonce随机数, 防重放攻击 (确保同一nonce在有效时间内不重复)
         */
        if (!StringUtils.hasText(nonce)) {
            throw new RuntimeException("nonce不能为空");
        }

        /*
        (4) 校验 timestamp 时间戳
        时间和当前时间间隔不能超过5分钟
         */
        if (!StringUtils.hasText(timestamp)) {
            throw new RuntimeException("timestamp不能为空");
        }

        /*
        (5) 校验签名
        TODO 实际情况是从数据库中查出 sk
         */
        if (!StringUtils.hasText(sign)) {
            throw new RuntimeException("sign不能为空");
        }
        String serverSign = genSign(body, "123456");
        if (!sign.equals(serverSign)) {
            throw new RuntimeException("签名不一致, 无权限");
        }

        return "POST JSON 你的名字是 " + user.getUsername();
    }
}
相关推荐
兮山与3 小时前
算法6.0
算法
代码对我眨眼睛3 小时前
739. 每日温度 LeetCode 热题 HOT 100
算法·leetcode
程序员莫小特3 小时前
老题新解|计算2的N次方
开发语言·数据结构·算法·青少年编程·信息学奥赛一本通
wearegogog1235 小时前
基于块匹配的MATLAB视频去抖动算法
算法·matlab·音视频
十重幻想5 小时前
PTA6-1 使用函数求最大公约数(C)
c语言·数据结构·算法
大千AI助手6 小时前
蛙跳积分法:分子动力学模拟中的高效数值积分技术
算法·积分·数值积分·蛙跳积分法·牛顿力学系统·verlet积分算法
zycoder.7 小时前
力扣面试经典150题day3第五题(lc69),第六题(lc189)
算法·leetcode·面试
西阳未落8 小时前
LeetCode——双指针
c++·算法
胖咕噜的稞达鸭9 小时前
C++中的父继子承:继承方式实现栈及同名隐藏和函数重载的本质区别, 派生类的4个默认成员函数
java·c语言·开发语言·数据结构·c++·redis·算法