本文解决的问题是目前流行的 Android/IOS 原生应用内嵌 WebView 网页时,原生与H5页面登录状态的同步。

大多数混合开发应用的登录都是在原生页面中,这就牵扯到一个问题,如何把登录状态传给H5页面呢?总不能打开网页时再从网页中登录一次系统吧… 两边登录状态的同步是必须的。

100 多位经验丰富的开发者参与,在 Github 上获得了近 1000 个 star 的全栈全平台开源项目想了解或参与吗?
项目地址:https://github.com/cachecats/coderiver

一、同步原理

其实同步登录状态就是把登录后服务器返回的 token 、userId 等登录信息传给H5网页,在发送请求时将必要的校验信息带上。只不过纯H5开发是自己有一个登录页,登录之后保存在 Cookie 或其他地方;混合开发中H5网页自己不维护登录页,而是由原生维护,打开 webview 时将登录信息传给网页。

实现的方法有很多,可以用原生与 JS 的通信机制把登录信息发送给H5,关于原生与 JS 双向通信,我之前写了一篇详解文章,不熟悉的同学可以看看:

Android webview 与 js(Vue) 交互

这里我们用另一种更简单的方法,通过安卓的 CookieManager 把 cookie 直接写入 webview 中。

二、安卓端代码

这是安卓开发需要做的。

先说一下步骤:

  1. 准备一个对象 UserInfo ,用来接收服务端返回的数据。
  2. 登录成功后把 UserInfo 格式化为 json 字符串存入 SharedPreferences 中。
  3. 打开 webview 时从 SharedPreferences 取出上一步保存的 UserInfo 。
  4. 新建一个 Map 将 UserInfo 以键值对的格式保存起来,便于下一步保存为 cookie。
  5. 将 UserInfo 中的信息通过 CookieManager 保存到 cookie 中。

看似步骤很多,其实就是得到服务端返回的数据,再通过 CookieManager 保存到 cookie 中这么简单,只不过中间需要做几次数据转换。

我们按照上面的步骤一步步看代码。UserInfo 对象就不贴了,都是些基本的信息。

将 UserInfo 保存到 SharedPreferences

登录接口请求成功后,会拿到 UserInfo 对象。在成功回调里通过下面一行代码保存 UserInfo 到 SharedPreferences

//将UserData存储到SP
SPUtils.putUserData(context, result.getData());
  • 1
  • 2

SPUtils 是操作 SharedPreferences 的工具类,代码如下。

包含了保存和取出 UserInfo 的方法(代码中对象名是 UserData),保存时通过 Gson 将对象格式化为 json 字符串,取出时通过 Gson 将 json 字符串格式化为对象。

public class SPUtils {
/**
* 保存在手机里面的文件名
*/
public static final String FILE_NAME = "share_data"; /**
* 存储用户信息
*
* @param context
* @param userData
*/
public static void putUserData(Context context, UserData userData) {
SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit(); Gson gson = new Gson();
String json = gson.toJson(userData, UserData.class);
editor.putString(SPConstants.USER_DATA, json);
SharedPreferencesCompat.apply(editor);
} /**
* 获取用户数据
*
* @param context
* @return
*/
public static UserData getUserData(Context context) {
SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
Context.MODE_PRIVATE);
String json = sp.getString(SPConstants.USER_DATA, "");
Gson gson = new Gson();
UserData userData = gson.fromJson(json, UserData.class);
return userData;
}
}

取出 UserInfo 并保存到 cookie 中

这里封装了一个带进度条的 ProgressWebviewActivity ,调用时直接打开这个 Activity 并将网页的 url 地址传入即可。在 Activity 的 onResume 生命周期方法中执行同步 cookie 的逻辑。为什么在 onResume 中执行?防止App 从后台切到前台 webview 重新加载没有拿到 cookie,可能放在 onCreate 大多数情况下也没有问题,但放到 onResume 最保险。

