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

相关推荐
车载诊断技术21 分钟前
电子电气架构 --- 什么是EPS?
网络·人工智能·安全·架构·汽车·需求分析
武子康22 分钟前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
豪宇刘1 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
秋恬意1 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
刘大辉在路上2 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
FF在路上2 小时前
Knife4j调试实体类传参扁平化模式修改:default-flat-param-object: true
java·开发语言
真的很上进2 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
众拾达人3 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
皓木.3 小时前
Mybatis-Plus
java·开发语言
不良人天码星3 小时前
lombok插件不生效
java·开发语言·intellij-idea