Android 客户端 okhttp3 与服务器之间的双向验证

本篇是Android 客户端基于okhttp3的网络框架 和后台服务器之间的双向验证


分为三个阶段

一:简单的后台服务器搭建

二:客户端接入okhttp3,并进行的网络请求

三:服务器和客户端的双向验证



第一步:   搭建简单的服务器


1:下载tomcat

2:配置tomcat

3:部署自己的web项目到tomcat



首先准备工具 eclipse 这个网上都有不多说,然后下载tomcat.     


http://tomcat.apache.org/


下载后建立一个文件夹方便以后查找,解压进入文件夹






进入bin文件夹中找到startup.bat 双击 会弹出命令框






有两种情况

一种是命令框打开后正常启动并一直存在






这种情况证明你的tomcat已经启动了.打开你的浏览器输入 localhost:8080 就可以看到默认的tomcat的页面了







出现上面情况的朋友 tomcat已经配置好了接下来就是把自己的web项目部署到tomcat上



第二种情况就是点击startup.bat 后 命令框一闪而过,点击几次都是同样的情况.出现这种情况的朋友是需要配置tomcat的环境变量.给出如何配置tomcat环境变量的链接这里就不做过多的赘述.



http://jingyan.baidu.com/article/154b4631aad2bb28ca8f4191.html




环境变量配好后 启动startup.bat  命令框就不会一闪而过了.然后输入 localhost:8080  就能出现tomcat默认的页面了


到这里tomcat已经可以使用接下来就是把自己的web项目部署到tomcat上了


打开eclipse  由于博主下成了中文版的eclipse了  和英文的  位置都是一样的


点击右键新建--->其他---->web---->动态Web项目  (英文是Dynamic Web Project)----->下一步

选择你下载的对应tomcat 的版本就行






输入自己的项目名称 (我的是LH)点击下一步,再点击下一步出现下面的页面 打钩箭头的部分(这是可选项 打钩了会自动创建xml文件  新手建议都打),然后点击完成






在左边的目录栏就可以看到刚刚创建的项目






在菜单栏里面选择window----->show View----->Others 

找server 点击确定






点击确定之后在日志栏那里会出现server 的页面   红色箭头就是    

点击紫色箭头






点击紫色箭头指的地方后选择自己的tomcat版本点击next







点击完成  server页面会出现刚添加的tomcat








右键找到  添加或删除 点击进入页面 在左边找到自己的web项目点击添加后再点击完成






在server页面点击tomcat后会出现添加后的web项目






点击右边的运行按钮






接着控制台会输入一大堆的日志 







出现上面的情况就代表你的tomcat启动成功了  然后在浏览器输入 http://localhost:8080/   就可以看到默认的tomcat页面  这个时候可能会说了 我想进入我的项目页面不想进入默认的页面.

好的  

在你eclipse 目录下找到WebContent创建一个indext.html







点击运行tomcat   在浏览器输入 http://localhost:8080/LH/index.html    LH (你的项目名)  index.html (你创建的html文件  这个html文件里面有简单的html标签  我是网上随便复制的)








到这里 第一步 搭建简单的服务器 已经完成了    

下面是搭服务器时我遇到的问题


在创建Web项目的时候在eclipse里面找不到Web选项.

这个问题是你的下载的eclispe是纯净版的 没有Web的这个插件

点击菜单栏里面的帮助按钮选择 安装新的插件








点击安装新的插件后会进入新的页面 在输入连接的地方输入   Eclipse Kepler repository - http://download.eclipse.org/releases/kepler   连接








输入连接后 会等一会(根据个人网络而定)会出现紫色箭头的各种插件

向下拉找到Web插件




    




勾选   

  • Eclipse Java EE Developer Tools
  • Eclipse Java Web Developer Tools
  • Eclipse Web Developer Tools
  • Eclipse XML Editors and Tools


这几个 点击下一步等待下载安装,安装成功后会提示你需要重新启动eclispe .重新启动以后点击new  others  里面就有Web项目了




