您的位置:首页 > 其它

谈谈移动应用的安全性实践

2017-12-27 11:01 218 查看
作为一家大数据公司,Glow不仅重视用户的数据,更加注重数据的安全性。

本文将从用户注册流程出发,逐步介绍我们在提高数据安全性方面采用的一些策略方法,供读者参考。下面将从 
Android
 和 
服务端
 两部分来进行讲解。


从注册说起

用户第一次打开app时便会进入注册页面,然后客户端会要求用户输入用户名、密码并传递给服务端去创建一个新的user。此时若通过明文传递用户名密码便是一个安全性隐患。或者说,如果有人监听注册API,那么很快就可以窃取到很多用户的账户数据,而且可以偷偷利用这些账户信息随时获取甚至更改用户数据。

这对于任何一家企业而言都是非常可怕的。


全站Https

因此,为了应对数据明文传输隐患这个问题,我们在自家所有app中都采用了Https方式通信。在Android端我们采用了Square家的OkHttp3作为网络层,为应用层提供Https服务。

下面先对Https的基本工作原理进行下介绍,以便后文的讲解。
首先,客户端去请求服务端的
数字证书
,这个证书包含了一个公钥。该证书购买后存储于我们自己服务器上。
当服务端收到客户端请求后,会把这个数字证书回传给客户端,由于是公钥,所以不害怕被窃取。
客户端收到数字证书后,先去
验证
证书的真实性。如果验证通过,就会从里面取出一个
公钥

客户端本地生成一个
随机数
,作为未来的
会话私钥
,利用前面的公钥进行
加密

客户端把
加密后会话私钥
回传给服务端,在这个过程中,即使
加密后的会话私钥
被窃取也不用担心,因为中间人并没有
解密私钥
,所以读不出里面的
会话私钥

服务端接收到
加密会话私钥
后,利用从CA购买证书时获得的
解密私钥
进行解密读出
真实会话私钥
。至此,客户端与服务端同时拥有了一个只有它们二者知道的
会话私钥
,非对称加密连接建立完成。
一旦客户端和服务端连接建立起来后,未来的数据通信都利用这个
会话私钥
进行对称加密传输数据。

采用了https后,我们所有网络传输的数据都由明文变成了密文,即使中间有人能够监听到数据包,也不能轻易获取user的帐户密码信息。

听起来,安全性问题基本解决了。

然而实际上,在步骤3用户需要去验证数字证书时,如果这个验证过程被欺骗了呢

试想这样一种场景,如果在最开始,攻击者就拦截掉客户端与服务端的通信。当客户端在请求证书时,攻击者回传一个他自己的
假证书
,而且攻击者已经通过其他手段欺骗用户在手机上
信任
了这个
假证书
,那么当客户端接收到证书并去验证时,是可以通过的

这也就意味着,一旦客户端遭受这样的攻击,未来客户端都会与一个
虚假的中间人
通信,而且中间人也可以拿着客户端传来的信息去与我们的服务端通信,而这个过程
客户端和我们服务端完全不知道中间人的存在
,这是很大的安全隐患。


公钥绑定(SSL Pinning)

为了防止客户端被虚假证书欺骗,可以采取的方式是把我们自己的公钥直接绑定给客户端,当客户端收到证书后,与提前绑定好的公钥进行验证,从而防止
虚假证书
的入侵。

在Android端,我们利用
OkHttp3
提供的
CertificatePinner
实现
公钥绑定

OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(
new CertificatePinner.Builder().add("your_host", "your_public_key").build())
.build();


至此,我们可以利用更为安全的https协议来传输用户名和密码,我们继续来看上面的注册流程。


Token机制

回到注册流程。当服务端拿到用户名密码后,会去创建一个新的
user
,同时我们会基于用户相关信息生成一个
Token
并回传给客户端。客户端在接收到
Token
后需要在本地进行存储。另外,由于每个http请求都是无状态的,因此未来客户端如果想把
user
信息传递给服务端时,就必须通过
Token
来传递,才能识别出某个请求的来源。

那么,我们应该如何在Android和服务端的代码里具体实现Token的
传递
解析
有效性验证机制
呢?

