likes
comments
collection
share

API接口验签原理与设计

作者站长头像
站长
· 阅读数 1

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

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

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

API接口验签原理与设计 这种情况,我们就需要对api接口进行额外的校验。

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

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

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

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

API接口验签原理与设计 代码示例如下:

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