API接口验签原理与设计

关于接口安全的设计有很多,比如token检验,对参数的安全性校验等等,但在一些安全性要求更高的业务要求中,常用的一些校验可能显得安全性不太够,所以就有了对api的验签。 简单来说就是在我们正常的访问请求下,再加一层对所有api进行校验的校验层。

我们最简易的一个流程大概就是如下,通过登录进行一个用户信息的校验,那用token去请求业务的系统进行业务操作。

但这种校验方式在很多安全性要求较高的情况下就不太适用。举个简单的例子,比如我们要转账100块给b,结果这个请求被'中间人'拦截,将b的信息改成c在重新去请求业务系统,在业务系统看来这个请求token也是对的,信息也没问题,最后就导致原本给b的钱转给了c。

这种情况,我们就需要对api接口进行额外的校验。

以上述案例,举一个简易的验签方案

首先我们要进行加密或者叫加签,由于是为了防止信息的篡改,所以可以选择对我们的信息进行一个加密,生成密文放在请求头中。加密的方式有很多,简单点可以进行一个md5的加密,更复杂可以进行非对称加密。这里我们可以先选择md5的方式进行加密。

验签就简单了,系统收到请求,根据信息进行一个相同md5函数的加密生成密文与请求头中的密文进行比较,相同就验签成功,不同则验签失败。看起来好像确实没什么问题了,也保护了参数不会被改变。

但是为了防止通过一些数据分析的方式,破解出加密的形式,所以可以选择在加密时多添加一个随机数,如果为了更安全还可以添加时间戳,每次请求验签成功时,将随机数和时间戳缓存,如果规定时间内有两次相同的请求则会之后的请求拦截,简易的防止重放的校验就完成了。最后的流程就可以变成这样。

代码示例如下:

java 复制代码
@Slf4j
public class InterfaceInterceptor implements HandlerInterceptor {

    private static final Integer SECONDS = 60 * 10;

    private static final Integer TIME_ALIVE = 5;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String servletPath = request.getServletPath();
        LogUtil.info(log, "InterfaceInterceptor.preHandle >> 进入拦截, {} ,{}", servletPath, handler.getClass().getName());
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        if (!StringUtils.containsIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
            LogUtil.error(log, "InterfaceInterceptor.preHandle >> 错误,当前不是JSON数据, contentType={}", Strings.nullToEmpty(request.getContentType()));
            //登录失效异常
            WebUtil.sendResponse(response, BaseResponse.failure(ErrorCodeEnum.INTERFACE_INVALID));
            return false;
        }
        String body = "{}";
        try {
            body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
        } catch (Exception e) {
            LogUtil.error(log, "InterfaceInterceptor.preHandle >> 对象解析失败 ,body={}", body, e);
            // 对象解析失败
            WebUtil.sendResponse(response, BaseResponse.failure(ErrorCodeEnum.INTERFACE_INVALID));
            return false;
        }
        TreeMap<String, Object> sortedMap = Maps.newTreeMap();

        //请求头中取出签名、时间戳和随机字符串
        final String signature = ServletUtil.getHeaderIgnoreCase(request, CommonConstant.SIGN);
        final String timestamp = ServletUtil.getHeaderIgnoreCase(request, CommonConstant.TIMESTAMP);
        final String nonce = ServletUtil.getHeaderIgnoreCase(request, CommonConstant.NONCE);

        String signMd5 = this.getMd5Sign(servletPath, body, sortedMap, nonce, timestamp);
        if (!signMd5.equalsIgnoreCase(signature)) {
            LogUtil.warn(log, "InterfaceInterceptor.preHandle >> 接口预校验 >> signature error, expected signature is {}", signMd5);
            WebUtil.sendResponse(response, BaseResponse.failure(ErrorCodeEnum.INTERFACE_INVALID));
            return false;
        }
        //判断时间是否是60s内
        long nowTime = DateUtil.currentSeconds();
        long reqTime = Long.parseLong(timestamp);
        long abs = Math.abs(nowTime - reqTime);
        if (abs > SECONDS) {
            LogUtil.warn(log, "InterfaceInterceptor.preHandle >> 接口预校验 >> 时间校验, 时间差值超过10分钟, 检查手机系统时间, nowTime={}, reqTime={} ", nowTime, reqTime);
            WebUtil.sendResponse(response, BaseResponse.failure(ErrorCodeEnum.INTERFACE_INVALID, "请校验手机系统时间是否准确"));
            return false;
        }

        RedissonClient redissonClient = SpringUtil.getBean(RedissonClient.class);
        RBucket<String> bucket = redissonClient.getBucket(StrUtil.format(RedisPrefixConstant.INTERFACE_NONCE_REDIS_KEY, nonce));
        if (bucket.isExists()) {
            LogUtil.warn(log, "InterfaceInterceptor.preHandle >> 缓存存在 , nonce={}", nonce);
            WebUtil.sendResponse(response, BaseResponse.failure(ErrorCodeEnum.INTERFACE_INVALID));
            return false;
        }
        //存入缓存
        bucket.set(nonce, TIME_ALIVE, TimeUnit.MINUTES);
        return true;
    }
private String getMd5Sign(String servletPath, String body, TreeMap<String, Object> sortedMap, String nonce, String timestamp) {
  ....md5校验
}  

总而言之,API接口验签主要点在于加密或者叫加签的过程,根据不同的业务需求可以对不同的参数进行加签,如果是为了防止参数修改就可以对参数进行一个加签,如果是别的情况就可以选择别的加签对象。至于加签的算法,就仁者见仁智者见智了,每个人都有自己的看法😀。

相关推荐
vx-Biye_Design4 小时前
SSM伴侣动物伴护星小程序06330-计算机课程设计、毕业设计
spring boot·后端·elasticsearch·小程序·架构·课程设计·idea
2分钟速写快排6 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
面向Google编程6 小时前
Apache Iceberg Variant 类型:v3 半结构化数据不再「二选一」
大数据·后端
lhldsg7 小时前
全民健身解决方案小程序开发:从0到1的技术实战
java·数据库·需求分析
X54先生(人文科技)7 小时前
ELR-SELLM Edge 神经元网络架构评估报告
人工智能·深度学习·架构·开源
jason成都7 小时前
Spring WebFlux 适配达梦新方案|dm‑r2dbc:Netty 异步传输的实验性 R2DBC 驱动
java·后端·spring
泡海椒7 小时前
内置SPI函数库详解:JQuick-Java Builtin工具类实战用法
java·开发语言·python
辰烨chenye7 小时前
LeetCode Hot 100 题解 · 二分篇
java·算法·leetcode
小羊没烦恼!8 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
小陈不好吃8 小时前
从单体到微服务:Spring Cloud Gateway 动态路由实战与踩坑记录
微服务·云原生·架构