1. 首先在Android端,为了把
Token
信息存入到所有请求的
http
header
里,我们采用了
okhttp3
提供的
interceptor接口
来。
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request.Builder newRequestBuilder = request.newBuilder();
String token = getAuthToken();
if (!TextUtils.isEmpty(token)) {
newRequestBuilder.addHeader("Authorization", token);
}

Request newRequest = newRequestBuilder.build();
return chain.proceed(newRequest);
}
})
.build();


2. 然后在服务端,我们需要
解析
客户端传递过来的Token信息并进行
校验
。这里可以创建一个
python
decorator
方法:
def mobile_request(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
kwargs = kwargs if kwargs or {}
if request.headers.get('Authorization'):
encrypted_token = request.headers.get('Authorization')
isValid, user_info = check_token(encrypted_token) //解析并验证token有效性
if not isValid:
abort(498) //token无效,返回498状态码
user = get_user(user_info)
if not user:
abort(403) //找不到user,返回403状态码
kwargs['user_info'] = user_info //成功解析出user_info
return func(**kwargs)
return wrapped

@app.route("/www/index")
@mobile_request // 使用decorator包装方法
def get_user(**kwargs):
user_info = kwargs['user_info'] // 取出decorator中封装好的user_info
return db.get_user(user_info) // 利用user_info进行逻辑处理


3. 最后,请求结果返回到客户端,如果通过监测状态码发现返回结果是与Token相关的
error/异常
,则表示
Token失效
,此时我们让用户强制重新登录,生成新Token。这一步仍然可以在上面的
interceptor
里进行。
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
... //put token into newRequest
Response response = chain.proceed(newRequest); // 获取服务端返回结果
switch(response.code()) {
case ResponseCode.USER_NOT_FOUND: // 状态码: 403 找不到user
eventBus.post(new UserNotFoundEvent()); // 强制logout
break;
case ResponseCode.TOKEN_EXPIRED: // 498 token失效
eventBus.post(new TokenExpiredEvent()); // 强制logout
break;
default:
break;
}
return response;
}
})
.build();


至此,我们完成了Android端和服务端的Token传递、解析和失效处理。

在完善了Token的管理机制后,我们未来的http请求中只要带上这个Token,就可以畅通无阻地去服务端做与自身user相关的各种操作了。

那么,既然Token像家里门禁卡一样,只要拥有就能进入我们服务端并获取这个特定user的所有数据。那也就意味着,一旦攻击者窃取了某个user的Token,那在Token失效前,攻击者随时可以利用这个Token获取这个user的一切信息。

遇到Token被盗,该怎么办呢?


调整Token过期时间

针对Token被盗这种威胁,我们可以缩短Token的过期时间的方法。这样即使一个Token泄漏了,在一段时间后,这个Token也会自动失效。当然这也做会需要用户频繁登录获取新Token;而且失效前的这段时间内,攻击者仍然是可以直接连上服务端随意获取数据的。


Request签名

这种方法也是OAuth推荐的一种方法,其原理是在客户端和服务端统一好某种加密方法和一个密钥,这个密钥同时存储在客户端和服务端。每次客户端准备发起一个请求时,利用这种加密算法和密钥,针对该请求的API和参数进行计算得到一个数,称之为这个
Request的签名
,然后我们把这个
签名
放入到Request中。当服务端接收到Request后,就可以利用相同的加密算法和密钥来验证其中签名的真实性。
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
String sign = RequestSignUtil.sign(request);
HttpUrl url = request.url().newBuilder()
.addQueryParameter("request_sign", sign)
.build();
Request newRequest = request.newBuilder().url(url).build();
return chain.proceed(newRequest);
}
})
.build();


通过对每一个Request签名,可以确保服务端接收到的所有Request都来自我们自己的客户端。即使有人得到了Token想伪造Request,他也不知道如何计算Request签名,从而减小了Token被盗的危害。

当然,每种安全方法都有漏洞,Request签名的方法意味着我们必须在客户端保存好加密算法和密钥,可以通过代码混淆、密钥存储到.so文件等方法来提高破解难度,这里就不再细述了。


小结

上文中,从注册流程开始,介绍了我们在数据安全性方面采取的一些策略和相关实现代码,希望能对读者有帮助。

最后,笔者认为没有完美的安全策略来确保万无一失,不过我们所做的每一步都能够加大被攻击的难度。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: