Android原生同步登陆状态到H5网页避免二次登陆

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

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

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

1、同步原理

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

实现的方法有不少,能够用原生与 JS 的通讯机制把登陆信息发送给H5,关于原生与 JS 双向通讯,我以前写了一篇详解文章,不熟悉的同窗能够看看:前端

Android webview 与 js(Vue) 交互vue

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

2、安卓端代码

这是安卓开发须要作的。android

先说一下步骤:git

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

看似步骤不少,其实就是获得服务端返回的数据,再经过 CookieManager 保存到 cookie 中这么简单,只不过中间须要作几回数据转换。程序员

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

将 UserInfo 保存到 SharedPreferences

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

//将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> 

 

 

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

3、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


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

在这里插入图片描述

相关文章
相关标签/搜索