@Override
protected void onResume() {
super.onResume();
Logger.d("onResume " + url);
//同步 cookie 到 webview
syncCookie(url);
webSettings.setJavaScriptEnabled(true);
} /**
* 同步 webview 的Cookie
*/
private void syncCookie(String url) {
boolean b = CookieUtils.syncCookie(url);
Logger.d("设置 cookie 结果: " + b);
}

同步操作封装到了 CookieUtils 工具类中,下面是 CookieUtils 的代码:

这个工具类中一共干了三件事,从 SharedPreferences 中取出 UserInfo,将 UserInfo 封装到 Map 中,遍历 Map 依次存入 cookie。

public class CookieUtils {

    /**
* 将cookie同步到WebView
*
* @param url WebView要加载的url
* @return true 同步cookie成功,false同步cookie失败
* @Author JPH
*/
public static boolean syncCookie(String url) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.createInstance(MyApplication.getAppContext());
}
CookieManager cookieManager = CookieManager.getInstance(); Map<String, String> cookieMap = getCookieMap();
for (Map.Entry<String, String> entry : cookieMap.entrySet()) {
String cookieStr = makeCookie(entry.getKey(), entry.getValue());
cookieManager.setCookie(url, cookieStr);
}
String newCookie = cookieManager.getCookie(url);
return TextUtils.isEmpty(newCookie) ? false : true;
} /**
* 组装 Cookie 里需要的值
*
* @return
*/
public static Map<String, String> getCookieMap() { UserData userData = SPUtils.getUserData(MyApplication.getAppContext());
String accessToken = userData.getAccessToken();
Map<String, String> headerMap = new HashMap<>();
headerMap.put("access_token", accessToken);
headerMap.put("login_name", userData.getLoginName());
headerMap.put("refresh_token", userData.getRefreshToken());
headerMap.put("remove_token", userData.getRemoveToken());
headerMap.put("unitId", userData.getUnitId());
headerMap.put("unitType", userData.getUnitType() + "");
headerMap.put("userId", userData.getUserId()); return headerMap;
} /**
* 拼接 Cookie 字符串
*
* @param key
* @param value
* @return
*/
private static String makeCookie(String key, String value) {
Date date = new Date();
date.setTime(date.getTime() + 3 * 24 * 60 * 60 * 1000); //3天过期
return key + "=" + value + ";expires=" + date + ";path=/";
}
}

syncCookie() 方法最后两行是验证存入 cookie 成功了没。

到这里 Android 这边的工作就做完了,H5可以直接从 Cookie 中取出 Android 存入的数据。

ProgressWebviewActivity封装

下面是封装的带进度条的 ProgressWebviewActivity