第二步 Android 端接入okHttp3



我的用的是Android Strudio 所以本篇是以Android Studio为主的

首先创建自己的Demo项目(很简单这里就不多说了)

在build.gradle添加okhttp3的依赖  

compile 'com.squareup.okhttp3:okhttp:3.5.0'

eclipse下直接在github上下载对应的jar关联到项目里面就行了


写一个负责网络请求的工具类 GetHttpRequest




    private OkHttpClient okHttpClient ;
    private Request.Builder builder;
    private Handler handler;
    private Context context;



    public GetHttpRequest(Context context,Handler handler)
    {
        this.handler = handler;
        this.context = context;

	this.okHttpClient = new OkHttpClient.Builder()  //初始化全局 okhttpclient
        .connectTimeout(10, TimeUnit.SECONDS)//设置超时时间
        .readTimeout(10, TimeUnit.SECONDS)
        .build();
builder = new Request.Builder(); }
    public void doGet (String url , int type) throws IOException    {   
     	Request request = builder.get().url(url).build();//发送get请求    
    	executeResponse(request,type);    }      
    public void executeResponse(final Request request , final int type) {  
      	Call call = okHttpClient.newCall(request);
	//        call.execute()        
	//SocketTimeoutException连接超时	        
	call.enqueue( new Callback() {          
	 @TargetApi(Build.VERSION_CODES.KITKAT)            
	@Override            
    public void onFailure(Call call, IOException e) {           
     	Log.i("lhh","Get e :"+e.toString());            }       
     	@Override            
    public void onResponse(Call call, Response response) throws IOException {
                Message message = new Message();
                message.what = type;
                String json = response.body().string();
                Log.i("lhh","Get json :"+json);
                message.obj = json;
                handler.sendMessage(message);

            }



        });



    }

    }




使用的是okhttp3里面的get请求   里面简单封装了一下  (菜鸟一个 随便封的 大神勿喷  还请大神指点封装技巧)

使用get请求,收到结果后通过handler发送到ui线程显示返回来的数据

onResponse   返回结果
onFailure    返回异常

在ui线程里面调用get请求

try {

    if(editText.getText().toString().trim() != null)
    {
        getHttpRequest.doGet(editText.getText().toString().trim(),1);
    }else
    {
        Toast.makeText(this, "不能为空", Toast.LENGTH_SHORT).show();
    }

}
 catch (IOException e) {

    e.printStackTrace();
}



记得在配置清单里面加入网络权限哦

运行项目   接口就用csdn的地址  http://bbs.csdn.net/home







ok  get请求成功 


第三步:客户端与服务器连接


现在把连接换成我们自己搭建的tomcat服务器的

需要注意的是 测试真机必须和你tomcat服务器在同一个网段内才能访问 

访问的ip地址也不是localhost而是你本机的ip.

点击开始输入cmd 进入命令输入框 输入 ipconfig  复制IPv4 地址  替换localhost











现在连接你自己的tomcat 的地址是 http://192.168.1.60:8080/LH/index.html

浏览器也是可以正常访问的

给APP换成新的接口,运行







可以很清晰的看到 LH Web 的字样 说明我们自己搭建的服务器和Android 客户端已经可以连接起来了

第三步也完成了!

细心的朋友发现了 说到现在连双向验证毛都没看到就完了? 当然没完 现在才开始准备讲双向验证,之前都是基本的配置.在讲双向验证之前,请允许我在费几句话.

双向验证是https有的,也就是说http是没有的.

之前我们的接口都是普通的http接口 不是https.那什么是https呢?和http有什么区别呢?

简单的说就是https比http更安全,安全的原因就是添加ssl证书的验证.

想知道详细区别的朋友请出门左手百度.




首先我们要把自己搭建的tomcat服务器加一个https的接口




1:要是我们自己的tomcat支持https首先需要一个证书,这个证书可以是第三方机构认证的这个是正版的.还有一个就是我们自己生成的一个我们自己签名的证书,接下来就是生成我们自己的证书



