2011年全球Android装置出货量可能突破2亿台,俨然,我们已经进入了智能移动时代了。
在之前,我们总说21世纪,人才跟信息最值钱。撇开人才不讲,我觉得进入移动时代之后,因为屏幕大小、使用习惯限制,人们已经很少会在手机设备上打开Google来搜索一个东西了,而是去应用市场上下载一个相关的应用。在这种环境下,从互联网上收集、整理信息然后开发一个应用来提供这些信息显然已经成为一件很有意义的事情了。
但是在国内这种抄袭成风的环境之下,我们辛辛苦苦收集整理来的数据,很可能一夜之间就被人通过提供给客户端使用的接口给爬走了,显然,通过一定的方式把服务器端的数据保护起来是非常有必要的。这里,我就介绍一下我自己的做法。
首先,我认为,保护服务器端的数据,有这么几个关键点:
- 不能对使用体验产生影响,这就排除掉了诸如每次接口调用都要求用户输入验证码这样的做法
- 接口调用的网络交互需要无规律可循,比如article/1 –> article/1000 这样的接口就太容易被其他人爬走了
- 要严格意义上阻击爬虫,需要每一次网络请求都是不可重放的,这样才能避免其他人通过监听网络交互并重放来爬取数据
- 对服务器端编码不产生太大影响,如果要对服务器端伤筋动骨的大改,肯定是要不得的
通常,我们会采用一种简单有效的方法:对服务器返回的数据加密来解决,但是,这种做法并没有解决上面所提到的第二点,接口调用的时候url的规律性太强,网络监听一下数据,就很容易找到url地址的规律了,加密的破解也很简单,反编译直接定位到解密函数,拿到密钥。当然,在强大的反编译工程面前,一切努力都是徒劳的,不管你用何种方法,都是可以把中间的逻辑找到并模拟成一个客户端来爬数据的。
我下面就提出一个破解更加复杂一些的方法,在客户端产生请求时,对接口url进行RSA加密处理。
假设我们本来需要访问 http://api.example.com/articles 这样的一个接口,接口返回json数据。在客户端访问之前,我们先对这个url进行这样的处理:
- 加客户端时间戳:http://api.example.com/1322470148/articles
- 对url的path段进行rsa加密,然后base64:http://api.example.com/TBhIskCgCN+WMK3PftbYzPQFAKvx9sE9OMOxvL00kCBlNiKw2C1Mb7oGcfUepTxauG06NLBNhr5BFtjt7Xu7uwdpUYyVcFRdI37SVyGRCOzaxACOGXGpX5dHZqQJia0icxwWJ+D1RiJqxFWQ++3/IgUOgDzgvQnPIl420bpztB8=
我们真实访问的地址就变成了这样一个长长的 url 结构,我们通过rsa算法的padding参数和时间戳,就可以让这个后面长长的bas64串在每次访问的时候都发生变化,同时,我们可以在服务器端把一个小时之内的请求过的串都记下来,并不让再次访问,这样就防止了爬虫的重放请求尝试。
在服务器端,我们就需要在做响应之前,把url还原回来。在服务器端,现在都是框架的天下,一般都有唯一的入口,如果使用的是php语言,主要在入口的index.php加上一些代码就可以了:
if ($_SERVER['HTTP_HOST'] == "api.example.com"){ // 只针对api这个域名做处理 include_once dirname(__FILE__).'/protected/components/EncryptUtil.php'; // 加解密库,你需要实现你自己的加解密类 $request_uri = $_SERVER['REQUEST_URI']; if(isset($_SERVER['HTTP_HOST'])){ if(strpos($request_uri,$_SERVER['HTTP_HOST'])!==false){ // 把 REQUEST_URI 中可能包含的host信息去除掉 $request_uri=preg_replace('/^\w+:\/\/[^\/]+/','',$request_uri); } } $encoded = base64_decode(substr($request_uri, 1)); if($encoded && strlen($encoded) % 128 ===0){ $real_uri = EncryptUtil::private_decrypt($encoded); // 解密url路径 if(!$real_uri){ echo ":)"; return; } // 解密失败 if(preg_match("/([0-9]+)\\/(.+)/", $real_uri, $matches)){ // 提取出时间戳和真实的url请求地址 $timestamp = $matches[1]; // 客户端请求的时间戳 $real_uri = $matches[2]; // 客户端请求的真实地址 $_SERVER['REQUEST_URI'] = $real_uri; // 置上本来应该有的全局$_SERVER['REQUEST_URI'] if(preg_match("/^[^?]+\\?(.+)/", $real_uri, $matches)){ $_SERVER['QUERY_STRING'] = $matches[1]; // 置上本来应该有的全局$_SERVER['QUERY_STRING'] parse_str($_SERVER['QUERY_STRING'], $array); $_REQUEST = array_merge($_REQUEST, $array); // 置上本来应该被设置的全局$_REQUEST $_GET = array_merge($_GET, $array); // 置上本来应该被设置的全局$_GET } }else{ // url的格式不符合,没有包含时间戳 echo ":)"; return; } }else{ // url的长度不符合规则 echo ":)"; return; } }
在经过这样一段代码处理之后,框架就一切正常,其他代码都不需要做变更,就有了rsa加密的url支持,当然,这几行代码还是不能阻止重放攻击的,里面并没有对请求过的url进行记录处理,要实现url访问的唯一性,还需要额外的更多代码。
服务器端完成了,那客户端也同样需要做相应操作,我这里就不详细讲解了,贴上一段修改过的实际运行的代码,IOS,应用了 three20库,并兼容TTURLRequest缓存机制。
// ApiRequest.h文件 #import "Three20Network/TTURLRequest.h" @interface BaiyiApiRequest : TTURLRequest{ NSString* urlPathBeforeEncoded; } @property (nonatomic, retain) NSString* urlPathBeforeEncoded; + (BaiyiApiRequest*)requestWithURL:(NSString*)URL delegate:(id)delegate; @end // .m文件 #import "ApiRequest.m" #import "EncryptUtil.h" // 你需要实现你自己的加密库 #import "Base64.h" #import "extThree20JSON/extThree20JSON.h" @implementation ApiRequest @synthesize urlPathBeforeEncoded; #define API_ROOT @"http://api.example.com/" + (NSString*)encodeUrl:(NSString*) urlString{ // 对传入的url地址进行加密处理,返回加密之后的url地址 if ([urlString isContains:API_ROOT]) { // 这是一个接口请求访问,进行转化处理 urlString = [urlString substringFromIndex:[API_ROOT length]]; NSString* newURL = [NSString stringWithFormat:@"%lld/%@", [[NSDate date] timeIntervalSince1970], urlString]; // 加入时间戳 return [API_ROOT stringByAppendingString:[Base64 encode:[EncryptUtil rsaEncryptString:newURL]]]; } else { return urlString; } } - (NSString*)generateCacheKey{ // 重载 TTURLRequest 的 generateCacheKey 函数,这是与 TTURLRequest 缓存兼容的关键 NSString* urlPathEncoded = [super urlPath]; [super setUrlPath:self.urlPathBeforeEncoded]; NSString* generatedKey = [super generateCacheKey]; [super setUrlPath:urlPathEncoded]; return generatedKey; } - (void) setUrlPath:(NSString *)urlPath{ // 重载 TTURLRequest 的 setUrlPath 函数,对传入的url进行转换加密处理 self.urlPathBeforeEncoded = urlPath; NSString* encoded = [self.class encodeUrl:urlPath]; [super setUrlPath:encoded]; } + (ApiRequest*)requestWithURL:(NSString*)URL delegate:(id)delegate { return [[[self alloc] initWithURL:URL delegate:delegate] autorelease]; }
Android的Java版本我就把实际运行中的代码的http部分抽离出来,因为牵涉到一些相关配置,代码不能正常编译,不过也放在这里,以供参考。
用法示例:
BaiyiApiRequest request = new BaiyiApiRequest("articles/1"); request.setListener(this); request.start();
总体来说过于复杂,也不利于排错,与直接加密返回数据比较,并没有明显优势,但也不失为一种思路