一、前言:
1:需求:
我们需要对http的请求参数加密,还要对http的返回参数加密,这就需要用到Interceptor 拦截器了。
2:OkHttp/Retrofit的实现
现在说到网络框架,应该毫无疑问是Retrofit了。上面说的加密方案说到底还是要在网络请求框架内加上,怎么做入侵最小,怎么做最方便才是重点。
1、坑定不能直接在接口调用层做加密,加参数,这样每个接口都要修改,这是不可能的。
2、ConverterFactory处理,这也是网上可以搜到的很多文章的写法,但我觉得还是有入侵。而且有点麻烦。
3、OkHttp添加拦截器,这种方法入侵最小(可以说没有),实现呢也非常优雅。
二、代码实现:
1、拦截器功能
/**
* @author by lyy on 2020/10/26.
* okhttp 拦截器,主要功能为添加请求头
*/
public class HttpInterceptorAll implements Interceptor {
private static String requestValue;
private static String requestHead;
private static String reponseValue;
DecryptBean decryptBean;
public HttpInterceptorAll() {
decryptBean = new DecryptBean();
}
@Override
public Response intercept(Chain chain) throws IOException {
Log.d("LUO", "-------------------------------------------拦截器前-------------------------------------------------");
Request.Builder updateRequest = null;
Request request = chain.request();
Response response = null;
//是否加密
String appSecurity = BuildConfig.APP_SECURITY;
//返回数据
String responseDecrypt = "";
String requestURI;
try {
String baseUrl = "";
//请求数据
String requestDecrypt = "";
//请求token
String token = SPUtils.getInstance(SPConstant.TOKEN).getString(SPConstant.TOKEN);
//获取请求方式:
String method = request.method();
//请求连接
requestURI = request.url().encodedPath();
Log.d("LUO", "method:" + method);
Log.d("LUO", "请求连接地址:" + requestURI);
if ("true".equals(appSecurity)) {
//加密
//添加请求头
updateRequest = request.newBuilder()
.header("token", token)
.addHeader("device_id", BaseApplication.device)
.addHeader("deviceType", "Android")
.addHeader("compact", "1")
.addHeader("deviceSystem", DeviceUtils.getModel())
.addHeader("screenSize", ScreenUtils.getAppScreenWidth() + "x" + ScreenUtils.getAppScreenHeight())
.addHeader("version", BuildConfig.VERSION_NAME);
if ("GET".equals(method)) {
//参数后的值
String param = request.url().encodedQuery();
if (!TextUtils.isEmpty(param)) {
String encrypt = AESUtil.encrypt(param);
//get请求必须URLEncoded
baseUrl = BuildConfig.TEST_BASE_URL + requestURI + "?compact=" + toURLEncoded(encrypt);
} else {
baseUrl = BuildConfig.TEST_BASE_URL + requestURI;
}
Log.d("LUO", "请求参数:" + param + "\n" + baseUrl);
} else if ("POST".equals(method)) {
baseUrl = BuildConfig.TEST_BASE_URL + requestURI;
String param = getRequestInfo(request);
if (!TextUtils.isEmpty(param)) {
//body加密
String decrypt = AESUtil.encrypt(param);
decryptBean.setCompact(decrypt);
requestDecrypt = JSON.toJSONString(decryptBean);
} else {
requestDecrypt = "";
}
Log.d("LUO", "请求参数:" + requestDecrypt);
RequestBody requestBody = RequestBody.create(request.body().contentType(), requestDecrypt);
updateRequest.post(requestBody);
} else if ("PUT".equals(method)) {
baseUrl = BuildConfig.TEST_BASE_URL + requestURI;
String param = getRequestInfo(request);
if (TextUtils.isEmpty(param)) {
//body加密
String decrypt = AESUtil.encrypt(param);
decryptBean.setCompact(decrypt);
requestDecrypt = JSON.toJSONString(decryptBean);
} else {
requestDecrypt = "";
}
//Log.d("LUO", "请求参数:" + requestDecrypt);
RequestBody requestBody = RequestBody.create(request.body().contentType(), requestDecrypt);
updateRequest.put(requestBody);
}
updateRequest.url(baseUrl);
response = chain.proceed(updateRequest.build()).newBuilder().build();
//请求的
Log.d("LUO", "所有的请求头:" + updateRequest.build().headers() + "\n" + "请求参数:" + baseUrl);
} else {
//未加密
//添加请求头
Request head = request.newBuilder().header("token", token)
.addHeader("device_id", BaseApplication.device)
.addHeader("deviceType", "Android")
.addHeader("deviceSystem", DeviceUtils.getModel())
.addHeader("screenSize", ScreenUtils.getAppScreenWidth() + "x" + ScreenUtils.getAppScreenHeight())
.addHeader("version", BuildConfig.VERSION_NAME)
.build();
String param = request.url().encodedQuery();
response = chain.proceed(head).newBuilder().build();
Log.d("LUO", "所有的请求头:" + head.headers() + "\n" + "请求参数:" + baseUrl + "?" + param);
}
/**
* 返回成功数据
*/
if (response.code() == 200) {
Log.d("LUO", "-------------------------------------------拦截器后-------------------------------------------------");
if ("true".equals(appSecurity)) {
//加密
//解析返回数据
String jsonStr = getResponseInfo(response);
Log.d("LUO", "解密之前返回数据:" + jsonStr);
if (!TextUtils.isEmpty(jsonStr) && jsonStr.contains("compact")) {
DecryptBean decryptBean = JSON.parseObject(jsonStr, DecryptBean.class);
String compact = decryptBean.getCompact();
try {
responseDecrypt = AESUtil.decrypt(compact);
} catch (Exception e) {
e.printStackTrace();
}
Log.d("LUO", "解密返回数据:" + responseDecrypt);
} else {
responseDecrypt = jsonStr;
}
//返回的
ResponseBody responseBody = ResponseBody.create(response.body().contentType(), responseDecrypt);
String json = getResponseInfo(response);
Log.d("LUO", "返回数据:" + json);
response = response.newBuilder().body(responseBody).build();
} else {
//未加密
Log.d("LUO", "token:" + token);
String str = getResponseInfo(response);
Log.d("LUO", "返回所有数据:" + str);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return response;
}
/**
* 打印请求消息
*
* @param request 请求的对象
*/
private String getRequestInfo(Request request) {
String str = "";
if (request == null) {
return str;
}
RequestBody requestBody = request.body();
if (requestBody == null) {
return str;
}
try {
Buffer bufferedSink = new Buffer();
requestBody.writeTo(bufferedSink);
Charset charset = Charset.forName("utf-8");
str = bufferedSink.readString(charset);
} catch (IOException e) {
e.printStackTrace();
}
return str;
}
/**
* 打印返回消息
*
* @param response 返回的对象
*/
private String getResponseInfo(Response response) {
String str = "";
if (response == null || !response.isSuccessful()) {
return str;
}
ResponseBody responseBody = response.body();
long contentLength = responseBody.contentLength();
BufferedSource source = responseBody.source();
try {
source.request(Long.MAX_VALUE); // Buffer the entire body.
} catch (IOException e) {
e.printStackTrace();
}
Buffer buffer = source.buffer();
Charset charset = Charset.forName("utf-8");
if (contentLength != 0) {
str = buffer.clone().readString(charset);
}
return str;
}
/**
* 对加密后的value数据url编码
* URLEncoded编码
* @param paramString
* @return
*/
public static String toURLEncoded(String paramString) {
if (paramString == null || paramString.equals("")) {
Log.d("LUO", "toURLEncoded error:" + paramString);
return "";
}
try {
String str = new String(paramString.getBytes(), "UTF-8");
str = URLEncoder.encode(str, "UTF-8");
return str;
} catch (Exception localException) {
Log.d("LUO", "toURLEncoded error:" + paramString, localException);
}
return "";
}
}
2、添加请求头
//然后OkHttp加入该拦截器:
new OkHttpClient.Builder()
.addInterceptor(new HttpInterceptorAll())
....
.build();
3、AES加解密工具
public class AESUtil {
//算法
private static final String ALGORITHM = "CBC";
/**
* 1.电码本模式(Electronic Codebook Book (ECB))不支持;
* 2.密码分组链接模式(Cipher Block Chaining (CBC))默认的加密算法;
* 3.计算器模式(Counter (CTR));
* 4.密码反馈模式(Cipher FeedBack (CFB));
* 5.输出反馈模式(Output FeedBack (OFB))。
*/
// 指定加密的算法、工作模式和填充方式
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
// 编码
private static final String ENCODING = "UTF-8";
// 密匙
private static final String KEY = "nt38siidkkkkHsooo0";
/***
* 头信息:compact,不传则按照原来数据格式返回,加密则传1
* aes:采用CBC加密模式
* aes key:nt38vfVZhAHsBEd7
* 偏移量:采用token前16位位偏移量,如token不存在或token长度不满16位则采用默认偏移量,默认偏移量:FseK2fyK7zgTjyUq
*/
// 偏移量
private static String OFFSET = "Fsjjjjdfdffpggq";
private static String token;
public AESUtil() {
//请求token e3fc31c7600a0a2c09e039e1e4f4c656_t
OFFSET = "dhhdhdhgTjyUq";
}
/**
* AES加密
*
* @param data
* @return
* @throws Exception
*/
public static String encrypt(String data) throws Exception {
token = SPUtils.getInstance(SPConstant.TOKEN).getString(SPConstant.TOKEN);
if (!TextUtils.isEmpty(token)) {
OFFSET = token.substring(0, 16);
} else {
OFFSET = "mmmoooTjyUxxxqll";
}
// 初始化cipher
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
//转化成JAVA的密钥格式
SecretKeySpec skeySpec = new SecretKeySpec(KEY.getBytes("ASCII"), ALGORITHM);
//使用CBC模式,需要一个向量iv,可增加加密算法的强度
IvParameterSpec iv = new IvParameterSpec(OFFSET.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
byte[] encrypted = cipher.doFinal(data.getBytes(ENCODING));
//此处使用BASE64做转码。
String result = new BASE64Encoder().encode(encrypted);
return result;
}
/**
* AES解密
*
* @param data
* @return
* @throws Exception
*/
public static String decrypt(String data) throws Exception {
Log.d("LUO", "初始化OFFSET:" + OFFSET);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
SecretKeySpec skeySpec = new SecretKeySpec(KEY.getBytes("ASCII"), ALGORITHM);
//使用CBC模式,需要一个向量iv,可增加加密算法的强度
IvParameterSpec iv = new IvParameterSpec(OFFSET.getBytes());
cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
byte[] buffer = new BASE64Decoder().decodeBuffer(data);
byte[] encrypted = cipher.doFinal(buffer);
//此处使用BASE64做转码。
String result = new String(encrypted, ENCODING);
return result;
}
}
三、功能解析:
1、功能介绍:
// 获取请求
1、Request request = chain.request();
//获取请求方式:
2、String method = request.method();
//请求连接
3、String requestURI = request.url().encodedPath();
//参数后的值
4、String param = request.url().encodedQuery();
//拼接url地址
5、String baseUrl = BuildConfig.TEST_BASE_URL + requestURI + "?compact=" + encrypt;
//添加请求头
6、 Request.Builder updateRequest = request.newBuilder()
.header("token", token)
.addHeader("device_id", BaseApplication.device)
.addHeader("deviceType", "Android")
.addHeader("compact", "1")
.addHeader("deviceSystem", DeviceUtils.getModel())
.addHeader("screenSize", ScreenUtils.getAppScreenWidth() + "x" + ScreenUtils.getAppScreenHeight())
.addHeader("version", BuildConfig.VERSION_NAME);
//POST请求参数
7、RequestBody requestBody = RequestBody.create(request.body().contentType(), “加密后数据”);
updateRequest.post(requestBody);
//添加requestBody参数
8、updateRequest.post(requestBody);
//添加请求的url
9、updateRequest.url(baseUrl);
//生成请求的Response
10、Response response = chain.proceed(updateRequest.build()).newBuilder().build();
//等于200是请求返回
11、response.code() == 200
// 请求返回的参数
12、ResponseBody responseBody = ResponseBody.create(response.body().contentType(), “解密后数据”);
// 添加请求返回的参数
13、 Response response = response.newBuilder().body(responseBody).build();
//正常不加密的请求头
14、Request head = request.newBuilder().header("token", token)
.addHeader("device_id", BaseApplication.device)
.addHeader("deviceType", "Android")
.addHeader("deviceSystem", DeviceUtils.getModel())
.addHeader("screenSize", ScreenUtils.getAppScreenWidth() + "x" + ScreenUtils.getAppScreenHeight())
.addHeader("version", BuildConfig.VERSION_NAME)
.build();
String param = request.url().encodedQuery();
Response response = chain.proceed(head).newBuilder().build();
//get请求参数加密,必须url的参数编码,否则服务器解析失败(URLEncode主要是把一些特殊字符转换成转移字符)
15、 String baseUrl = BuildConfig.TEST_BASE_URL + requestURI + "?compact=" + toURLEncoded(encrypt);
//编码方法
16、 /**
* 对加密后的value数据url编码
* URLEncoded编码
* @param paramString
* @return
*/
public static String toURLEncoded(String paramString) {
if (paramString == null || paramString.equals("")) {
Log.d("LUO", "toURLEncoded error:" + paramString);
return "";
}
try {
String str = new String(paramString.getBytes(), "UTF-8");
str = URLEncoder.encode(str, "UTF-8");
return str;
} catch (Exception localException) {
Log.d("LUO", "toURLEncoded error:" + paramString, localException);
}
return "";
}
2、主要注意点:
1、和接口无关的新加的数据放在请求头里。
2、该close的要close,不然会内存泄漏。
3、新旧Request和Response要区分好,新的要替换旧的去传递或返回。
4、要对response.code()做处理,只有在和后台约定好的返回码下才走解密的逻辑,具体看自己的需求,不一定都是200。
3、特别需要注意的地方:
//这个方法只能调一次;多次调用会出现多次请求
Response response = chain.proceed(request);
4、其它参考:
----->先定义一个拦截器的实现:
public class DataEncryptInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
//请求
Request request = chain.request();
RequestBody oldRequestBody = request.body();
Buffer requestBuffer = new Buffer();
oldRequestBody.writeTo(requestBuffer);
String oldBodyStr = requestBuffer.readUtf8();
requestBuffer.close();
MediaType mediaType = MediaType.parse("text/plain; charset=utf-8");
//生成随机AES密钥并用serverPublicKey进行RSA加密
SecretKeySpec appAESKeySpec = EncryptUtils.generateAESKey(256);
String appAESKeyStr = EncryptUtils.covertAESKey2String(appAESKeySpec);
String appEncryptedKey = RSAUtils.encryptDataString(appAESKeyStr, serverPublicKey);
//计算body 哈希 并使用app私钥RSA签名
String appSignature = RSAUtils.signature(oldBodyStr, appPrivateKey);
//随机AES密钥加密oldBodyStr
String newBodyStr = EncryptUtils.encryptAES(appAESKeySpec, oldBodyStr);
RequestBody newBody = RequestBody.create(mediaType, newBodyStr);
//构造新的request
request = request.newBuilder()
.header("Content-Type", newBody.contentType().toString())
.header("Content-Length", String.valueOf(newBody.contentLength()))
.method(request.method(), newBody)
.header("appEncryptedKey", appEncryptedKey)
.header("appSignature", appSignature)
.header("appPublicKey", appPublicKeyStr)
.build();
//响应
Response response = chain.proceed(request);
if (response.code() == 200) {//只有约定的返回码才经过加密,才需要走解密的逻辑
//获取响应头
String serverEncryptedKey = response.header("serverEncryptedKey");
//用app的RSA私钥解密AES加密密钥
String serverDecryptedKey = RSAUtils.decryptDataString(serverEncryptedKey, appPrivateKey);
SecretKeySpec serverAESKeySpec = EncryptUtils.covertString2AESKey(serverDecryptedKey);
//用AES密钥解密oldResponseBodyStr
ResponseBody oldResponseBody = response.body();
String oldResponseBodyStr = oldResponseBody.string();
String newResponseBodyStr = EncryptUtils.decryptAES(serverAESKeySpec, oldResponseBodyStr);
oldResponseBody.close();
//构造新的response
ResponseBody newResponseBody = ResponseBody.create(mediaType, newResponseBodyStr);
response = response.newBuilder().body(newResponseBody).build();
}
response.close();
//返回
return response;
}
}
----->然后OkHttp加入该拦截器:
new OkHttpClient.Builder()
.addInterceptor(new DataEncryptInterceptor())
....
.build();
----->这样就搞定了。
参考地址:https://www.waitalone.cn/mobilesecurity/okhttp.html