2:进入命令输入框doc页面 输入下面的命令

keytool -genkey -alias lh_server -keyalg RSA -keystore lh_server.jks -validity 3600 -storepass 000000


-alias lh_server 是文件别名 -keystore lh_server.jks 生成的证书名字 -storepass 000000 密钥密码








按照提示输入在你的C:\Users\Trust   底下就会出现刚生成的jks文件了 







3:接着用刚生成的jks文件签发证书在doc下输入



keytool -export -alias lh_server -file lh_server.cer -keystore lh_server.jks -storepass000000


在你的C:\Users\Trust  目录下会生成一个 lh_server.cer的证书文件   这个就是我们服务器的自签名证书了








4:配置我们的服务器.找到你的tomcat的文件,在tomcat的文件夹下找到conf的文件点进去找到server.xml文件






5:使用文本编辑打开  搜索 port="8443"  可以看到默认是注释掉的






把上下的注释删掉后添加这样的属性  

 <Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol"
               maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
  keystoreFile="C:\Users\Trust\lh_server.jks" keystorePass="000000"
               clientAuth="false" sslProtocol="TLS" />


可以直接复制替换

注意keystoreFile的值为我们刚才生成的jks文件的路径:C:\Users\Trust\lh_server.jks(填写你的路径).keystorePass值为密钥库密码:000000

点击保存 启动tomcat  在浏览器里面输入新的地址 和之前不一样了  https://localhost:8443/LH/index.html  就会出现不安全的提示





这是因为证书不是第三方机构签发的而是我们自己签发的点击高级里面的继续进入






'

可以很清晰看到 显示的不安全,到这一步服务器的配置稍微告一段落.现在直接用APP试着登录我们经过自签名证书的服务器看看

我们会发现APP上点击按钮后没反应,在看看Android Studio 后台日志 






我们可以看到后台打印出了一条错误信息仔细看错误信息


javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.


没有找到证书,因为我们的APP还没有做任何验证证书的处理

ok现在处理我们的APP


上面这种处理有两种方法可以解决  



1:让我们的APP默认信任所有的证书


2:把想要访问的服务器的证书获取到,手动把证书添加到okhttp3信任证书的白名单里面



这里两种都讲一下



第一种:

默认信任所有证书

首先在我们的Android项目里面新添加一个工具类 TrustALLCerts 


public class TrustAllCerts  implements X509TrustManager {

    @Override
    public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws 
CertificateException {

    }

    @Override
    public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws 
CertificateException {

    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }





    public static class TrustAllHostnameVerifier implements HostnameVerifier {
        @Override
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    }


    public static SSLSocketFactory createSSLSocketFactory() {
        SSLSocketFactory ssfFactory = null;

        try {
            SSLContext sc = SSLContext.getInstance("TLS");
            sc.init(null,  new TrustManager[] { new TrustAllCerts() }, new SecureRandom());

            ssfFactory = sc.getSocketFactory();
        } catch (Exception e) {
        }

        return ssfFactory;
    }
}

上面是信任所有证书的代码


然后修改GetHttpRequest里面的代码


public class GetHttpRequest {

    private OkHttpClient okHttpClient ;
    private Request.Builder builder;
    private Handler handler;
    private Context context;



    public GetHttpRequest(Context context,Handler handler)
    {
        this.handler = handler;
        this.context = context;
	this.okHttpClient = new OkHttpClient.Builder()
        .sslSocketFactory(TrustAllCerts.createSSLSocketFactory()) //这个aip已经过时  后面会用新的api替代
        .hostnameVerifier(new TrustAllCerts.TrustAllHostnameVerifier())
    	.connectTimeout(10, TimeUnit.SECONDS)
    	.readTimeout(10, TimeUnit.SECONDS)
    	.build();
builder = new Request.Builder(); }
    public void doGet (String url , int type) throws IOException    {     
   	Request request = builder.get().url(url).build();        
	executeResponse(request,type);    }   
    public void executeResponse(final Request request , final int type) {      
  	Call call = okHttpClient.newCall(request);
	//        call.execute()       
 	//SocketTimeoutException连接超时      
  	call.enqueue( new Callback() {           
 	@TargetApi(Build.VERSION_CODES.KITKAT)         
   	@Override           
    public void onFailure(Call call, IOException e) {          
      	Log.i("lhh","Get e :"+e.toString());           
 	}           
 	@Override            
    public void onResponse(Call call, Response response) throws IOException {        
        Message message = new Message();         
        message.what = type;               
 	String json = response.body().string();                 
   	Log.i("lhh","Get json :"+json);               
 	message.obj = json;               
 	handler.sendMessage(message);           
 	}        });    }  }
