鉴于当前日益严峻的行业竞争和用户的安全忧虑,我司近期上线了对全接口的安全监控,以下记录了方案设计过程中的一些想法。

HTTPS安全吗?

HTTPS可以在一定程度上隐藏接口调用的路径和参数,但是如果使用不当,依然会暴露在MIM攻击下,现在流行的网络调试工具都可以支持MIM。如Charles:

Surge:

在MIM攻击下,通过HTTPS的请求依旧一览无余。所以我们需要一个更为可靠的接口加密方案,来抵御爬虫等针对接口的攻击。

案例分析

先来看看当前市面上一个比较成熟的产品的接口机密方案。

可以看到所有的接口请求都带着一个key字段,这个字段用于验证这个请求是否合法,无论是修改 key字段或者param中携带的请求参数,都会返回签名认证失败的提示。

我们通过反编译部分代码后了解到,这个方案通过给明文参数添加一个混淆值之后执行一个单向hash算法sha1获取一个用于校验的字段key。

这个方案安全吗?

单向hash的缺点就在于不可逆(排除碰撞的可能),由于http是无状态的,计算单向hash传入的参数都需要显式传递给后端,或者取前后端预先约定好的一个值,这就意味着,如果计算单向hash的算法暴露了,只需要根据算法计算对应的hash值,就可以很容易的构造出一个合法的请求。 而且,如果方案暴露,所有接口都无一幸免,因为参数都是明文,对于所有请求加密都形同虚设。 在反编译技术已经非常非常成熟的今天,这个算法的暴露基本是时间问题。 具体反编译和分析过程在下一篇博文中介绍。

我们的方案设计

如图所示:

这里就不详叙AESRSA的相关细节。 大致可以认为,AES是一种对称加密算法,RSA是一种非对称加密算法,仅仅通过密文来试图解密明文都是不可能的(密钥长度足够)。 读懂了这张图,就基本能明白这个方案的设计。补充几点设计的细节:

为什么不采用案例中的方案

案例中的参数以明文的方式传输,如果hash值的计算算法被通过反编译暴露,所有接口都同时受到威胁。 这个方案可以在加密方案暴露之后,通过保护明文请求参数提高接口的安全。

为什么不直接对请求参数采用非对称加密

对称加密和非对称加密在计算消耗上存在显著的差距,具体可以参见:AES和RSA加密算法调研 如果所有接口都采用非对称加密,对客户端和服务器端都会带来巨大的计算压力,从而严重影响用户体验。

AES密钥的生成技巧

  • AES可以由客户端在每次请求的时候生成,确保每个请求生成的AES密钥的唯一性,后台通过校验AES密钥的唯一性可以监控到爬虫的重放攻击。为了监控AES密钥的唯一性,后台存放和查询的压力会逐渐提升,所以可以通过存放在redis的方式,并通过ttl来控制记录的存活时间。
  • 如上一条所述后台记录的AES密钥是有一定存活时间的,意味着超过这个存活时间,就无法监控到重放攻击,所以可以在AES密钥中携带时间信息,如果后台监控到当前时间和AES密钥中携带的时间超过了后台记录的存活时间,也可以记录为可疑请求。

是否安全?

如果要破解这个方案,需要同时满足以下条件:

  • 通过反编译或者其他方式获取了RSA的加密公钥
  • 通过反编译或者其他方式获取了AES密钥的生成规则
  • 通过反编译或者其他方式获取了请求参数的结构

同时满足着三个条件是有可能的,但是不同于上文中介绍的案例,在接口重放成本上是有很显著的差距的,如果配合后台监控得当,基本可以把恶意用户及时禁用。 如果客户端配合得当,通过代码混淆等方式增加反编译的难度,保护加密公钥,就能很大程度拦住恶意用户。

服务端如何无痛的接入当前的应用

想必所有接触过spring的用户脑袋里面都冒出了三个字:AOP。 没错,这个方案通过AOP可以做到很好的无痛接入,将切点定义在所有需要加密的请求方法,将解密逻辑存放在对应的Advise 中即可。主要有以下注意的地方:

确保这个切面具有最高的优先级

除了这个切面,系统中还存在很多业务相关的切面,比如校验登录,记录日志等,都需要明文的请求参数,所以我们要确保这个切面具有最高的优先级。 如果通过@Aspect注解来定义切面,有一个对应的注解@Order可以用于定义切面的优先级,值越小意味着优先级越高,在触发的时候会越早被执行。

兼容spring的表单校验

如果在接口方法参数中带了@Valid注解,spring会对对应的表单参数执行校验,校验结果存放在接口方法的Errors或者BindingResult中,因为加密之后,这个校验是肯定不会通过的(因为明文参数字段的值都为空),所以需要在Advise方法中重新执行校验:

1
2
3
Errors errors = new BeanPropertyBindingResult(newForm, formClazz.getName());
validator.validate(newForm, errors);
args[i] = errors;

或者:

1
2
3
BindingResult result = new BeanPropertyBindingResult(newForm, formClazz.getName());
validator.validate(newForm, result);
args[i] = result;

这个validator在spirng容器中定义,注入到当前Aspect

1
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />