本菜鸟什么web后台云端什么的都没学过,就开始看起Android来了,以至于今天在做一个项目的登陆功能,用Volley框架实现,因为之前没做过,看着后台给的接口方法,一条url获取验证码图片成功了,开心的不行,就打算一条url把user_name、password等参数POST到服务器,坐等服务器响应返回登陆成功的消息了,然而磨磨唧唧半天服务器返回的都是验证码错误的结果,这我就郁闷了。
网上一查资料,才发现事情没有想象中的简单。你想呀,你获取到一个来自服务器的验证码,然后又把这个验证码当成参数传回去,这时服务器怎么知道你的验证码是不是正确的验证码呢?也许在你获取了验证码的同时也有许多其他人也获取了验证码,所以显然,服务器在同一时刻返回了许多有效的验证码,也因此无法直接确认某个客户端返回的验证码是不是正确的。
搜一下,这种情况专业些的说法是
“Web应用程序是使用HTTP协议传输数据的,HTTP协议是无状态的协议。一旦数据交换完毕,客户端与服务器端的连接就会关闭,再次交换数据需要建立新的连接,这就意味着服务器无法从连接上跟踪会话”
再百度一下Http是怎么传输的,
“通常HTTP消息包括客户机向服务器的请求消息和服务器向客户机的响应消息。这两种类型的消息由一个起始行,一个或者多个头域,一个指示头域结束的空行和可选的消息体组成。HTTP的头域包括通用头,请求头,响应头和实体头四个部分。每个头域由一个域名,冒号(:)和域值三部分组成。域名是大小写无关的,域值前可以添加任何数量的空格符,头域可以被扩展为多行,在每行开始处,使用至少一个空格或制表符”。
HTTP请求信息由3部分组成:
l 请求方法URI协议/版本
l 请求头(Request Header)
l 请求正文HTTP响应
HTTP应答与HTTP请求相似,HTTP响应也由3个部分构成,分别是:
l 状态行
l 响应头(Response Header)
l 响应正文
在接收和解释请求消息后,服务器会返回一个HTTP响应消息。
状态行由协议版本、数字形式的状态代码、及相应的状态描述,各元素之间以空格分隔。
格式: HTTP-Version Status-Code Reason-Phrase CRLF
为了弥补http协议的这种单次沟通的缺陷,Cookit机制出现了。
Cookit,甜饼(我也不知道为什么叫cookit),其实就是一小段保存在客户端本地的文本,它的内容总是以一个“JSESSIONID=?”的键值对开头,而JSESSIONID呢,就是session id,不用想就知道是代表一个session的唯一索引。
原来服务器在创建一个与客户端的会话时,会为其保留一个session(翻译过来就是会话),session会有一个在服务器中唯一的id,就是JSESSIONID,客户端发送第一条请求时,它的请求头(许多键值对)中将没有Cookit,或者有Cookit但是Cookit中并没有JSESSIONID的值,此时服务器会认为这是第一次客户端和自己交流,为了下次交流时自己还能认出它,服务器会为这个客户端创建一个session并分配一个JSESSIONID,然后在响应头中附加上Set-Cookit提示,收到Set-Cookit的客户端应该把JSESSIONID从Set-Cookit中保存为Cookit存放在本地,下次发起请求时将Cookit当做POST的参数捎给服务端,使得会话继续而不是重新开始:
响应头示例:
请求头示例:
顺带一提,使用Volley进行Http的POST请求是没办法直接加参数的,想要使用POST方式进行http请求,需要做一些方法的重写处理:
比如使用StringRequest进行POST请求时,至需要重写getParams()方法,在其中传入参数键值对,但是在使用JSonRequest进行POST请求时,重写的getParams()方法是不会被调用的,也就无法达到传参的目的,此时需要大神从源码的角度帮你分析原因和解决方法: [Android] 解决Volley中JsonObjectRequest的Post请求添加参数的问题;
好了大概知道要怎么做了,
使用Volley实现session会话保持,我们只需要在第一次请求得到服务器响应时,将响应头中,Set-Cookit的JSESSIONID保存为Cookit:
//parseNetworkResponse方法在服务端响应后调用
@Override
protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
setCookieFromHeader(response.headers);
return super.parseNetworkResponse(response);
}
//如果参数 响应头 中有SET_COOKIT,说明是第一次连接,需要根据SET_COOKIT建立新的Cookit保存在sharePreference中
public void setCookieFromHeader(Map<String, String> responseHeaders) {
if (responseHeaders.containsKey(SET_COOKIE)
&& responseHeaders.get(SET_COOKIE).startsWith(JSESSIONID)) {
String cookie = responseHeaders.get(SET_COOKIE);
if (cookie.length() > 0) {
String[] splitCookie = cookie.split(";");
String[] splitSessionId = splitCookie[0].split("=");
cookie = splitSessionId[1];
SharedPreferences.Editor prefEditor = preferences.edit();
prefEditor.putString(JSESSIONID, cookie);
prefEditor.apply();
}
}
}
接着在下次进行请求时,将Cookit捎给服务端:
//getHeaders在向服务器发送参数前被调用
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = super.getHeaders();
//这一段代码并非多余的,在Volley中,header在未被传参数之前是一个Collections.emptyMap()引用,空的键值对表,此引用是immutable(不可改变的),改变时即将报错,至于这样一个不能修改的空Map的意义在哪里,底部overFlow上看到有解释
if (headers == null
|| headers.equals(Collections.emptyMap())) {
headers = new HashMap<>();
}
Utils.getInstance(mContext).addCookieToHeader(headers);
return headers;
}
// 在参数 请求头 中,加入从sharePreference得到的“Cookit:JSESSIONID=xxxxxxxx
public void addCookieToHeader(Map<String, String> requestHeaders) {
String sessionId = preferences.getString(JSESSIONID, "");
if (sessionId.length() > 0) {
StringBuilder builder = new StringBuilder();
builder.append(JSESSIONID);
builder.append("=");
builder.append(sessionId);
if (requestHeaders.containsKey(COOKIE)) {
builder.append("; ");
builder.append(requestHeaders.get(COOKIE));
}
requestHeaders.put(COOKIE, builder.toString());
}
}
这样就可以登录接口啦
资料来源:使用Volley实现session会话保持、为什么要用Collections.emptyMap()、jsessionid什么时候 生成的、完整的Set-Cookie 头 .