美文网首页NetAndroid知识网络
Retrofit 2.0 超能实践(一),完美支持加密Https

Retrofit 2.0 超能实践(一),完美支持加密Https

作者: Tamic | 来源:发表于2016-06-07 23:31 被阅读28751次

    前阵子看到圈子里Retrofit 2.0,RxJava(Android), OkHttp3.3 ,加之支持Android和 iOS 的React Native 热更新技术, 火的不要不要的, 2015年新技术一大波来袭 ,看着自己项目还在用httpClient, asyncTask的原生开发 感觉自己已成火星人,实在顶不住内心强烈的自卑感,加之对新技术的追求,入手移动开发新三剑客,运用在目前的项目中,虽然目前关于他们的介绍资料网上一大把,但是自己亲自实践后,发现坑不少,为了能方便其他人安全顺利入坑,今天就先从Retrofit说起,前方高能,准备躲避。

    Retrofit 2.0

    Retrofit是SQUARE美国一家移动支付公司最近新发布的在Android平台上http访问的开源项目


    一 什么Retrofit

    官方标语;A type-safe HTTP client for Android and Java
    语意很明显一款android安全类型的http客户端, 那么怎么样才算安全?支持https?支持本地线程安全?
    发现Rertofit其内部都是支持lambda语法(国内称只链式语法),内部支持okhttp, 并且支持响应式RxJAava,当然jdk1.8 和android studio工具也支持lambda。带着这些疑问 我开始探究一下。

    在此之前准备入手资料:

    国外博客
    https://inthecheesefactory.com/blog/retrofit-2.0/en

    官方github
    http://square.github.io/retrofit/

    OKHttp原理请看我写的这个系列:
    OkHttp 3.x 源码解析之Interceptor 拦截器

    二 Retrofit怎么使用

    下文之前先给大家看下传统的httpclient(urlConnection) + AsyncTask实现的登录功能,这样我们才能发现Retrofit的优雅之处.

    传统方式:

      /**
     * Represents an asynchronous login/registration task used to authenticate
     * the user.
     */
    public class UserLoginTask extends AsyncTask<Void, Void, Boolean> {
    
        private final String mEmail;
        private final String mPassword;
    
        UserLoginTask(String email, String password) {
            mEmail = email;
            mPassword = password;
        }
    
        @Override
        protected Boolean doInBackground(Void... params) {
            // TODO: attempt authentication against a network service.
    
            try {
                // Simulate network access.
                String result = "";
                BufferedReader in = null;
                String path ="http://localhost:8080/login/?" +"email =" + mEmail + "& password =" + mPassword;
                URL url =new URL(path);
                HttpURLConnection conn = (HttpURLConnection)url.openConnection();
                conn.setConnectTimeout(5 * 1000);
                conn.setRequestMethod("GET");
                InputStream inStream = conn.getInputStream();
                in = new BufferedReader(
                        new InputStreamReader(conn.getInputStream()));
                String line;
                while ((line = in.readLine()) != null)
                {
                    result += "\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n" + line;
                }
    
            }catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            } catch (ProtocolException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
       //在这里我们还要对返回的json数据进行 要主动映射到modle上
        ………
            for (String credential : DUMMY_CREDENTIALS) {
                String[] pieces = credential.split(":");
                if (pieces[0].equals(mEmail)) {
                    // Account exists, return true if the password matches.
                    return pieces[1].equals(mPassword);
                }
            }
    
            // TODO: register the new account here.
            return true;
        }
    
        @Override
        protected void onPostExecute(final Boolean success) {
            mAuthTask = null;
    
    
            if (success) {
                // do SomeThing
            } else {
                mPasswordView.setError(getString(R.string.error_incorrect_password));
                mPasswordView.requestFocus();
            }
        }
    
        @Override
        protected void onCancelled() {
            mAuthTask = null;
            showProgress(false);
        }
    }
    
    private void enterhome() {
        Intent intent = new Intent(LoginActivity.this, MainListActivity.class);
        startActivity(intent);
    }
    

    发现姿势也很简单,点击loginbtn开启一个异步线程 在AsyncTaskdoInBackground中访问登录API,在onPostExecute中进行UI更新;也能很简单流畅的解决UI线程请求网络 非UI线程更新UI的问题, 但是AsyncTask 处理大数据耗时就会有弊端,况且他默认线程也是5个,容易造成泄漏,接下来介绍用Retrofit实现以上相同的功能的方式

    2 Retrofit

      /**
     * 登录!
     */
    private  void getLogin() {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://localhost:8080/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        ApiManager apiService = retrofit.create(ApiManager.class);
    
        Call<LoginResult> call = apiService.getData("lyk", "1234");
       call.enqueue(new Callback<LoginResult>() {
           @Override
           public void onResponse(Call<LoginResult> call, Response<LoginResult> response) {
               if (response.isSuccess()) {
                   // do SomeThing
               } else {
                  //直接操作UI 返回的respone被直接解析成你指定的modle 
               }
           }
    
           @Override
           public void onFailure(Call<LoginResult> call, Throwable t) {
    
               // do onFailure代码
           }
       });
    }
    

    ApiManager接口

    /**
     * Created by LIUYONGKUI on 2016-05-03.
    */
    public interface ApiManager {
    
     @GET("login/")
     Call<LoginResult> getData(@Query("name") String name, @Query("password") String pw);
    

    好了 看了以上代码 或许你已经看到了他的链式优雅高大上的地方了,也许看不懂,有点蒙逼,但没关系我们继续入门。

    1 配置gradle

    compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
    compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'  
    

    com.squareup.retrofit2:converter-gson:2.0.0-beta4 此依赖非必须,只是方便我对http返回的数据进行解析。

    2 定义实例化

    1》初始化Retrofit

     Retrofit retrofit = new Retrofit.Builder()
              .baseUrl("http://localhost:8080/")
              .addConverterFactory(GsonConverterFactory.create())
               .build();
    

    通过 Retrofit.Builder 来创建一个retrofit客户端,接着添加host url, 然后制定数据解析器,上面依赖的gson就是用在这里做默认数据返回的, 之后通过build()创建出来
    Retrofit也支持且内部自带如下格式:

    • Gson: com.squareup.retrofit2:converter-gson
    • Jackson: com.squareup.retrofit2:converter-jackson
    • Moshi: com.squareup.retrofit2:converter-moshi
    • Protobuf: com.squareup.retrofit2:converter-protobuf
    • Wire: com.squareup.retrofit2:converter-wire
    • Simple XML: com.squareup.retrofit2:converter-simplexml
    • Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

    2》编写API

      @GET("login/")
      Call<LoginResult> getData(@Query("name") String name, @Query("password") String pw);
    

    Call<T>是继承Cloneable的 并支持泛型,且此类是Retrofit统一返回对象,支持Callback<T>回调,在2.0上已支持RxJava观察者对象Observable<T>,此案例暂时用call ,后面入门了retrofit之后再接入RxJava,接着我们可以传入制定的解析Modle,就会在主线程里返回对应的model数据,无需开发者手动解析json数据,返回格式由开发者自己设置,这里主要用注解@get @post 设置请求方式,后面“login/”是方法Url, @Query("name")来设定body的parameters.

    • 如果想用表单 @FieldMap
      @FormUrlEncoded
      @POST("/url")
      Call<T> postForm(
      @FieldMap Map<String , Object> maps);

    • 如果直接用对象 @Body

      @POST("url")
       Call<T> PostBody(
            @Body Objects objects);
      
    • 如果直接多参数 @QueryMap

      @PUT("/url")
      Call<T> queryMap(
            @QueryMap Map<String, String> maps);
      
    • 如果上传文件 @Part

      @Multipart
      @POST("/url")
      Call<ResponseBody> uploadFlie(
            @Part("description") RequestBody description,
            @Part("files") MultipartBody.Part file);
      
    • 如果多文件上传 @PartMap()

      @Multipart
      @POST("{url}")
      Call<T> uploadFiles(
            @Path("url") String url,
            @PartMap() Map<String, RequestBody> maps);
      

    3》 调用API
    Retrofit支持异步和同步,案例中用call.enqueue(new Callback<LoginResult>)来采用异步请求,如果 调用call.execute() 则采用同步方式

       Call<LoginResult> call = apiService.getData("lyk", "1234");
       call.enqueue(new Callback<LoginResult>() {
           @Override
           public void onResponse(Call<LoginResult> call, Response<LoginResult> response) {
             
           }
    
           @Override
           public void onFailure(Call<LoginResult> call, Throwable t) {
    
               
           }
       });
    }
    

    取消请求

    直接用call实例进行cancel即可

      call.cancel(); 
    

    如果还未理解请阅读参考入门资料:Retrofit 2.0:有史以来最大的改进

    三 进阶拓展

    通过以上的介绍和案列,我们了解了怎样运用Retrofit请求网络数据,展现数据更新UI,用什么数据模型接收 Retroifit就会返回什么类型的数据,我们也不用关心是否在主线程里访问网络 还是子线程更新ui的问题,但实际开发中会存在很多问题,很多同学会遇到:Retrofit的内部Log都无法输出 , header怎么加入,请求怎么支持https,包括怎么结合RxJava.? 不用担心,这些Retrofit 2.0 都提供了支持okhttp的自定义的Interceptor(拦截器),通过不同的Interceptor可以实现不同的自定义请求形式,比如统一加head,参数,加入证书(ssl)等,前提必须结合okhttp来实现 , 通过给OkHttpClient添加Interceptor,然后给Retrofit设置http客户端即可.Retrofit提供了
    .client()方法供我们传入自定义的网络客户端,当然默认请求客户端就是okhttps.

    OkHttp入门请移步:
    ~https://github.com/square/okhttp
    ~ OKHttp源码解析

    1 开启Log

    可以用拦截器自己实现, retrofit已经提供了HttpLoggingInterceptor 里面有四种级别,输出的格式 可以看下面介绍。

    
    public enum Level {
        /** No logs. */
        NONE,
        /**
         * Logs request and response lines.
         *
         * <p>Example:
         * <pre>{@code
         * --> POST /greeting http/1.1 (3-byte body)
         *
         * <-- 200 OK (22ms, 6-byte body)
         * }</pre>
         */
        BASIC,
        /**
         * Logs request and response lines and their respective headers.
         *
         * <p>Example:
         * <pre>{@code
         * --> POST /greeting http/1.1
         * Host: example.com
         * Content-Type: plain/text
         * Content-Length: 3
         * --> END POST
         *
         * <-- 200 OK (22ms)
         * Content-Type: plain/text
         * Content-Length: 6
         * <-- END HTTP
         * }</pre>
         */
        HEADERS,
        /**
         * Logs request and response lines and their respective headers and bodies (if present).
         *
         * <p>Example:
         * <pre>{@code
         * --> POST /greeting http/1.1
         * Host: example.com
         * Content-Type: plain/text
         * Content-Length: 3
         *
         * Hi?
         * --> END GET
         *
         * <-- 200 OK (22ms)
         * Content-Type: plain/text
         * Content-Length: 6
         *
         * Hello!
         * <-- END HTTP
         * }</pre>
         */
        BODY
      }
    
    
    

    开启请求头

         Retrofit retrofit = new Retrofit.Builder().client(new OkHttpClient.Builder()
                                
                             .addNetworkInterceptor(
                                        new   HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.HEADERS))       
         .build())
    
    

    开启body日志

    .addNetworkInterceptor(
                                        new   HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) 
    
    
    

    基础输出

    .addNetworkInterceptor(
                                        new   HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)) 
    
    
    

    2 增加头部信息

    通用请求头

     new Retrofit.Builder()
               .addConverterFactory(GsonConverterFactory.create())
               
               .client(new OkHttpClient.Builder()
                       .addInterceptor(new Interceptor() {
                           @Override
                           public Response intercept(Chain chain) throws IOException {
                               Request request = chain.request()
                                       .newBuilder()
                                       .addHeader("mac", "f8:00:ea:10:45")
                                       .addHeader("uuid", "gdeflatfgfg5454545e")
                                       .addHeader("userId", "Fea2405144")
                                       .addHeader("netWork", "wifi")
                                       .build();
                               return chain.proceed(request);
                           }
                       })
    
                       .build()
    

    单独加入

    @Headers({ "Accept: application/vnd.github.v3.full+json", "User-Agent: Retrofit-your-App"})
    @get("users/{username}")
    Call<User>   getUser(@Path("username") String username);
    

    3 添加证书Pinning

    证书可以在自定义的OkHttpClient加入certificatePinner 实现

    OkHttpClient client = new OkHttpClient.Builder()
        .certificatePinner(new CertificatePinner.Builder()
                .add("YOU API.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
                .add("YOU API..com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=")
                .add("YOU API..com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
                .add("YOU API..com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
                .build())
    

    4 支持https

    加密和普通http客户端请求支持https一样,步骤如下:

    1 CertificateFactory 得到Context.getSocketFactory
    2 添加证书源文件
    3 绑定到okhttpClient
    4设置okhttpClient到retrofit中

    证书同样可以设置到okhttpclient中,我们可以把证书放到raw路径下

       SLSocketFactory sslSocketFactory =getSSLSocketFactory_Certificate(context,"BKS", R.raw.XXX);
    

    准备证书源文件

    加入证书源文件,我的证书是放在Raw下面的: 证书

    绑定证书

    protected static SSLSocketFactory getSSLSocketFactory(Context context, int[] certificates) {
    
    if (context == null) {
        throw new NullPointerException("context == null");
    }
    
    CertificateFactory certificateFactory;
    try {
        certificateFactory = CertificateFactory.getInstance("X.509");
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null, null);
    
        for (int i = 0; i < certificates.length; i++) {
            InputStream certificate = context.getResources().openRawResource(certificates[i]);
            keyStore.setCertificateEntry(String.valueOf(i), certificateFactory.generateCertificate(certificate));
    
            if (certificate != null) {
                certificate.close();
            }
        }
        SSLContext sslContext = SSLContext.getInstance("TLS");
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);
        sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
       return sslContext.getSocketFactory();   
    

    构建HostnameVerifier

     protected static HostnameVerifier getHostnameVerifier(final String[] hostUrls) {
    
        HostnameVerifier TRUSTED_VERIFIER = new HostnameVerifier() {
    
            public boolean verify(String hostname, SSLSession session) {
                boolean ret = false;
                for (String host : hostUrls) {
                    if (host.equalsIgnoreCase(hostname)) {
                        ret = true;
                    }
                }
                return ret;
            }
        };
    
    return TRUSTED_VERIFIER;
    

    }

    设置setSocketFactory

      okhttpBuilder.socketFactory(HttpsFactroy.getSSLSocketFactory(context, certificates));
    

    certificates 是你raw下证书源ID, int[] certificates = {R.raw.myssl}

    设置setNameVerifie

    okhttpBuilder.hostnameVerifier(HttpsFactroy.getHostnameVerifier(hosts));
    

    hosts是你的host数据 列如 String hosts[]`= {“https//:aaaa,com”, “https//:bbb.com”}

    实现自定义 添加到Retrofit

      okHttpClient = okhttpBuilder.build(); 
      Retrofit retrofit = new Retrofit.Builder() .client(okHttpClient) .build();
    

    如果信任所有https请求,
    可以直接将OkHttpClient的HostnameVerifier设置为false

    
      OkHttpClient client = new OkHttpClient();
     
        client.setHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String s, SSLSession sslSession) {
                return true;
            }
        });
        TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
            @Override
            public void checkClientTrusted(
                    java.security.cert.X509Certificate[] x509Certificates,
                    String s) throws java.security.cert.CertificateException {
            }
    
            @Override
            public void checkServerTrusted(
                    java.security.cert.X509Certificate[] x509Certificates,
                    String s) throws java.security.cert.CertificateException {
            }
    
            @Override
            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                return new java.security.cert.X509Certificate[] {};
            }
        } };
        try {
            SSLContext sc = SSLContext.getInstance("TLS");
            sc.init(null, trustAllCerts, new java.security.SecureRandom());
            client.setSslSocketFactory(sc.getSocketFactory());
        } catch (Exception e) {
            e.printStackTrace();
        }
    
    
             clent.protocols(Collections.singletonList(Protocol.HTTP_1_1))
             .build();
    
    
    

    常规问题归总

    1 url被转义

       http://api.myapi.com/http%3A%2F%2Fapi.mysite.com%2Fuser%2Flist
    

    请将@path改成@url

       public interface APIService { 
        @GET Call<Users> getUsers(@Url String url);}
    

    或者:

      public interface APIService {
        @GET("{fullUrl}")
        Call<Users> getUsers(@Path(value = "fullUrl", encoded = true) String fullUrl);
    }
    

    2Method方法找不到

    java.lang.IllegalArgumentException: Method must not be null

    请指定具体请求类型@get @post等

       public interface APIService { 
    
       @GET Call<Users> getUsers(@Url String url);
    }
    

    3Url编码不对,@fieldMap parameters must be use FormUrlEncoded

    如果用fieldMap加上FormUrlEncoded编码

    @POST()
    @FormUrlEncoded
    Observable<ResponseBody> executePost(
            @FieldMap Map<String, Object> maps);
    

    上层需要转换将自己的map转换为FieldMap

     @FieldMap(encoded = true) Map<String, Object> parameters,
    

    4 paht和url一起使用

    Using @Path and @Url paramers together with retrofit2

    java.lang.IllegalArgumentException: @Path parameters may not be used with @Url. (parameter #4

    如果你是这样的:

     @GET
    Call<DataResponse> getOrder(@Url String url,
     @Path("id") int id);
    

    请在你的url指定占位符.url:

    www.myAPi.com/{Id}
    

    总结

    看了以上的知识点你发现Retrofit同样支持RxJava,通过以下设置Call适配模式.就可以完美关联RxJava。

     retrofit .addCallAdapterFactory(RxJavaCallAdapterFactory.create())    
    

    关于 Retrofit+ RxJava的案列, 结尾源码已经结合,以及实际遇到的坑可以看本人的系列文章(Retrofit+Rxjava使用技巧一文)。RxJava也是一款强大的多线程通讯利器,也支持本地线程安全,从以前编程习惯迁移到这种链式风格, 估计入门会让你头痛,但会让你在实际应用开发中无时无刻,随心所欲进行多线程响应式编程开发。一句话 :谁用谁知道!

    Retrofit 2.0系列请阅读


    参考文章:

    相关文章

      网友评论

      • xmliu:感谢分享
      • 阳光的小四:哥,你的证书是指的什么?我有点迷茫呀,是公钥还是私钥呀?
      • 嘻嘻疯子:getSSLSocketFactory_Certificate 这个方法在哪呀
      • 0539c44157b1:《Retrofit 2.0 超能实践(一),完美支持加密Https传输 - 简书》写的挺不错的,已经收藏了。

        源码解析:http://sina.lt/fdcQ


      • Mid_Night:楼主,问个问题,ssl证书是有有效期的吧,证书放进APP里面,这个证书过期了怎么办,要更新APP的证书吗,没做过https这方面的。
        Hi川:@蛧虫 放服务器的话,被中间代理服务劫持后,下发了一个假证书怎么办
        Mid_Night:@蛧虫 好的,谢谢
        fd1e786b155b:证书在公司项目中都是放服务端的
      • today_work:有没有https 的post一个json的例子?
        Tamic: @today_work 后面文章有介绍 你按步骤来就行了
        today_work:@Tamic 可以的。。我已经解决了。。
        Tamic: @today_work 这俩碍不着边
      • 笔墨Android:楼主了你的文章,我想请问你一个问题,如果我添加公共请求参数的话,最后一个参数是要获取所有的key,value 进行加密的话应该怎么实现
        Tamic:@Angle0 看最新的两篇
      • 就是这个强:楼主,你好。证书是自已生成的么?具体该如何用呢?如果只是简单的想支持https请求,retrofit有更简单的方式么?
        Tamic:@简述安卓 用浏览器点击钥匙按钮 导出来证书就行了,这个网上教程挺多的 你搜下
        Tamic:@简述安卓 可以的直接过滤所以白名单就行
      • huangyirui:请问 ,如果要对请求的所有参数进行加密得到一个参数再 将这个参数添加到请求中,应该怎么做额?
        Tamic:@huangyirui 看最新的一篇
        huangyirui:@Tamic 额,但是不知道怎么实现额,求帮助额? 或者有没有博客可以看下的额:pensive:
        Tamic:@huangyirui 用拦截器就可以做到啊
      • 有兴不虚昧:还是有些不明白
      • 浪淘沙xud:SLSocketFactory sslSocketFactory =getSSLSocketFactory_Certificate(context,"BKS", R.raw.XXX); BKS 是 keystoreType,但是你后面的代码没有体现出来,楼主能否把Https证书详细使用步骤写出来
      • Jinwong:按照上面做的,发现报SSLProtocolException:SSL handshake aborted
      • 迩少装_: SLSocketFactory sslSocketFactory =getSSLSocketFactory_Certificate(context,"BKS", R.raw.XXX); “BKS” 下文也没体现 按照方法一步一步来 最后验证还是失败
        Tamic:@迩少装_ 失败原因呢
        迩少装_:@Tamic SSLContext sslContext = SSLContext.getInstance("TLS"); 是这里吗 我是TSL失败了
        Tamic:@迩少装_ TLS格式的试下
      • wan7451:retrofit 好像可以直接加证书
        Tamic:@Androider_ 最新的版本没关注了 求好的姿势吗
      • johnzz:你好,楼主,感谢分享这么多干货,建议能否按照难度高低给你retrofit系列文章排个序呢,比如retrofit-讲解1,2,3这样,按照难度系数给每篇文章命名,这样看起来相对 :blush: 方向清晰点。谢谢哈
        Tamic:@johnzz 好的 后面我整理下
      • 爱篮球的小码农:type-safe 不是类型安全的意思?说的是请求结果的类型与定义的一致吧
        Tamic:@爱篮球的小码农 就看你怎么理解吧
      • 小范屯:请教getSSLSocketFactory_Certificate这个方法在哪里,第二个参数是什么意思?
        Tamic:@小范屯 certificates 是你raw下证书源ID, int[] certificates = {R.raw.myssl
      • 小范屯:SLSocketFactory 这个拼写错了
        Tamic:@小范屯 :innocent::innocent:
      • Tamic:当然你单独给某个API加head也可以的

        @Headers({
        "Accept: application/vnd.github.v3.full+json",
        "User-Agent: Retrofit-your-App"})‘
        @get("users/{username}")
        Call<User> getUser(@Path("username") String username);
      • zy_zhangyuan88:好东西
      • 8314e3a0c30e:支持一下,好文章!

      本文标题:Retrofit 2.0 超能实践(一),完美支持加密Https

      本文链接:https://www.haomeiwen.com/subject/vofjdttx.html