Retrofit请求加密,响应解密并生成类对象(自定义ConverterFactory)

1.自定义JsonConverterFactory类

public class JsonConverterFactory extends Converter.Factory {
    private static final String TAG = "JsonConverterFactory";
    private final Gson gson;

    public static JsonConverterFactory create() {
        return create(new Gson());
    }

    public static JsonConverterFactory create(Gson gson) {
        return new JsonConverterFactory(gson);

    }

    private JsonConverterFactory(Gson gson) {
        if (gson == null) throw new NullPointerException("gson == null");
        this.gson = gson;
    }


    @Nullable
    @Override
    public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
        TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
        return new JsonRequestBodyConverter<>(gson, adapter); //请求
    }

    @Nullable
    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
        return new JsonResponseBodyConverter<>(gson, adapter); //响应
    }

    /**
     * JsonRequestBodyConverter<T>
     * @param <T>
     */
    public static class JsonRequestBodyConverter<T> implements Converter<T, RequestBody> {
        private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8");
        private final Gson gson;
        private final TypeAdapter<T> adapter;

        /**
         * 构造器
         */
        public JsonRequestBodyConverter(Gson gson, TypeAdapter<T> adapter) {
            this.gson = gson;
            this.adapter = adapter;
        }

        @Override
        public RequestBody convert(T value) throws IOException {

            //这里需要,特别注意的是,request是将T转换成json数据。
            //你要在T转换成json之后再做加密。
            //再将数据post给服务器,同时要注意,你的T到底指的那个对象

            //加密操作,返回字节数组
            byte[] encrypt = AESUtils.encrypt(value.toString());

            Log.i("xiaozhang", "request中传递的json数据:" + value.toString()); //打印:加密前的json字符串
            Log.i("xiaozhang", "加密后的字节数组:" + encrypt.toString());//打印:字节数组

            //传入字节数组,创建RequestBody 对象
            return RequestBody.create(okhttp3.MediaType.parse("application/json; charset=utf-8"),encrypt);
        }
    }

    /**
     * JsonResponseBodyConverter<T>
     * @param <T>
     */
    public class JsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
        private final Gson mGson;//gson对象
        private final TypeAdapter<T> adapter;

        /**
         * 构造器
         */
        public JsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
            this.mGson = gson;
            this.adapter = adapter;
        }

        /**
         * 转换
         *
         * @param responseBody
         * @return
         * @throws IOException
         */
        @Override
        public T convert(ResponseBody responseBody) throws IOException {

            byte[] bytes = responseBody.bytes();

            //对字节数组进行解密操作
            String decryptString = AESUtils.decrypt(bytes);

            //对解密的字符串进行处理
            int position = decryptString.lastIndexOf("}");
            String jsonString = decryptString.substring(0,position+1);

            Log.i(TAG, "需要解密的服务器数据字节数组:" + bytes.toString());
            Log.i(TAG, "解密后的服务器数据字符串:" + decryptString);
            Log.i(TAG, "解密后的服务器数据字符串处理为json:" + jsonString);

            //这部分代码参考GsonConverterFactory中GsonResponseBodyConverter<T>的源码对json的处理
            Reader reader = StringToReader(jsonString);
            JsonReader jsonReader = gson.newJsonReader(reader);
            try {
                return adapter.read(jsonReader);
            } finally {
                reader.close();
                jsonReader.close();
            }
        }

        /**
         * String转Reader
         * @param json
         * @return
         */
        private Reader StringToReader(String json){
            Reader reader  = new StringReader(json);
            return reader;
        }
    }
}

注:为了可以像GsonConverterFactory将Json字符串转化为Java类对象,我参看了GsonConverterFactory的源码,在处理完解密操作之后得到了Json字符串,然后我加上GsonConverterFactory的源码部分来处理Json字符串。这样其实就是在GsonConverterFactory的基础上添加了加密和解密操作。

参考

1. Retrofit 2 之自定义Converter实现加密解密

其他参考
retrofit 自定义请求参数加密 和自定义响应解密 带你走出那些坑
Retrofit 进阶篇 自定义转换器
Retrofit:打造自己的Converter之byte[]

2.AES/CBC/NoPadding加密解密

找到了一篇关于PHP和Java的AES互通兼容加密文章,看完之后发现了原来PHP的AES加密填充只有ZeroPadding(补零 - 因为数据长度不是16的整数倍就需要填充),而Java是没有这种填充模式,杯具的只能自己写一个了,那Java的填充模式就用NoPadding(不填充内容);

public class AESUtils {

    //初始向量(偏移)
    public static final String iv= "7983B5439EF75A69";   //AES 为16bytes. DES 为8bytes

    //编码方式
    //public static final String bm = "utf-8";

