Android会话保持实现,包括webview会话保持,多个后端session的会话保持
转载请注明出处:http://blog.csdn.net/u011457774/article/details/78904668
一、概述
这是我的第一篇博客,一直想写,但是不知道写些什么,主要是好多都可以百度到。最近遇到个比较麻烦的问题,麻烦不是因为难,而是不好排查问题,主要是这个知识点需要对后端开发有一定的了解,不然不好排查,还好我以前在学校做过java的后端开发,不然我也不知道怎么下手。所以决定写篇博客,记录下。本文主要介绍的有三点,Android端会话保持的实现,webview的会话保持,多个后端session的会话保持,可能篇幅有点长。最近项目里用到了会话保持技术,虽然不是什么难题,但是因为之前没接触过类似的,所以也踩了不少坑,期间,没有各种百度,谷歌过,但是没有找到一个比较满意的答案。当时的一个现象是,app中webview不能正常打开,请求其它接口,不能正确的返回请求的响应,最懵逼的是,同样的webview和接口,IOS都能正常访问,因为IOS的底层会自动实现会话保持,只有我们Android端不能正常访问,我当时都懵逼了,后台的同事也懵逼了。 经过自己的各种摸索,最终还是解决了问题,现在总结一下踩过的坑,算是学习笔记吧,也希望能帮助其他遇到类似问题的人。
二、什么是会话保持
简单来说,就是客户端与服务器之间可以用通一个身份进行多次会话交流,即进行多次接口请求。比如这样的一个场景,我们登录淘宝,登录成功后,,我们去挑选商品,加入购物车,然后去购物车结账。我们会发现这中间是跳转了几个页面的,我们的操作也很顺畅,中间不会说再让我们输入用户名和密码,以确认我们的身份。但是以程序员的角度来看,这中间它其实是经过了几个接口的,比如,登录的接口,加入购物车的接口,购物车中结账的接口,起码有这三个接口。那么,我们有没有想过这样一个问题,比如登录的是张三这个用户,这个是在登录接口提交的用户信息,加入购物车的接口怎么知道加入购物车的是现在这个登录的张三加入的购物车,而不是李四,王五加入的购物车,同样,结账的接口也有这个问题。这里面就用到了会话保持,当我们登录成功后,后面跳转的页面,购物车,结账等都是带有我们的用户信息的,直至你关闭了浏览器,当你关闭浏览器,再重新打开后就是一个新的会话。有些人会问,当关闭再打开后不用再次登录页可以购买东西啊,这里面就是cookie的东西了,有兴趣的可以自行查资料,这里就不深入了。
三、为什么要用会话保持
至于为什么要用会话保持,应该是为了方便吧,当然这个只是我个人的理解,你想,如过不用会话保持,客户端请求后台的的接口中,所有需要用到用户信息的接口,你都得传个suerId或者token给服务器,不然服务器是没法知道你是哪个用户的,也就拿不到你的用户信息。对于后台服务器而言,每个用到用户信息的接口,他都得去根据suseId或者token去数据库或者缓存中查找一遍,缓存还好,去数据库查的话,用户量大了还是很费时的,耗性能。所以一般都是存在缓存中吧,以java Web为例,一般是存在session缓存中(因为后端的话我只学过java后端,只能以java举例了)。这里再介绍一下,java Web中有四大缓存作用域:
- page:指当前页面有效。在一个jsp页面里有效
- request:指在一次请求的全过程中有效,即从http请求到服务器处理结束,返回响应的整个过程,存放在HttpServletRequest对象中
- session:用户全局变量,在整个会话期间都有效。只要页面不关闭就一直有效(或者直到用户一直未活动导致会话过期)
- application:是程序全局变量,它的有效范围是整个应用。 整个应用是指从应用启动,到应用结束。没有说“从服务器启动,到服务器关闭”,是因为一个服务器可能部署多个应用,当然你关闭了服务器,就会把上面所有的应用都关闭了。 application作用域里的变量,它们的存活时间是最长的,如果不进行手工删除,它们就一直可以使用。
好了,说了这么多,下面开始进入正题,如何实现Android端的会话保持,webview的会话保持,多个后短服务的session会话保持。
四、手动实现Android端会话保持
我这里用网络请求框架是okhttp,不过我用的是鸿洋大神封装过的一个改善库,它里面也有对会话保持的一些封装,不过我当时还不会用, 后面再仔细看了后才会用
https://github.com/hongyangAndroid/okhttputils
首先,我们需要知道,这个所谓的会话保持,其实就是在http或者https请求头或者响应头当中附了一些参数。响应头里面的字段叫做”Set-Cookie”,请求头里面的字段叫做”cookie”。这里不做详解,有兴趣的可以看下面这篇博客
http://blog.csdn.net/yaochangliang159/article/details/50433682
我们需要做的就是,当会话建立的时候我们把响应头里面的”Set-Cookie”当中的sessionId保存起来,在其他需要会话保持的接口的请求头加入”cookie”中,以”cookie”为键,保存的sessionId为值。这是因为,后台服务从缓存中拿出我们的用户信息的时候一般是从session中获取的,而每一个session都对应着一个会话, 拥有唯一的sessionId。之前我之所以不能正确的访问接口,就是因为我没有在请头中添加”cookie”键值对,所以就相当于新建了一个会话,领外生成了一个sessionId,所以后台服务器不能拿到我的个人信息,我就拿不到正确的响应。至于什么时候保存”Set-Cookie”中的sessionId,就看你的实际需求,会话的起点是哪,如果是登录成功后就是会话的起点,那么就在登录成功的接口的响应头操作,如果你们的后端服务是单独提供一个接口,让你传userId或者token来建立会话,那就在这个接口的响应头做操作。下面,看一下例子:
OkHttpUtils.get().url(Consts.HOMECHECKOUT_URL + MyApplication.getUserCredential())
.build().execute(new Callback() {
@Override
public Object parseNetworkResponse(Response response, int id) throws Exception {
JSONObject jsonObject = new JSONObject(response.body().string());
if (jsonObject.getInt("code") == 200) {
//获取session的操作,session放在cookie头
Headers headers = response.headers();
List<String> cookies = headers.values("Set-Cookie");
String session = cookies.get(0);
//COOKIE = session.substring(0, session.indexOf(";"));//截取sessionid
SPUtils.putSessionCookie(getContext(), session)
Log.i("tag", "headers: " + headers.toString());
Log.i("tag", "sessionId: " + session);
}
return null;
}
@Override
public void onError(Call call, Exception e, int id) {
ToastUtils.showLongToast("服务器异常!");
}
@Override
public void onResponse(Object response, int id) {
}
});
这里需要注意,这里的parseNetworkResponse(Response response, int id)中的response是okhttp返回的原始响应数据,没有经过任何修改封装,这是打印的log
我们可以看到,这里打印的请求头有很多信息, 其中就有我们需要的 Set-Cookie,Set-Cookie是一个列表,不过这里只有一个sessionid,也就是JSESSIONID=B42166621784D9C2F5BA3E17AED00B16;path=/zhg_webapp/;HttpOnly
正常情况下,我们只需要用String截取JSESSIONID=B42166621784D9C2F5BA3E17AED00B16这个,放在请求头就够用了,但是,这里有一个坑,在webview会话保持时,会出问题,具体什么问题,下面再说,所以这里我把整个sessId都保存在SharedPreferences中了,也就是JSESSIONID=B42166621784D9C2F5BA3E17AED00B16;path=/zhg_webapp/;HttpOnly。我们已经拿到sessionId了,下面看下怎么使用:
OkHttpUtils.get().addHeader("cookie", SPUtils.getSessionCookie()).url(Consts.HOME_URL).build().execute(new StringCallback() {
@Override
public void onError(Call call, Exception e, int id) {
}
@Override
public void onResponse(String response, int id) {
});
可以看到,这里所做的操作,就是在请求需要会话保持的接口当中加了个请求头,就是”cookie”,值就是刚才我们在响应头当中获取的sessionId,即JSESSIONID=B42166621784D9C2F5BA3E17AED00B16;path=/zhg_webapp/;HttpOnly
至于如何添加响应头,每个网络框架都不一样,这个就自己解决了,我这里用的是鸿洋封装过后的okhttp库,上面已经说过并给出链接了。到这里,Adroid端会话保持就已经实现了,当然这种方式稍微复杂些,每个接口都得自己手动添加请求头,有点麻烦,那我们能不能通过okhttp来实现呢,答案是肯定的。
五、 通过okhttp网络请求框架实现会话保持
okhttp作为现在Android主流的网络请求框架,也是带有同步cookie,即会话保持,不过需要我们自己做些处理。okhttp提供了一个接口CookieJar,这个接口中有两个需要我们重写的方法,saveFromResponse(HttpUrl url, List cookies)以及loadForRequest(HttpUrl url),看名字我们就知道,saveFromResponse是用来保存cookie(其中包含sessionId)的,loadForRequest是用来在请求接口的时候添加cookie请求头的,这个方法的话网上其他地方也有,我这里只是做下系统的介绍,这里用的也是鸿神的okhttpUtils中的方法。首先写一个接口CookieStore,用于添加cookie,获取cookie,移除cookie
public interface CookieStore
{
void add(HttpUrl uri, List<Cookie> cookie);
List<Cookie> get(HttpUrl uri);
List<Cookie> getCookies();
boolean remove(HttpUrl uri, Cookie cookie);
boolean removeAll();
}
然后写一个CookieStore的一个实现类,这个实现类可以根据实际的需求实现,你可以把sessionId保存在SharedPreferences ,内存,文件等当中,我这里是保存在内存中
public class MyMemoryCookieStore implements CookieStore
{
private final HashMap<String, List<Cookie>> allCookies = new HashMap<>();
@Override
public void add(HttpUrl url, List<Cookie> cookies)
{
List<Cookie> oldCookies = allCookies.get(url.host());
if (oldCookies != null)
{
Iterator<Cookie> itNew = cookies.iterator();
Iterator<Cookie> itOld = oldCookies.iterator();
while (itNew.hasNext())
{
String va = itNew.next().name();
while (va != null && itOld.hasNext())
{
String v = itOld.next().name();
if (v != null && va.equals(v))
{
itOld.remove();
}
}
}
oldCookies.addAll(cookies);
} else
{
allCookies.put(url.host(), cookies);
}
}
@Override
public List<Cookie> get(HttpUrl uri)
{
List<Cookie> cookies = allCookies.get(uri.host());
if (cookies == null)
{
cookies = new ArrayList<>();
allCookies.put(uri.host(), cookies);
}
return cookies;
}
@Override
public boolean removeAll()
{
allCookies.clear();
return true;
}
@Override
public List<Cookie> getCookies()
{
List<Cookie> cookies = new ArrayList<>();
Set<String> httpUrls = allCookies.keySet();
for (String url : httpUrls)
{
cookies.addAll(allCookies.get(url));
}
return cookies;
}
@Override
public boolean remove(HttpUrl uri, Cookie cookie)
{
List<Cookie> cookies = allCookies.get(uri.host());
if (cookie != null)
{
return cookies.remove(cookie);
}
return false;
}
}
我们还要写一个CookieJarImpl实现okhttp提供的CookieJar接口:
public class CookieJarImpl implements CookieJar {
private CookieStore cookieStore;
public CookieJarImpl(CookieStore cookieStore) {
if (cookieStore == null) Exceptions.illegalArgument("cookieStore can not be null.");
this.cookieStore = cookieStore;
}
@Override
public synchronized void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
cookieStore.add(url, cookies);
}
@Override
public synchronized List<Cookie> loadForRequest(HttpUrl url) {
return cookieStore.get(url);
}
public CookieStore getCookieStore() {
return cookieStore;
}
}
至于怎么使用呢,我是在Application的onCreate()方法中初始化的OkHttpClient,然后全局配置, 当然你也可以根据自己的需求,单个接口,单个配置,看下面:
CookieJar cookieJar = new CookieJarImpl(new MyMemoryCookieStore());
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(10000L, TimeUnit.MILLISECONDS)
.readTimeout(10000L, TimeUnit.MILLISECONDS)
.addNetworkInterceptor(HttpLogUtil.init(false))
.addInterceptor(CacheInterceptor.REWRITE_CACHE_CONTROL_INTERCEPTOR2)
.cache(cache)
.cookieJar(cookieJar)
.build();
OkHttpUtils.initClient(okHttpClient);
下面,看下webview中如何实现会话保持
六、Android webview实现会话保持
下面看下具体的实现:
/**
* 同步一下cookie
*/
public static void synCookies(Context context, String url) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.createInstance(context);
}
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
cookieManager.removeSessionCookie();// 移除
cookieManager.removeAllCookie();
cookieManager.setCookie(url, SPUtils.getSessionCookie());//为url设置cookie
}
CookieSyncManager.getInstance().sync();//同步cookie
}
这是为webview同步cookie的方法,cookieManager.setCookie(url, SPUtils.getSessionCookie())中的url就是你需要加载webview的连接地址,SPUtils.getSessionCookie(),就是上面第四点中手动实现会话保持时获取的sessionID。这里重点提示一下,这里有一个坑,上面说过,这里回答一下,就是cookieManager.setCookie()这个方法的第二个参数必须是用一个完整的cooklie,什么是一个完整的cookie,就是这个JSESSIONID=B42166621784D9C2F5BA3E17AED00B16;path=/zhg_webapp/;HttpOnly,这其中包含有项目名,上面也讲过我们使用JSESSIONID=B42166621784D9C2F5BA3E17AED00B16作为值,也可以做到会话保持,但是webview里面就会踩坑,我当时遇到这问题也搞老半天呢,如果我们使用sessioID(即JSESSIONID=B42166621784D9C2F5BA3E17AED00B16)作为cookieManager.setCookie()方法的第二个参数,就会遇到一个现象,我们能够正常打开webview,但是我们不能在webview中做与用户信息挂钩的操作,比如评论,所以第二个参数需要一个完整的cookie,即JSESSIONID=B42166621784D9C2F5BA3E17AED00B16;path=/zhg_webapp/;HttpOnly,不能只截取JSESSIONID=B42166621784D9C2F5BA3E17AED00B16作为值,那样会留坑。好了,有了这个方法,使用起来就很简单了,我们在加载webview之前,调用一下这个方法,就行了
synCookies(WebViewActivity.this, _url);
_webView.loadUrl(_url.trim());
至于okhttp实现的,其实也一样的,MyMemoryCookieStore中有一个public List get(HttpUrl uri)方法,我们只需要获取到他,传入会话起点的接口地址(就是你保存sessionId的那么接口,不需要传参数),就能拿到对应的cookie了,具体,如下,
public static MyMemoryCookieStore memoryCookieStore = new MyMemoryCookieStore();
@Override
public void onCreate() {
CookieJar cookieJar = new CookieJarImpl(memoryCookieStore);
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(10000L, TimeUnit.MILLISECONDS)
.readTimeout(10000L, TimeUnit.MILLISECONDS)
.addNetworkInterceptor(HttpLogUtil.init(false))
.addInterceptor(CacheInterceptor.REWRITE_CACHE_CONTROL_INTERCEPTOR2)
.cache(cache)
.cookieJar(cookieJar)
.build();
OkHttpUtils.initClient(okHttpClient);
}
webview 中:
/**
* 同步一下cookie
*/
public static void synCookies(Context context, String url) {
HttpUrl httpUrl = null;
try {
httpUrl = HttpUrl.get(new URI(Consts.HOMECHECKOUT_URL));
} catch (URISyntaxException e) {
e.printStackTrace();
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.createInstance(context);
}
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
cookieManager.removeSessionCookie();// 移除
cookieManager.removeAllCookie();
//cookieManager.setCookie(url, SPUtils.getSessionCookie());//为url设置cookie
if (HuiyouApplication.memoryCookieStore.get(httpUrl).size() > 0) {
cookieManager.setCookie(url, MyApplication.memoryCookieStore.get(httpUrl).get(0).toString());//为url设置cookie
}
CookieSyncManager.getInstance().sync();//同步cookie
}
七、多个后端session的会话保持
何为多个后端session的会话保持呢?这里有这么一个场景,当我们的项目功能又多又复杂的时候,后端的接口可能就需要进行模块分化了,也就是在服务器中跑着多个后端项目,我们访问的接口不仅是一个项目了,而是多个项目。列如,后端提供给我们的接口会是这样https://test.plus.cn/app1/login
,https://test.plus.cn/app2/getUserAddress
,这样的接口,这里是同一个域名的两个不同项目app1和app2,我们需要做的是为两个不同的后端项目分别做会话保持,还有一种情况是不同域名的接口,如https://test.plus1.cn/app/login
,https://test.plus2.cn/app/getUserAddress
,这是两个不同域名的接口,有两种办法,一种是手动的方法,就是在两个项目会话开始的时候分别保存Set-Cookie的sessionId,,然后再在对应的接口中添加对应的请求头即可,这种方法比较好理解,这里就不做详细介绍了。下面我们介绍使用okhttp来做多个后端项目的session会话保持。
起初,我以为使用上面的第五种方法应该可以解决,毕竟是框架嘛,怎么说也会智能一点,应该能够智能的识别对应项目的cookie,然而,我错了,在实际使用的时候,两个的项目的cookie使用错了,导致出了一系列问题,比如,拿项目1的cookie用在了项目2中,拿项目2的cookie用在了项目1中然后就引发一堆错了。当时也是很懵逼的,各种百度,谷歌,都没有找到解决的办法,没办法,只能自己再看看代码,研究了。首先我把目标放在了实现了okhttp的CookieJar的CookieJarImpl类中,这里有两个方法
@Override
public synchronized void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
cookieStore.add(url, cookies);
}
@Override
public synchronized List<Cookie> loadForRequest(HttpUrl url) {
return cookieStore.get(url);
}
saveFromResponse 是保存cookie,loadForRequest是在请求接口的时候添加cookie,可以看到这里并没有具体的操作,然后我就把目标转移到了具体的实现类,即上面的MyMemoryCookieStore类中。
public void add(HttpUrl url, List<Cookie> cookies)
{
List<Cookie> oldCookies = allCookies.get(url.host());
if (oldCookies != null)
{
Iterator<Cookie> itNew = cookies.iterator();
Iterator<Cookie> itOld = oldCookies.iterator();
while (itNew.hasNext())
{
String va = itNew.next().name();
while (va != null && itOld.hasNext())
{
String v = itOld.next().name();
if (v != null && va.equals(v))
{
itOld.remove();
}
}
}
oldCookies.addAll(cookies);
} else
{
allCookies.put(url.host(), cookies);
}
}
可以看到,这里添加cookie的时候是以url.host()为键值的,那么这个是什么呢,我们打印一下
12-26 16:25:52.916 7296-7567/net.szhuiyou.meetyou I/tag: cookie: test.plus.cn
可以看到这个就是我们接口的域名(当然我这改过了,不是实际项目的域名),然后刚巧,我们项目的两个后端接口项目的域名是一样的,只是项目名不同,所以我解决的思路就是以项目名为键值,保存cookie,怎么得到HttpUrl中的项目名呢,我也找了一会,在HttpUrl中有一个pathSegments()方法,我看着挺像,打印了一下, 据我观察发现了,它里面有三个值,第一个事项目名,第二个是请求的接口所在的类名,第三个是请求的接口的方法名,是不是没个接口都有这三个值,我就没验证过了,当然,项目名应该是肯定有的,所以我只要把url.pathSegments().get(0)即项目名作为键值保存cookie就行了,具体如下
public class MyMemoryCookieStore implements CookieStore {
private final HashMap<String, List<Cookie>> allCookies = new HashMap<>();
@Override
public void add(HttpUrl url, List<Cookie> cookies) {
List<Cookie> oldCookies = allCookies.get(url.pathSegments().get(0));
if (oldCookies != null) {
Iterator<Cookie> itNew = cookies.iterator();
Iterator<Cookie> itOld = oldCookies.iterator();
while (itNew.hasNext()) {
Cookie cookie = itNew.next();
String va = cookie.name();
while (va != null && itOld.hasNext()) {
String v = itOld.next().name();
if (v != null && va.equals(v)) {
itOld.remove();
}
}
}
oldCookies.addAll(cookies);
} else {
allCookies.put(url.pathSegments().get(0), cookies);
}
}
@Override
public List<Cookie> get(HttpUrl uri) {
List<Cookie> cookies = allCookies.get(uri.pathSegments().get(0));
if (cookies == null) {
cookies = new ArrayList<>();
allCookies.put(uri.pathSegments().get(0), cookies);
}
return cookies;
}
@Override
public boolean removeAll() {
allCookies.clear();
return true;
}
@Override
public List<Cookie> getCookies() {
List<Cookie> cookies = new ArrayList<>();
Set<String> httpUrls = allCookies.keySet();
for (String url : httpUrls) {
cookies.addAll(allCookies.get(url));
}
return cookies;
}
@Override
public boolean remove(HttpUrl uri, Cookie cookie) {
List<Cookie> cookies = allCookies.get(uri.pathSegments().get(0));
if (cookie != null) {
return cookies.remove(cookie);
}
return false;
}
}
这个需要根据实际的需求写,我这里是域名一样,项目名不同,所以我写的是以项目名为键值,如果你的多个后端接口中,域名是不一样的,那么就不用更改,使用第五种的方式就行,即以域名为键值,保存cookie,当然你也可以根据自己的需求,以其他键值保存cookie。到这里,本文终于结束了,第一次写博客,对排版什么的不熟悉,还是挺费时的。