API签名认证

API签名认证算法

为什么需要用到API签名认证算法?

API签名认证算法主要用于确保API请求的合法性参数的完整性以及防止重放攻击

比如我们在调用图床服务的时候,其实图传服务端就是使用了API签名认证算法,我们每次调用API的时候,本地的SDK会第一次为我们请求签名,当签名发送到服务端的时候

什么是重放攻击?

在这里插入图片描述

假如你的请求在通过一个代理服务器的时候被拦截了,攻击者可以在浏览器重复发起你之前发过的请求

为什么不能使用Token替代API签名认证?
  1. Token无法保证请求的完整性,Token内包含用户身份和访问权限信息,在传输过程中可能被截获并篡改,API签名认证后即使请求被篡改了也可以校验出来

  2. Token无法防范重放攻击

在实际应用中,很多系统会同时使用Token和API签名认证算法,以综合利用它们的优势。Token用于身份验证和授权,而API签名认证算法用于确保请求的完整性和安全性。这样的组合可以提供更全面的安全保障,特别是在处理敏感数据或执行重要业务逻辑的情况下。

签名流程
  1. 在用户在本地客户端对Body和SK进行MD5加密,将AK、serverSign(签名)、请求参数发送到网关层
  2. 网关层根据AK获取到SK,根据在客户端一样的签名流程,将加密出的签名与客户端上传的签名作比较,如果不相同,说明签名遭到了篡改,拒绝服务的请求
签名实现

签名的实现需要以下参数

accessKey(用户标识)

作用:识别用户身份

secretKey(用户密钥)

作用:结合accessKey,判断用户是否具有请求权限,不再请求中携带

sign(签名)

sign一般会使用单向加密算法实现,例如MD5,具体内容为MD5(请求体 + secretKey + timestamp)

作用:对请求签名,防止请求被篡改

timestamp(时间戳)

要注意客户端与服务端的时间差问题

作用:发起请求的时间,如果请求超过发起请求的5分钟,就判定请求失败

nonce(随机数)

作用:请求唯一标识,防止重放攻击,一般使用UUID实现,存储在服务端,配合timestamp使用,可以存放在缓存Redis中,超过timestamp时间就删除nonce

实操
客户端(SDK)
/**
 * 构造访问API的请求头
 */
public Map<String, String> getHeaderMap(String body) {
    Map<String, String> headerMap = new HashMap<>();
    headerMap.put("accessKey", accessKey);
    headerMap.put("nonce", RandomUtil.randomNumbers(12));
    headerMap.put("body", body);
    headerMap.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
    headerMap.put("sign", SignUtil.getSign(body, secretKey));
    return headerMap;
}

/**
*加密工具类
*/
public class SignUtil {
    public static String getSign(String body, String secretKey) {
        Digester md5 = new Digester(DigestAlgorithm.MD5);
        String content = body.toString() + "." + secretKey;
        return md5.digestHex(content);
    }
}
// 调用服务端API
public String getUserNameByPost(User user) {
        String json = JSONUtil.toJsonStr(user);
        String result2 = HttpRequest.post(GATE_WAY_HOST + "/api/name/user").addHeaders(getHeaderMap(json))
                .body(json)
                .execute().body();
        System.out.println(result2);
        return "Post用户的名字是:" + result2;
    }
服务端(网关层)
@Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1. 请求日志
        ServerHttpRequest request = exchange.getRequest();
        String path = INTERFACE_HOST + request.getPath().value();
        String method = request.getMethod().toString();
        log.info("请求唯一标识:" + request.getId());
        log.info("请求路径:" + path);
        log.info("请求方法:" + method);
        log.info("请求参数:" + request.getQueryParams());
        String sourceAddress = request.getLocalAddress().getHostString();
        log.info("请求来源地址:" + sourceAddress);
        log.info("请求来源地址:" + request.getRemoteAddress());
        ServerHttpResponse response = exchange.getResponse();
        // 3. 用户鉴权(判断 ak、sk 是否合法)
        HttpHeaders headers = request.getHeaders();
        String accessKey = headers.getFirst("accessKey");
        String nonce = headers.getFirst("nonce");
        String timestamp = headers.getFirst("timestamp");
        String sign = headers.getFirst("sign");
        String body = headers.getFirst("body");
        // 去数据库根据accessKey查询用户
        User invokeUser = null;
        try {
            invokeUser = innerUserService.getInvokeUser(accessKey);
        } catch (Exception e) {
            log.error("getInvokeUser error", e);
        }
        if (invokeUser == null) {
            return handleNoAuth(response);
        }

        // 时间和当前时间不能超过 5 分钟
        long currentTime = System.currentTimeMillis() / 1000;
        final long FIVE_MINUTES = 60 * 5L;
        if ((currentTime - Long.parseLong(timestamp)) >= FIVE_MINUTES) {
            return handleNoAuth(response);
        }

        // 校验nonce, 判断是否是重放攻击
        boolean nonceExisted = Boolean.TRUE.equals(redisTemplate.hasKey(SIGN_KEY + nonce + timestamp));
        if (nonceExisted) {
            return handleNoAuth(response);
        }
        // 从数据库中查出 secretKey与加密后的字符进行比较
        String secretKey = invokeUser.getSecretKey();
        String serverSign = SignUtil.getSign(body, secretKey);
        if (sign == null || !sign.equals(serverSign)) {
            return handleNoAuth(response);
        }

        // 4. 请求的模拟接口是否存在,以及请求方法是否匹配
        InterfaceInfo interfaceInfo = null;
        try {
            interfaceInfo = innerInterfaceInfoService.getInterfaceInfo(path, method);
        } catch (Exception e) {
            log.error("getInterfaceInfo error", e);
        }
        if (interfaceInfo == null) {
            return handleNoAuth(response);
        }
        // todo 是否还有调用次数
        // 5.判断用户是否还有调用次数
        boolean hasInvokeNum = innerUserInterfaceInfoService.hasInvokeNum(interfaceInfo.getId(), invokeUser.getId());
        if (!hasInvokeNum) { // 没有调用次数
            return handleNoAuth(response);
        }

        // 将nonce存入redis, 设置超时时间为5min
        redisTemplate.opsForValue().set(SIGN_KEY + nonce + timestamp, nonce, FIVE_MINUTES, TimeUnit.SECONDS);
        // 5. 请求转发,调用模拟接口
        return handleResponse(exchange, chain, interfaceInfo.getId(), invokeUser.getId());

    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值