    //私钥  (密钥)
    private static final String key="7983b5439ef75a69";   //AES固定格式为128/192/256 bits.即:16/24/32bytes。DES固定格式为128bits,即8bytes。

    /**
     * 加密
     * @param data 加密前的字符串
     * @return 加密后的字节数组
     */
    public static byte[] encrypt(String data){
        try {

            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            int blockSize = cipher.getBlockSize();

            //判断待加密的字节数组的长度,在此长度基础上扩展一个字节数组的长度为16的倍数
            byte[] dataBytes = data.getBytes();
            int plaintextLength = dataBytes.length;
            if (plaintextLength % blockSize != 0) {
                plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
            }

            //创建需新的待加密的字节数组,将上面的字节数组复制进来,多余的补0
            byte[] plaintext = new byte[plaintextLength];
            System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);

            SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
            IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());

            cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);

            //加密后的字节数组
            byte[] encrypted = cipher.doFinal(plaintext);

            return encrypted;

        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 解密
     * @param encrypted1 解密前的字节数组
     * @return 解密后的字符串
     */
    public static String decrypt(byte[] encrypted1) {
        try
        {
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
            IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());

            cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);
            
            //解密后的字节数组
            byte[] original = cipher.doFinal(encrypted1);
            String originalString = new String(original);
            return originalString;
        }
        catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

可以看到aes加密的中间结果是byte[]类型,直接new String(byte[])会看不到有意义的中间结果,可以在这里用的是base64,是因为各个语言都有这样的支持。在同个语言内,也有bytesToHexString这样的方式。

跨语言加解密的要求是:AES/CBC/ZeroPadding 128位模式,key和iv一样,编码统一用utf-8。不支持ZeroPadding的就用NoPadding.

参考

1. AES加密CBC模式兼容互通四种编程语言平台【PHP、Javascript、Java、C#】(看Java的AES加密解密)

2.C#, Java, PHP, Python和Javascript几种语言的AES加密解密实现【多种语言AES/CBC/PKCS5Padding通用加解密数据】(看最后的总结)

3.AES对称加密算法扫盲(看AES加密的方式)

4.在线AES等加密解密验证工具(验证加解密)

其他参考
java加密算法之AES小记
JAVA实现AES加密
java使用Hex编码解码实现Aes加密解密功能示例
Java加密算法 AES

3.Retrofit+RxJava

(1)定义HttpService接口

public interface HttpService {
 /**
     * 管理员登录
     * @return
     */
    @Headers({"Content-Type: application/json","Accept: application/json"})//需要添加头
    @POST("ictweb.cgi")
    Observable<Result<MasterLoginInfo>> masterLoginEncrypt(@Body String parmasJson);
}

注:这里的参数是@Body String

(2)// 获取retrofit的实例

retrofit = new Retrofit
          .Builder()
          .baseUrl(UrlHelper.BASE_URL)  //自己配置
          .client(client)
          .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
       // .addConverterFactory(GsonConverterFactory.create())
          .addConverterFactory(JsonConverterFactory.create())
          .build();

(3)获取HttpService代理对象

HttpService httpService = retrofit.create(HttpService.class);

(4)构建请求的json字符串

String jsonString= "{\n" +
            "\t\"cmd\":\"10\",\n" +
            "\t\"content\":{\n" +
            "\t\t\"LoginName\":\"admin\",\t\t\n" +
            "\t\t\"Password\":\"admin\",\t\t\n" +
            "\"ClientType\":\"1\"\t\t\t\n" +
            "}\n" +
            "}";

(5)请求数据

httpService.masterLoginEncrypt(jsonString)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<Result<MasterLoginInfo>>() {
                    @Override
                    public void onCompleted() {
                        Log.i(TAG, "onCompleted: ");
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.i(TAG, "error: ");
                    }

                    @Override
                    public void onNext(Result<MasterLoginInfo> masterLoginInfoResult0) {
                        Log.i(TAG, "onNext: result="+masterLoginInfoResult0.getContent().getResult());
                    }
                });

综上,使用Rertofit+RxJava进行网络操作,通过自定义继承Converter.Factory类可以实现,在客户端数据请求时进行AES/CBC/NoPadding加密,在服务器数据响应时进行AES/CBC/NoPadding解密。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,510评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 看有一期的《天天向上》,汪涵说起节日家里的男人给女人送花,岳父给岳母献花,说的不是“我爱你”,而是“我依你”。很简...
    学徒贤芳阅读 2,650评论 0 3
  • 玫瑰娇嫩美艳,因此花枝上长满了刺用以防御;荷花生于泽地,因此需要荷叶的托举。可我总也无法想象玉兰的枝究竟是有怎样的...
    Zzz宣儿阅读 432评论 0 2