可以看到 我们在okhttpClient初始化的时候添加了一个
sslSocketFactory()api

把我们写的默认信任所有证书的工具类加入到okhttpClient里面

好了现在运行项目;


ok 成功了 现在我们来解决 okhttp3提供的过时api用新的api替换

	this.okHttpClient = new OkHttpClient.Builder()
        	.sslSocketFactory(TrustAllCerts.createSSLSocketFactory(),
		new TrustAllCerts()
) .hostnameVerifier( new TrustAllCerts.TrustAllHostnameVerifier())
	.connectTimeout(10, TimeUnit.SECONDS)        
	.readTimeout(10, TimeUnit.SECONDS)        
	.build();
在我们初始化okHttpClient 的时候使用新的api
sslSocketFactory();

和之前的api一样的那是参数是两个,之前的是一个参数.这个参数可以直接像我写的那样,还有一种方法比较麻烦的就是创建一个工具类Myx509

public class Myx509 implements X509TrustManager {
    @Override
    public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws 
CertificateException {

    }

    @Override
    public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws 
CertificateException {

    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }

里面的参数就变成

.sslSocketFactory(TrustAllCerts.createSSLSocketFactory(),new Myx509())

这两种都是可以使用的


然后就是获取到指定服务器的证书后,把服务器证书添加到okHttpClinet中了,原理很简单


获取服务器证书(由于我们是自签名证书所以不需要专门获取服务器的证书)如果要使用指定的服务器的证书话,出门左拐百度谢谢


把证书添加到Android 项目里面的assets文件下


放到assets文件后修改网络请求的代码


基本代码不变 新添加一个api    setCertificates(InputStream ... certificates)  


  public void setCertificates(InputStream... certificates)
    {
        try
        {
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null);
            int index = 0;
            for (InputStream certificate : certificates)
            {
                String certificateAlias = Integer.toString(index++);
                keyStore.setCertificateEntry(certificateAlias, 
		certificateFactory.generateCertificate(certificate));

                try
                {
                    if (certificate != null)
                        certificate.close();
                } catch (IOException e)
                {
                }
            }



            SSLContext sslContext = SSLContext.getInstance("TLS");

            TrustManagerFactory trustManagerFactory =
                    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

            trustManagerFactory.init(keyStore);

           
	    
		sslContext.init
        	(
                	null,
                	trustManagerFactory.getTrustManagers(),
                	new SecureRandom()
        	);
		this.okHttpClient = new OkHttpClient.Builder()
		.sslSocketFactory(sslContext.getSocketFactory(),new Myx509())
        	.connectTimeout(10, TimeUnit.SECONDS)
        	.readTimeout(10, TimeUnit.SECONDS)
        	.build();
   		



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


    }

可以看到新添加了我们自己写的一个方法setCertificates(InputStream ... certificates)    这个方法是把assets中服务器证书通过流的形式读取出来并把流添加到okHttpClient中.  把在构造方法里面初始化okHttpClient的操作放到这个方法里面并把证书流添加进去.


接着在ui线程进行网络请求操作的时候先调用这个方法,在进行网络请求


getHttpRequest = new GetHttpRequest(MainActivity.this,handler);
try {
    getHttpRequest.setCertificates(getAssets().open("lh_server.cer"));
} catch (IOException e) {
    e.printStackTrace();
}


然后运行项目

- - !   怎么没反应?  接着看as日志




错误贴出来   

javax.net.ssl.SSLPeerUnverifiedException: Hostname 192.168.1.60 not verified:
                                                certificate: sha256/mtDQaP/u/+7qzPWTX2YjVD+gWR5t0k5bAtcGOUtwnns=
                                                DN: CN=lh,OU=lh,O=lh,L=sh,ST=sh,C=cn
                                                 subjectAltNames: []


- - ! 什么情况? 网上搜索一堆没有找到相应的办法,最后在借鉴了一位博主的博文  附上链接

http://blog.csdn.net/notHeadache/article/details/52133468

主要是讲如何忽略所有证书 这和我们上面说的是默认信任所有证书的效果一样,非常感谢这位博主,我从中借鉴了一个API.

hostnameVerifier()   
这个api  这个API是验证证书的api  借鉴的文章是重写里面的方法 强制返回true 

回到我们的代码中,在初始化okHttpClient的地方添加这个api


this.okHttpClient = new OkHttpClient.Builder()
        .hostnameVerifier(new TrustAllCerts.TrustAllHostnameVerifier())
        .sslSocketFactory(sslContext.getSocketFactory(),new Myx509())
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(10, TimeUnit.SECONDS)
        .build();

可以看到我们新添加的这个api hostnameVerifie(newTrustAllCerts.TrustAllHostnameVerifier())

我们看一下 new TrustAllCerts.TrustAllHostnameVerifier()这个类

public static class TrustAllHostnameVerifier implements HostnameVerifier {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        return true;
    }
}

