大三的时候写过一段时间的 php ,那时候已经对 html、css、js、cookie、session 这些东西了一点认知,但基本都是浮于表面,知其然而不知其所以然。于是这几天翻了翻《图解http》,书上的知识和自己的以前的理解结合起来,感觉对于 http 协议有了一些比较深刻的理解。
在这里把那些知识点整理记录一下,而因为 HTTP 协议的知识点较多,所以会有一个系列的博客去介绍。这篇文章就先讲一下 Cookie 和 Session 吧。
Cookie & Session 毕业设计有个功能是实现用户的注册登录,而注册账号的时候需要有输入验证码的功能。
众所周知,HTTP 协议是无状态协议,即协议对于事务处理没有记忆能力。但就像这里,我们需要实现一个验证码功能,我们从服务器获取验证码的图片,然后再将用户输入的验证码传回服务器进行对比,这就要求服务器记录之前随机生成的验证码了。于是,两种用于保持HTTP连接状态的技术就应运而生了,一个是 Cookie,而另一个则是 Session。
Cookie Cookie 是由服务器端生成,发送给 User-Agent(一般是浏览器),浏览器会将 Cookie 的 key/value 保存到某个目录下的文本文件内,下次请求同一网站时就发送该 Cookie 给服务器(前提是浏览器设置为启用 cookie )。
我们可以在 chrome 浏览器页面按 F12 打开控制台,选择 Network 标签查看与网站进行 HTTP 协议交流的数据。
这里我们看看登录B站 的时候究竟发生了什么事情:
首先我们输入账号密码和验证码之后点击登陆,浏览器会发生账号密码等数据给服务器,之后服务器返回数据。我们查看返回报文的 header 可以看到一堆的 Set-Cookie 字段:
客户端会把这些 cookie 记录下来,在下次访问服务器的时候就会把它们传回给服务器,这样就能实现数据的保持:
Session 但 Cookie 的数据都是保存在客户端的,客户端很容易就能查看和修改 cookie,十分不安全。例如 chrome 有一个 EditThisCookie 插件,就能直接查看修改网页的 cookie:
所以就有了存放在服务器端的内存中的 sessio,session可以看作一个存放在服务器的键值对集。
当服务器创建一个 session 对象的时候,就会对应的生成一个sessionId,服务器可以在 session 中写入数据,但它不会将session 的内容告诉客户端,它只会将生成的 sessionId 以 cookie 的方式传给客户端,而客户端在下次访问服务器的时候把 sessionId 又传回给服务器,这样服务器就能找到之前保存的数据了。
在 php 中这个 sessionid 的名字默认叫做 PHPSESSID,当然也能在php.ini中修改。
因为 session 保存在服务器中,所以安全性比 cookie 高的多。
关于 Cookie 和 Session,各位有兴趣的话可以自己去网上搜索一下,或者希望对 HTTP 协议有更深入的理解的话可以去读一下《图解http》。最近就在读这本书,等读完我会写一篇博客,介绍一些 HTTP 协议的重点知识,这里就不再多说了。
okHttp3 使用 Cookie okHttp3 的 cookie 管理方式对比 okHttp2 有了很大的变化,这里有一篇博客专门介绍OkHttp3实现Cookies管理及持久化 。希望各位在读我这篇博客之前先浏览一下。
okHttp3 使用 CookieJar 接口来管理 Cookie:
1 2 3 4 5 6 7 8 9 10 public interface CookieJar { CookieJar NO_COOKIES = new CookieJar () { @Override public void saveFromResponse (HttpUrl url, List<Cookie> cookies) { } @Override public List<Cookie> loadForRequest (HttpUrl url) { return Collections.emptyList(); } };
我们只要在创建 OkHttpClient 的时候指定我们自己的 CookieJar 就能让 OkHttpClient 实现 Cookie 的自动管理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class MainActivity extends AppCompatActivity { private Map<String, List<Cookie>> mCookieStore; ... private OkHttpClient createHttpClient () { CookieJar cookieJar = new CookieJar () { @Override public void saveFromResponse (HttpUrl url, List<Cookie> cookies) { mCookieStore.put(url.host(), cookies); } @Override public List<Cookie> loadForRequest (HttpUrl url) { List list = mCookieStore.get(url.host()); return list != null ? list : new ArrayList <Cookie>(); } }; return new OkHttpClient .Builder() .cookieJar(cookieJar) .build(); } ... }
这里有点要注意,我们是拿 host 作 map 的 key 值,《OkHttp3实现Cookies管理及持久化》 这篇博文直接用 url 当 key 值,这样的话该 Cookie 就只能在当前页面可用了,而我们是整个网站可用。
Retrofit 使用 Cookie 在 Retrofit 中使用 Cookie 就更加简单了,因为它内部使用 OkHttp3,只要把之前设置了 CookieJar 的 OkHttpClient 设置给它就可以了:
1 2 3 4 5 6 HttpService service = new Retrofit .Builder() .client(mHttpClient) .baseUrl(mBaseUrl) .addConverterFactory(GsonConverterFactory.create()) .build() .create(HttpService.class);
验证码小 Demo 现状我们用一个实现了验证码功能的小 Demo 来更加深刻的理解之前所讲的知识。
首先我写了两个页面:
访问第一个页面能获得一张随机的验证码图片,而第二个页面使用 GET 方法来检测验证码(键值为 verifycode)。
获取验证码图片 首先我们使用 OkHttp3 访问第一个页面,下载一张验证码图片,将它显示在 ImageView 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 private void loadVerifyCode (final ImageView imageView, final HttpUrl url) { final Request request = new Request .Builder() .url(url) .build(); new Thread (new Runnable () { @Override public void run () { try { okhttp3.Response response = mHttpClient.newCall(request).execute(); InputStream is = response.body().byteStream(); final Bitmap bm = BitmapFactory.decodeStream(is); runOnUiThread(new Runnable () { @Override public void run () { imageView.setImageBitmap(bm); } }); List<Cookie> cookies = Cookie.parseAll(request.url(), response.headers()); for (int i = 0 ; i < cookies.size(); i++) { Log.d("cookie" , "request headers: " + cookies.get(i).toString()); } } catch (IOException e) { e.printStackTrace(); } } }).start(); }
运行程序可以看到验证码被显示出来:
我们还能能看到服务器返回的 Cookie 信息,因为我的网页使用 php 写的,所以它返回了一个 PHPSESSID ,用来标记服务器保存的 Session 对象。服务器的 Session 对象里面就保存了验证码的值。之后我们把用户输入的验证码传会服务器的时候只要把这个 PHPSESSID 一同传过去,服务器就能找到之前生成的验证码的值,并和用户所输入的进行对比了:
发送用户输入的验证码 这里我们直接使用 Retrofit 将用户输入的验证码传给服务器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public void onClick (View v) { HttpService service = new Retrofit .Builder() .client(mHttpClient) .baseUrl(mBaseUrl) .addConverterFactory(GsonConverterFactory.create()) .build() .create(HttpService.class); String verifyCode = mEditText.getText().toString(); if (verifyCode == null ) { return ; } Call<VerifyCodeResult> call = service.getResult(verifyCode); call.enqueue(new Callback <VerifyCodeResult>() { @Override public void onResponse (Call<VerifyCodeResult> call, Response<VerifyCodeResult> response) { Log.d("cookie" , "request headers: " + call.request().headers().toString()); Toast.makeText(MainActivity.this , response.body().getResult(), Toast.LENGTH_SHORT).show(); } @Override public void onFailure (Call<VerifyCodeResult> call, Throwable t) { } });
1 2 3 4 public interface HttpService { @GET("okhttp_cookie_demo/checkverifycode.php") Call<VerifyCodeResult> getResult (@Query("verifycode") String yam) ; }
1 2 3 4 5 6 7 8 9 10 11 public class VerifyCodeResult { private String result; public String getResult () { return result; } public void setResult (String result) { this .result = result; } }
运行程序,输入验证码可以看到结果:
程序正常运行,但看 log 输出,Request 并没有把 PHPSESSID 传过去。这是怎么回事?没有传 PHPSESSID,服务器又怎么能知道之前生成的验证码是什么?
在 CookieJar 的 loadForRequest 方法设置断点,可以发现在发送验证码的时候确实有调用,随之运行到 HttpEngine 的源码,发现原来框架创建了个新的 Resquest 副本,将 Cookie 传入这个新的副本中去连接服务器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public void sendRequest () throws RequestException, RouteException, IOException { ... Request request = networkRequest(userRequest); ... } ... private Request networkRequest (Request request) throws IOException { Request.Builder result = request.newBuilder(); if (request.header("Host" ) == null ) { result.header("Host" , hostHeader(request.url())); } if (request.header("Connection" ) == null ) { result.header("Connection" , "Keep-Alive" ); } if (request.header("Accept-Encoding" ) == null ) { transparentGzip = true ; result.header("Accept-Encoding" , "gzip" ); } List<Cookie> cookies = client.cookieJar().loadForRequest(request.url()); if (!cookies.isEmpty()) { result.header("Cookie" , cookieHeader(cookies)); } if (request.header("User-Agent" ) == null ) { result.header("User-Agent" , Version.userAgent()); } return result.build(); }
原来如此,操作都使用了副本 Request 去执行,怪不得我们直接用下面的代码输出,请求头部不能看到 PHPSESSID 的 Cookie 值:
1 2 3 4 5 @Override public void onResponse (Call<VerifyCodeResult> call, Response<VerifyCodeResult> response) { Log.d("cookie" , "request headers: " + call.request().headers().toString()); Toast.makeText(MainActivity.this , response.body().getResult(), Toast.LENGTH_SHORT).show(); }
Glide 使用 Cookie Glide 是Google推荐的图片加载库,用来加载图片十分之方便,最少只需要三行代码就能将网络图片加载到 ImageView 上。
我有在 Glide 的文档上看到它也能使用 OkHttp3,理论上应该也能使用设置 OkHttpClient 的方法使用 Cookie。
但弄了很久还是没有搞定,等以后有时间找到实现方法再把这一节补全。
Demo 完整代码 MainActivity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 public class MainActivity extends AppCompatActivity { private static final String mBaseUrl = "http://www.islinjw.cn" ; private static final String mVerifyCideUrl = "http://www.islinjw.cn/okhttp_cookie_demo/verifycode.php" ; private OkHttpClient mHttpClient; private EditText mEditText; private ImageView mImageView; private Map<String, List<Cookie>> mCookieStore; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); mEditText = (EditText) findViewById(R.id.input); mImageView = (ImageView) findViewById(R.id.yzm); mCookieStore = new HashMap <>(); mHttpClient = createHttpClient(); loadVerifyCode(mImageView, HttpUrl.parse(mVerifyCideUrl)); } private OkHttpClient createHttpClient () { CookieJar cookieJar = new CookieJar () { @Override public void saveFromResponse (HttpUrl url, List<Cookie> cookies) { mCookieStore.put(url.host(), cookies); } @Override public List<Cookie> loadForRequest (HttpUrl url) { List list = mCookieStore.get(url.host()); return list != null ? list : new ArrayList <Cookie>(); } }; return new OkHttpClient .Builder() .cookieJar(cookieJar) .build(); } private void loadVerifyCode (final ImageView imageView, final HttpUrl url) { final Request request = new Request .Builder() .url(url) .build(); new Thread (new Runnable () { @Override public void run () { try { okhttp3.Response response = mHttpClient.newCall(request).execute(); InputStream is = response.body().byteStream(); final Bitmap bm = BitmapFactory.decodeStream(is); runOnUiThread(new Runnable () { @Override public void run () { imageView.setImageBitmap(bm); } }); List<Cookie> cookies = Cookie.parseAll(request.url(), response.headers()); for (int i = 0 ; i < cookies.size(); i++) { Log.d("cookie" , "response headers: " + cookies.get(i).toString()); } } catch (IOException e) { e.printStackTrace(); } } }).start(); } public void onClick (View v) { HttpService service = new Retrofit .Builder() .client(mHttpClient) .baseUrl(mBaseUrl) .addConverterFactory(GsonConverterFactory.create()) .build() .create(HttpService.class); String verifyCode = mEditText.getText().toString(); if (verifyCode == null ) { return ; } Call<VerifyCodeResult> call = service.getResult(verifyCode); call.enqueue(new Callback <VerifyCodeResult>() { @Override public void onResponse (Call<VerifyCodeResult> call, Response<VerifyCodeResult> response) { Log.d("cookie" , "request headers: " + call.request().headers().toString()); Toast.makeText(MainActivity.this , response.body().getResult(), Toast.LENGTH_SHORT).show(); } @Override public void onFailure (Call<VerifyCodeResult> call, Throwable t) { } }); } }
HttpService:
1 2 3 4 public interface HttpService { @GET("okhttp_cookie_demo/checkverifycode.php") Call<VerifyCodeResult> getResult (@Query("verifycode") String yam) ; }
1 2 3 4 5 6 7 8 9 10 11 public class VerifyCodeResult { private String result; public String getResult () { return result; } public void setResult (String result) { this .result = result; } }