/**
* 带进度条的 WebView。采用原生的 WebView
*/
public class ProgressWebviewActivity extends Activity { private WebView mWebView;
private ProgressBar web_bar;
private String url;
private WebSettings webSettings; @Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_web);
url = getIntent().getStringExtra("url");
init();
} private void init() {
//Webview
mWebView = findViewById(R.id.web_view);
//进度条
web_bar = findViewById(R.id.web_bar);
//设置进度条颜色
web_bar.getProgressDrawable().setColorFilter(Color.RED, android.graphics.PorterDuff.Mode.SRC_IN); //对WebView进行必要配置
settingWebView();
settingWebViewClient(); //加载url地址
mWebView.loadUrl(url);
} /**
* 对 webview 进行必要的配置
*/
private void settingWebView() {
webSettings = mWebView.getSettings();
//如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript
// 若加载的 html 里有JS 在执行动画等操作,会造成资源浪费(CPU、电量)
// 在 onStop 和 onResume 里分别把 setJavaScriptEnabled() 给设置成 false 和 true 即可
webSettings.setJavaScriptEnabled(true); //设置自适应屏幕,两者合用
webSettings.setUseWideViewPort(true); //将图片调整到适合webview的大小
webSettings.setLoadWithOverviewMode(true); // 缩放至屏幕的大小 //缩放操作
webSettings.setSupportZoom(true); //支持缩放,默认为true。是下面那个的前提。
webSettings.setBuiltInZoomControls(true); //设置内置的缩放控件。若为false,则该WebView不可缩放
webSettings.setDisplayZoomControls(false); //隐藏原生的缩放控件 //其他细节操作
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); //没有网络时加载缓存
//webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); //关闭webview中缓存
webSettings.setAllowFileAccess(true); //设置可以访问文件
webSettings.setJavaScriptCanOpenWindowsAutomatically(true); //支持通过JS打开新窗口
webSettings.setLoadsImagesAutomatically(true); //支持自动加载图片
webSettings.setDefaultTextEncodingName("utf-8");//设置编码格式 //不加的话有些网页加载不出来,是空白
webSettings.setDomStorageEnabled(true); //Android 5.0及以上版本使用WebView不能存储第三方Cookies解决方案
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().setAcceptThirdPartyCookies(mWebView, true);
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
} /**
* 设置 WebViewClient 和 WebChromeClient
*/
private void settingWebViewClient() {
mWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
Logger.d("onPageStarted");
} @Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Logger.d("onPageFinished");
} // 链接跳转都会走这个方法
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Logger.d("url: ", url);
view.loadUrl(url);// 强制在当前 WebView 中加载 url
return true;
} @Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.proceed();
super.onReceivedSslError(view, handler, error);
}
}); mWebView.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
Logger.d("current progress: " + newProgress);
//更新进度条
web_bar.setProgress(newProgress); if (newProgress == 100) {
web_bar.setVisibility(View.GONE);
} else {
web_bar.setVisibility(View.VISIBLE);
}
} @Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
Logger.d("标题:" + title);
}
});
} /**
* 同步 webview 的Cookie
*/
private void syncCookie(String url) {
boolean b = CookieUtils.syncCookie(url);
Logger.d("设置 cookie 结果: " + b);
} /**
* 对安卓返回键的处理。如果webview可以返回,则返回上一页。如果webview不能返回了,则退出当前webview
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && mWebView.canGoBack()) {
mWebView.goBack();// 返回前一个页面
return true;
}
return super.onKeyDown(keyCode, event);
} @Override
protected void onResume() {
super.onResume();
Logger.d("onResume " + url);
//同步 cookie 到 webview
syncCookie(url);
webSettings.setJavaScriptEnabled(true);
} @Override
protected void onStop() {
super.onStop();
webSettings.setJavaScriptEnabled(false);
}
}

Activity 的布局文件:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"> <WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" /> <ProgressBar
android:id="@+id/web_bar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="-7dp"
android:layout_marginTop="-7dp"
android:indeterminate="false"
/>
</RelativeLayout>

上面两个文件复制过去就能用,进度条的颜色可以任意定制。

三、H5端代码(Vue实现)

相比之下H5这边的代码就比较少了,只需在进入页面时从 cookie 中取出 token 等登录信息。

其实如果你们后端的校验是从 cookie 中取 token 的话,前端可以不做任何处理就能访问成功。

因为其他接口需要用到 userId 等信息,所以在刚进入页面时从 cookie 取出 UserInfo 并保存到 vuex 中,在任何地方都可以随时用 UserInfo 啦。

//从Cookie中取出登录信息并存入 vuex 中
getCookieAndStore() {
let userInfo = {
"unitType": CookieUtils.getCookie("unitType"),
"unitId": CookieUtils.getCookie("unitId"),
"refresh_token": CookieUtils.getCookie("refresh_token"),
"userId": CookieUtils.getCookie("userId"),
"access_token": CookieUtils.getCookie("access_token"),
"login_name": CookieUtils.getCookie("login_name"),
};
this.$store.commit("setUserInfo", userInfo);
}

把这个方法放到尽可能早的执行到的页面的生命周期方法中,比如 created()mounted()、或 activated()。因为我的页面中用到了 <keep-alive>,所以为了确保每次进来都能拿到信息,把上面的方法放到了 activated() 中。

上面用到了一个工具类 :CookieUtils,代码如下:

主要是根据名字取出 cookie 中对应的值。

/**
* 操作cookie的工具类
*/
export default { /**
* 设置Cookie
* @param key
* @param value
*/
setCookie(key, value) {
let exp = new Date();
exp.setTime(exp.getTime() + 3 * 24 * 60 * 60 * 1000); //3天过期
document.cookie = key + '=' + value + ';expires=' + exp + ";path=/"; }, /**
* 移除Cookie
* @param key
*/
removeCookie(key) {
setCookie(key, '', -1);//这里只需要把Cookie保质期退回一天便可以删除
}, /**
* 获取Cookie
* @param key
* @returns {*}
*/
getCookie(key) {
let cookieArr = document.cookie.split('; ');
for (let i = 0; i < cookieArr.length; i++) {
let arr = cookieArr[i].split('=');
if (arr[0] === key) {
return arr[1];
}
}
return false;
}
}