这个类和之前说的一样让返回true.但我们这样添加以后不是默认通过所有证书而是把我们的证书设置信任

好运行项目




可以正常访问到我们的服务器了 ,如果觉得这样写可能是默认通过所有证书的朋友你可以随便拿一个get请求的https的网站  我用的是 12306网站  https://kyfw.12306.cn/otn/

之后运行项目会报错   提示你的   javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.  不是一个信任的证书


当然也可以把证书用String的形式来用   把证书用String的形式保存只需要在调用setCertificates()方法修改一下参数格式就行了  setCertificates(new Buffer().writeUtf8(CER_lh).inputStream())   其中CER_lh 是你保存证书的String变量名 然后运行项目 效果是一样的



好了现在到了我们重点 双向认证了 做到前面的这些操作只是完成了 单向认证 基本上多数应用已经够用 但比如一

些金融机构就会用到

双向认证.


双向认证 就是 服务器生成自己的.jks和.cer证书文件 客户端生成自己的.jks和.cer文件 服务器需要把客户端的

证书密钥添加到自己的密钥库里 接着客户端做请求的时候带着自己的证书和服务器的证书,服务器收到后与自己密钥

库里面的白名单做匹配 成功可以正常访问失败就拒绝访问


我们之前已经生成 服务器的.jks 和.cer文件了 所以我们还用上面的方法创建客户端的证书文件 取名 lh_client



生成好了后 我们要修改自己的服务器了 使我们的服务器支持双向验证

打开我们的server.xml文件继续到 配置https 的地方配置

找到clientAuth="false" 改成 true 然后 新属性

truststoreFile="C:\Users\Trust\lh_client.cer"

添加以后启动tomcat会发现报错了Invalid keystore format


这个错是因为keystore的格式不合法

我们需要将客户端证书添加到 .jks中

在doc下输入命令

keytool -import -alias lh_client -file lh_clinet.cer -keystore lh_client_for_sever.jks




好了 在目下可以找到生成好的lh_client_for_sever.jks文件



在server.xml中配置路径 truststoreFile="C:\Users\Trust\lh_client_for_sever.jks"


最后的配置是这样的


 <Connector SSLEnabled="true" clientAuth="true"

keystoreFile="C:\Users\Trust\lh_server.jks"

keystorePass="000000" maxThreads="150" port="8443"

protocol="org.apache.coyote.http11.Http11Protocol"

scheme="https" secure="true" sslProtocol="TLS"

