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等方式)。

代码

直接在拦截器统一处理GETPOST请求。

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加密其实不难,将需求一步步剥离出来就好处理了,而且加解密重点是密钥。

全部评论

相关推荐

牛客162194370号:
点赞 评论 收藏
分享
不愿透露姓名的神秘牛友
11-29 12:19
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务