Android接口AES加密实践
需求
为了安全考虑,Android端的接口需要做加密
刚接到这个需求时,心里一惊。因为这一块,算是属于知识的盲区。不过也正好趁着这个机会,学一下关于接口加密相关的知识。
AES加解密
加密方法分好多种,因为后端同学和ios端已经接入了AES加密,所以这里主要介绍AES加密。
关于AES加密,重点还是密钥,客户端和后台根据对应的密钥进行加解密。
而接口加密,并不是加密整个接口。
www.baidu.com/search?=android
例如这个接口,如果需要对其进行加密的话,一般情况下都是对"search?=android"进行加密。加密的方法一般是由后台和前端统一商量(大部分还都是后端做决定的)。
加密逻辑
网络请求大抵分两种,一种是GET
,另一种是POST
。注:当然还有其他的的请求,如PUT、DELETE,但大多数开发情况下,只会用到这两种方式。
GET
请求是将参数query拼接在url后面,而POST
则是将参数存放在requestBody中。所以这两种方法加密逻辑是不一样的。
GET
按着后端同学提供出来的文档可以知道,GET
请求分两种情况,如果参数为空,则不需要加密,直接请求;如果参数不为空,则将所有参数加密,将加密后的字符串拼接在域名后面,
例如,
原链接为www.baidu.com/search?=android
那么,加密后格式为www.baidu.com/url?key=abcdefg
其中”abcdefg“为加密后的字符串
POST
因为post请求所有参数都在body里,所以只要对整个body进行加密,然后body传递加密后的字符串就行了。
Response
所有响应接口返回的数据都进行Base64编码及Aes解密。
其他的逻辑
这里提到的加密逻辑,只是和后端协商后得到的,当然还有其他的加密方式,像可以在接口处拼接时间戳,将token进行MD5,再拼接在url上,或者其他的加密方式(RSA等方式)。
代码
直接在拦截器统一处理GET
和POST
请求。
1、判断方法
2、如果是GET,则获取所有的query,否则直接第5步 3、将获取的参数转为json格式并加密 4、拼接 5、如果是POST,获取body数据 6、将body转为json格式字符串并加密 7、将字符串作为请求体post请求 8、返回response的data数据解密1、在拦截器内,通过val originalRequest = chain.request()
方法获取请求链接,再通过 originalRequest.method()
方法获取请求的方式。
2、通过val url = originalRequest.url()
获取请求的url链接,再通过 url.queryParameterNames()
获取到此次GET请求的所有参数名,通过url.queryParameter(QueryName)
获取参数名对应的参数值。
3、将所有参数转换为json格式,然后通过密钥进行加密操作。
4、拼接。这里用密钥对json字符串进行aes加密,然后对链接进行拼接。可以通过url.encodePath()
获取路径。
5、POST请求同理
6、将response的body的data数据解密
7、将response的body替换,并返回response
问题
1、POST请求的body方法不能直接获取。
2、有一点疑惑的是,什么叫将body加密,再把加密后的字符串作为body传值。
3、response并不能直接设置body。
解决
1、这个容易处理,网上有很多方案。
2、一般POST请求,会通过
data.toJson().toRequestBody("application/json;charset=UTF-8".toMediaTypeOrNull())
或者
RequestBody。create(MediaType.parse("application/json;charset=utf-8"), data.toJson())
这两种方法,都将数据转为body,而通过POSTMAN可以看到此时body是一串json字符串
{”name“:"xiaoming", age: 13}
。
那,怎么讲json转化,然后将加密后的内容重新作为body传值呢?
可能聪明的大家一下子想到了,但当时coding的时候,我脑子一时间没转过来。
既然data
可以toRequestBody转换成body的值,那么,加密串呢?
AesUtil.decrypt(data).toRequestBody("application/json;charset=UTF-8".toMediaTypeOrNull())
发现这样也是行的,body里可以传任何数据,但它必须是RequestBody格式。
3、原来的val response = chain.proceed(originalRequest)
中的response是不能设置body的,那么,解密后的数据只能用新建的response。
val newResponse = okhttp3.Response.Builder()
将老的response的code、message赋值给newResponse,设置body,当然还不要忘记了设置newRepsonse的request和protocol,不然后报错。
相关代码
以下是拦截器内的代码
var originalRequest = chain.request()
val builder = originalRequest.newBuilder()
builder.header("token", token)
val url = originalRequest.url()
val path = url.encodedPath()
val query = url.encodedQuery()
val method = originalRequest.method()
when (method) {
"GET" -> {
if (!query.isNullOrEmpty()) {
//请求参数不为空:GET请求接口加密为全参数加密,具体格式为 url?key=加密串
val queryList = url.queryParameterNames()
val map = HashMap<String, String>()
val iterator = queryList.iterator()
for (i in queryList.indices) {
val queryName: String = iterator.next()
map[queryName] = url.queryParameter(queryName) ?: ""
}
val s = gson.toJson(map)
val newBody = AesUtil.encrypt(s, KEY)
val newQuery = "?key=${URLEncoder.encode(newBody)}"
val newUrl = BASE_URL + path + newQuery
builder.url(newUrl)
runOnUiThread {
tvNew?.text = newUrl
}
} else {
//请求参数为空:不需要进行加密,直接请求
}
}
"POST" -> {
val body = originalRequest.body()
//暂不考虑formBody的情况,因为表单格式提交的post请求,不需要加密
val buffer = okio.Buffer()
body?.writeTo(buffer)
var charset = Charset.forName("utf-8")
val contentType = body?.contentType()
if (contentType != null) {
charset = contentType.charset(Charset.forName("utf-8"))
}
val data = buffer.readString(charset)
runOnUiThread {
tvOrigin?.text = data
}
val newBody =
AesUtil.encrypt(data, KEY) ?: ""
runOnUiThread {
tvNew?.text = newBody
}
val requestBody = RequestBody.create(
MediaType.parse("application/json;charset=utf-8"),
newBody
)
builder.post(requestBody)
}
}
originalRequest = builder.build()
val response = chain.proceed(originalRequest)
val responseBody = response.body()
var charset = Charset.forName("utf-8")
val source = responseBody?.source()
source?.request(Long.MAX_VALUE)
val buffer = source?.buffer
val contentType = responseBody?.contentType()
if (contentType != null) {
try {
charset = contentType.charset(Charset.forName("utf-8"))
} catch (e: Exception) {
return@Interceptor response
}
}
val contentLength = responseBody?.contentLength()
if (contentLength != 0L) {
val oriData = buffer?.clone()?.readString(charset)
Log.e("TAG", oriData.toString())
val data = AesUtil.decrypt(oriData, KEY) ?: ""
val newResponse = okhttp3.Response.Builder()
newResponse.code(response.code())
newResponse.message(response.message())
val responseBody1 =
ResponseBody.create(MediaType.parse("application/json;charset=utf-8"), data)
newResponse.body(responseBody1)
newResponse.request(originalRequest)
newResponse.protocol(response.protocol())
return@Interceptor newResponse.build()
}
response
结语
新技能get!+1
接口加密,尤其是AES加密其实不难,将需求一步步剥离出来就好处理了,而且加解密重点是密钥。