truststoreFile="C:\Users\Trust\lh_client_for_sever.jks"/>


启动我们的tomcat服务器 在浏览器输入我们的链接 https://localhost:8443/LH/index.html 会出现

不接受您的登录证书,或者您可能没有提供登录证书。

这个时候服务器设置成功了




使用我们的APP输入上面的网址点击确定 没有反应,后台爆出一个错

javax.net.ssl.SSLHandshakeException: Handshake failed

现在我们给APP添加双向验证的代码 修改我们网路请求的代码


 public void setCertificates(InputStream... certificates)
    {
        try
        {
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null);
            int index = 0;
            for (InputStream certificate : certificates)
            {
                String certificateAlias = Integer.toString(index++);
                keyStore.setCertificateEntry(certificateAlias, certificateFactory.
                generateCertificate(certificate));

                try
                {
                    if (certificate != null)
                        certificate.close();
                } catch (IOException e)
                {
                }
            }



            SSLContext sslContext = SSLContext.getInstance("TLS");

            TrustManagerFactory trustManagerFactory =
                    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

            trustManagerFactory.init(keyStore);




            //初始化keystore
            KeyStore clientKeyStore = KeyStore.getInstance("BKS");
            clientKeyStore.load(context.getAssets().open("lh_clinet.bks"), "000000".toCharArray());

            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory
	    .getDefaultAlgorithm());
            keyManagerFactory.init(clientKeyStore, "000000".toCharArray());

            sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers()
	    , new SecureRandom());


//            okHttpClient.setSslSocketFactory(sslContext.getSocketFactory());


                this.okHttpClient = new OkHttpClient.Builder().sslSocketFactory(sslContext.
		getSocketFactory())
                        .connectTimeout(10, TimeUnit.SECONDS)
                        .readTimeout(10, TimeUnit.SECONDS)
                        .build();



            this.okHttpClient = new OkHttpClient.Builder()
                    .connectTimeout(10, TimeUnit.SECONDS)
                    .sslSocketFactory(sslContext.getSocketFactory(),new TrustAllCerts())
                    .hostnameVerifier(new TrustAllCerts.TrustAllHostnameVerifier())
                    .readTimeout(10, TimeUnit.SECONDS)
                    .build();

            Log.i("lhh", "setCertificates: ");



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


    }

可以看到我们添加了一段 Keystore的代码 这个代码里面有一个

lh_clinet.bks  

这是什么东西呢? 这个就是我们的lh_clinet.jks 因为.jks在java 里面是默认支持的 但是在Android 支持的

是.bks文件 所以我们要把,jks文件转换成.bks文件

转换工具是 portecle-1.9 我会上传或者大家可以百度进官网下载 使用步骤也很简单就是安装好后点击

portecle.jar 文件 会自动打开相应的操作页面 点击File---->Open Keystore file 选择客户端的证书文件

lh_client.jks 输入密钥 在点击 Tools Change Keystore Type ----->BKS 设置密钥 会提示成功然后关闭

当前页面 会提示你保存的位置 注意 保存的时候 必须带上.bks 不然是无效的


http://download.csdn.net/detail/leng_wen_rou/9767218  工具链接














把生成的.bks文件放到项目的assets文件中

在项目中不要用.jks的要用.bks

不然会报错

Java.io.IOException: Wrong version of key store

在代码里面把服务器证书以及客户端证书的密钥写入

运行项目:



ok 请求成功

感谢看到这里的朋友,篇幅比较长从头到尾都讲了一遍如果还有什么不明白的可以留言.写这么长就是想把我遇到

的问题以及解决办法都说出来,让大家少走点弯路.

从AndroudStudio复制的代码有点多看的很费劲,因为我不太会弄,希望大家多多包含.

谢谢



本文借鉴了 张洪洋大哥的博客 再次感谢

附上链接

http://blog.csdn.net/lmj623565791/article/details/48129405

END

猜你喜欢

转载自blog.csdn.net/edmond999/article/details/80169765
今日推荐