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

相关推荐
摇滚侠4 小时前
《Java并发编程之美》第 2 章 并发编程的其他基础知识 阅读笔记 3
android·java·笔记
墨子夕4 小时前
设计模式——Java × Python 双语言精讲 · 23 个经典模式 + 现代演进(第一篇)
java·python·设计模式
程序 代码狂人5 小时前
经典 SQL 面试题:连续登录问题的两种经典解法
java·开发语言·算法
门思科技5 小时前
ThinkLink 触发模型与告警规则实战:数据一小时才来一次,告警怎么做到不迟到、不刷屏、找对人
前端·后端
weixin_BYSJ19875 小时前
【计算机毕设】房屋租赁系统02647
java·vue.js·spring boot·python·django·课程设计
站大爷IP5 小时前
Python的编码解码把我坑惨了,原来encode和decode的区别这么大
后端
明月_清风5 小时前
拆解 DeepSeek 最新开源推理框架 DeepInfer:万亿级大模型的高效落地实战
人工智能·后端·deepseek
VX_bysjlw9855 小时前
基于协同过滤的美食推荐系统的设计与实现32221-计算机毕设原创(免费领源码+带部署教程)
java·spring boot·redis·mysql·tomcat·maven·mybatis
Thneonl5 小时前
选举之后,数据去哪了:MongoDB rollback 目录里的失踪写入
后端·架构