以上就是用最简单的方法同步安卓原生登录状态到H5网页中的方法。如果你有更便捷的方式,欢迎在评论区交流。


全栈全平台开源项目 CodeRiver

CodeRiver 是一个免费的项目协作平台,愿景是打通 IT 产业上下游,无论你是产品经理、设计师、程序员或是测试,还是其他行业人员,只要有好的创意、想法,都可以来 CodeRiver 免费发布项目,召集志同道合的队友一起将梦想变为现实!

CodeRiver 本身还是一个大型开源项目,致力于打造全栈全平台企业级精品开源项目。涵盖了 React、Vue、Angular、小程序、ReactNative、Android、Flutter、Java、Node 等几乎所有主流技术栈,主打代码质量。

目前已经有近 100 名优秀开发者参与,github 上的 star 数量将近 1000 个。每个技术栈都有多位经验丰富的大佬坐镇,更有两位架构师指导项目架构。无论你想学什么语言处于什么技术水平,相信都能在这里学有所获。

通过 高质量源码 + 博客 + 视频,帮助每一位开发者快速成长。

项目地址:https://github.com/cachecats/coderiver


您的鼓励是我们前行最大的动力,欢迎点赞,欢迎送小星星✨ ~

Android原生同步登录状态到H5网页避免二次登录的更多相关文章

  1. App保持登录状态的常用方法(转)

    我们在使用App时,一次登录后App如果不主动退出登录或者清除数据,App会在很长一段时间内保持登录状态,或者让用户感觉到登录一次就不用每次都输入用户密码才能进行登录.银行.金融涉及到支付类的App一 ...

  2. App保持登录状态的常用方法

    我们在使用App时,一次登录后App如果不主动退出登录或者清除数据,App会在很长一段时间内保持登录状态,或者让用户感觉到登录一次就不用每次都输入用户密码才能进行登录.银行.金融涉及到支付类的App一 ...

  3. 利用express-session插件实现nodejs中登录状态的保存

    什么是session? session就是会话,客户端和服务器直接的会话.他的粒度比http链接更粗,一次会话包含了多次连接.即一个session是多次http连接的集合.从我的客户端连接到服务器到关 ...

  4. phpcms V9静态判断会员登录状态的方法

    phpcms v9如何在任意地方判断会员的登录状态呢?在php中是比较好判断的,代码如下 <?php if (!$_userid){ echo"会员没有登录"; }else ...

  5. Django(十三)状态保持 —— cookie与session+ajax异步请求+session记住登录状态+cookie记住登录名密码

    一.状态保持的概述 http协议是无状态的.下一次去访问一个页面时并不知道上一次对这个页面做了什么.因此引入了cookie.session两种方式来配合解决此问题. Duplicate entry:重 ...

  6. 单点登录,系统B如何辨别用户已登录系统A

    首先系统A去访问受限资源,跳转到sso认证中心https://login.sso.com/login?redirectURL=https://www.a.com/center,用户登录成功之后,sso ...

  7. Android:webView加载h5网页视频,播放不了,以及横屏全屏的问题和实现自定义加载进度条的效果

    1.webView加载h5网页视频,播放不了,android3.0之后要在menifest添加硬件加速的属性 android:hardwareAccelerated="true". ...

  8. JAVAEE——宜立方商城10:使用freemarker实现网页静态化、ActiveMq同步生成静态网页、Sso单点登录系统分析

    1. 学习计划 1.使用freemarker实现网页静态化 2.ActiveMq同步生成静态网页 2. 网页静态化 可以使用Freemarker实现网页静态化. 2.1. 什么是freemarker ...

  9. Postman+Postman interceptor的安装和使用-解决把chrome浏览器登录状态同步到postman进行有依赖的接口测试 Postman 使用方法详解

    Postman+Postman interceptor的安装和使用-解决把chrome浏览器登录状态同步到postman进行有依赖的接口测试   问题引入:做接口测试时,有依赖关系的接口往往不好测试( ...

随机推荐

  1. PHP-FPM的知识点

    https://blog.csdn.net/resilient/article/details/82420863 这个URL,将php的各种模式与知识点说清楚了. 因为php-fpm默认编译进了php ...

  2. C#模拟鼠标、键盘操作

    C语言 在程序中打开网页,模拟鼠标点击.键盘输入 一.简述         记--使用C语言 打开指定网页,并模拟鼠标点击.键盘输入.实现半自动填写账号密码,并登录网站(当然现在的大部分网站都有验证码 ...

  3. MySQL之自连接

    自连接就是说,在同一个表中,看做是两个表,下表表示 找每个人的领导,如果没有领导,显示无领导,eid 对应 leaderid,请看员工表 mysql> select * from emp; +- ...

  4. Cocos2d-x学习小结 配置篇

    Cocos2d-x学习小结 配置篇 学习工具:Cocos2d-x用户手册,<Cocos2d-x游戏开发之旅> 首先官网下载cocos2d-x源码,安装vs2019.如果没有安装python ...

  5. Java 内部类和Lambda

    Java内部类 内部类又称为嵌套类,是在类中定义另外一个类.内部类可以处于方法内/外,内部类的成员变量/方法名可以和外部类的相同.内部类编译后会成为完全不同的两个类,分别为outer.class和ou ...

  6. 使用GCOV进行代码覆盖率统计

    GCOV是随GCC一起发布的用于代码覆盖率统计的工具,一般配合其图形化工具LCOV一起使用. 一.安装 GCOV不需要单独安装,LCOV下载后执行sudo make install即可完成安装. 二. ...

  7. Linux端口转发工具rinetd

    介绍:Rinetd是为在一个Unix和Linux操作系统中为重定向传输控制协议(TCP)连接的一个工具.Rinetd是单一过程的服务器,它处理任何数量的连接到在配置文件etc/rinetd中指定的地址 ...

  8. python 数据分析

    pandas 格式化数据的读取 numpy 提供数组处理,类似matlap matplotlib 数据可视化 https://www.cnblogs.com/5poi/p/7148000.html

  9. php 正则达达示中的模式修正符

    我们通过元字符和原子完成了正则表达示的入门.有一些特殊情况我们依然需要来处理.深圳dd马达 如果abc在第二行的开始处如何匹配?我不希望正则表达示特别贪婪的匹配全部,只匹配一部份怎么办? 这个时候,我 ...

  10. jsp大附件上传,支持断点续传

    我们平时经常做的是上传文件,上传文件夹与上传文件类似,但也有一些不同之处,这次做了上传文件夹就记录下以备后用. 这次项目的需求: 支持大文件的上传和续传,要求续传支持所有浏览器,包括ie6,ie7,i ...