本文首发在 Glow Tech Blog
虽然没有完美的安全性,但我们所做的每一步都能加大被攻击的难度。
本文将从用户注册流程出发,介绍下个人实践中在提高数据安全性方面采用的一些策略方法,供读者参考。下文将从 Android
和 服务端
两部分来进行讲解。
从注册说起
用户第一次打开app时便会进入注册页面。然后客户端会要求用户输入用户名、密码并传递给服务端去创建一个新的user。此时通过明文传递用户名密码便是一个安全性隐患。或者说,如果有人监听注册API,那么很快就可以窃取到很多用户的账户信息,而且可以偷偷利用这些账户信息随时获取甚至更改用户数据。
这对于任何一家企业而言都是非常可怕的。
全站Https
因此,为了应对数据明文传输隐患这个问题,我们可以采用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_id
等信息传递给服务端时,就必须通过Token
来传递,才能识别出某个请求的来源。
那么,我们应该如何在Android和服务端的代码里具体实现Token的传递
、解析
及有效性验证机制
呢?
1. 首先在Android端,为了把Token
信息存入到所有请求的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_id = check_token(encrypted_token) //解析并验证token有效性
if not isValid:
abort(498) //token无效,返回498状态码
user = get_user_by_id(user_id)
if not user:
abort(1001) //找不到user,自定义601状态码
kwargs['user_id'] = user_id //成功解析出user_id
return func(**kwargs)
return wrapped
@app.route("/www/index")
@mobile_request // 使用decorator包装方法
def get_user(**kwargs):
user_id = kwargs['user_id'] // 取出decorator中封装好的user_id
return db.get_user(user_id) // 利用user_id进行逻辑处理
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: // 自定义状态码: 1001 找不到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文件等方法来提高破解难度,这里就不再细述了。
小结
上文中,从注册流程开始,介绍了我们在数据安全性方面采取的一些策略和相关实现代码,希望能对读者有帮助。
最后,笔者认为虽然没有完美的安全性,但我们所做的每一步都能加大被攻击的难度。
如果有问题欢迎联系我。
谢谢!
wingjay