(转)Android原生同步登录状态到H5网页避免二次登录

原文链接:https://juejin.im/post/5c89f81cf265da2db3059684

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

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

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

一、同步原理

其实同步登录状态就是把登录后服务器返回的 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

 
  1. //将UserData存储到SP

  2. SPUtils.putUserData(context, result.getData());

  3. 复制代码

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

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

 
  1. public class SPUtils {

  2. /**

  3. * 保存在手机里面的文件名

  4. */

  5. public static final String FILE_NAME = "share_data";

  6.  
  7. /**

  8. * 存储用户信息

  9. *

  10. * @param context

  11. * @param userData

  12. */

  13. public static void putUserData(Context context, UserData userData) {

  14. SharedPreferences sp = context.getSharedPreferences(FILE_NAME,

  15. Context.MODE_PRIVATE);

  16. SharedPreferences.Editor editor = sp.edit();

  17.  
  18. Gson gson = new Gson();

  19. String json = gson.toJson(userData, UserData.class);

  20. editor.putString(SPConstants.USER_DATA, json);

  21. SharedPreferencesCompat.apply(editor);

  22. }

  23.  
  24. /**

  25. * 获取用户数据

  26. *

  27. * @param context

  28. * @return

  29. */

  30. public static UserData getUserData(Context context) {

  31. SharedPreferences sp = context.getSharedPreferences(FILE_NAME,

  32. Context.MODE_PRIVATE);

  33. String json = sp.getString(SPConstants.USER_DATA, "");

  34. Gson gson = new Gson();

  35. UserData userData = gson.fromJson(json, UserData.class);

  36. return userData;

  37. }

  38. }

  39.  
  40. 复制代码

取出 UserInfo 并保存到 cookie 中

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

 
  1. @Override

  2. protected void onResume() {

  3. super.onResume();

  4. Logger.d("onResume " + url);

  5. //同步 cookie 到 webview

  6. syncCookie(url);

  7. webSettings.setJavaScriptEnabled(true);

  8. }

  9.  
  10. /**

  11. * 同步 webview 的Cookie

  12. */

  13. private void syncCookie(String url) {

  14. boolean b = CookieUtils.syncCookie(url);

  15. Logger.d("设置 cookie 结果: " + b);

  16. }

  17. 复制代码

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

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

 
  1. public class CookieUtils {

  2.  
  3. /**

  4. * 将cookie同步到WebView

  5. *

  6. * @param url WebView要加载的url

  7. * @return true 同步cookie成功,false同步cookie失败

  8. * @Author JPH

  9. */

  10. public static boolean syncCookie(String url) {

  11. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {

  12. CookieSyncManager.createInstance(MyApplication.getAppContext());

  13. }

  14. CookieManager cookieManager = CookieManager.getInstance();

  15.  
  16. Map cookieMap = getCookieMap();

  17. for (Map.Entry entry : cookieMap.entrySet()) {

  18. String cookieStr = makeCookie(entry.getKey(), entry.getValue());

  19. cookieManager.setCookie(url, cookieStr);

  20. }

  21. String newCookie = cookieManager.getCookie(url);

  22. return TextUtils.isEmpty(newCookie) ? false : true;

  23. }

  24.  
  25. /**

  26. * 组装 Cookie 里需要的值

  27. *

  28. * @return

  29. */

  30. public static Map getCookieMap() {

  31.  
  32. UserData userData = SPUtils.getUserData(MyApplication.getAppContext());

  33. String accessToken = userData.getAccessToken();

  34. Map headerMap = new HashMap<>();

  35. headerMap.put("access_token", accessToken);

  36. headerMap.put("login_name", userData.getLoginName());

  37. headerMap.put("refresh_token", userData.getRefreshToken());

  38. headerMap.put("remove_token", userData.getRemoveToken());

  39. headerMap.put("unitId", userData.getUnitId());

  40. headerMap.put("unitType", userData.getUnitType() + "");

  41. headerMap.put("userId", userData.getUserId());

  42.  
  43. return headerMap;

  44. }

  45.  
  46. /**

  47. * 拼接 Cookie 字符串

  48. *

  49. * @param key

  50. * @param value

  51. * @return

  52. */

  53. private static String makeCookie(String key, String value) {

  54. Date date = new Date();

  55. date.setTime(date.getTime() + 3 * 24 * 60 * 60 * 1000); //3天过期

  56. return key + "=" + value + ";expires=" + date + ";path=/";

  57. }

  58. }

  59. 复制代码

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

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

ProgressWebviewActivity封装

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

 
  1. /**

  2. * 带进度条的 WebView。采用原生的 WebView

  3. */

  4. public class ProgressWebviewActivity extends Activity {

  5.  
  6. private WebView mWebView;

  7. private ProgressBar web_bar;

  8. private String url;

  9. private WebSettings webSettings;

  10.  
  11. @Override

  12. protected void onCreate(@Nullable Bundle savedInstanceState) {

  13. super.onCreate(savedInstanceState);

  14. setContentView(R.layout.activity_web);

  15. url = getIntent().getStringExtra("url");

  16. init();

  17. }

  18.  
  19. private void init() {

  20. //Webview

  21. mWebView = findViewById(R.id.web_view);

  22. //进度条

  23. web_bar = findViewById(R.id.web_bar);

  24. //设置进度条颜色

  25. web_bar.getProgressDrawable().setColorFilter(Color.RED, android.graphics.PorterDuff.Mode.SRC_IN);

  26.  
  27. //对WebView进行必要配置

  28. settingWebView();

  29. settingWebViewClient();

  30.  
  31. //同步 cookie 到 webview

  32. syncCookie(url);

  33.  
  34. //加载url地址

  35. mWebView.loadUrl(url);

  36. }

  37.  
  38. /**

  39. * 对 webview 进行必要的配置

  40. */

  41. private void settingWebView() {

  42. webSettings = mWebView.getSettings();

  43. //如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript

  44. // 若加载的 html 里有JS 在执行动画等操作,会造成资源浪费(CPU、电量)

  45. // 在 onStop 和 onResume 里分别把 setJavaScriptEnabled() 给设置成 false 和 true 即可

  46. webSettings.setJavaScriptEnabled(true);

  47.  
  48. //设置自适应屏幕,两者合用

  49. webSettings.setUseWideViewPort(true); //将图片调整到适合webview的大小

  50. webSettings.setLoadWithOverviewMode(true); // 缩放至屏幕的大小

  51.  
  52. //缩放操作

  53. webSettings.setSupportZoom(true); //支持缩放,默认为true。是下面那个的前提。

  54. webSettings.setBuiltInZoomControls(true); //设置内置的缩放控件。若为false,则该WebView不可缩放

  55. webSettings.setDisplayZoomControls(false); //隐藏原生的缩放控件

  56.  
  57. //其他细节操作

  58. webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); //没有网络时加载缓存

  59. //webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); //关闭webview中缓存

  60. webSettings.setAllowFileAccess(true); //设置可以访问文件

  61. webSettings.setJavaScriptCanOpenWindowsAutomatically(true); //支持通过JS打开新窗口

  62. webSettings.setLoadsImagesAutomatically(true); //支持自动加载图片

  63. webSettings.setDefaultTextEncodingName("utf-8");//设置编码格式

  64.  
  65. //不加的话有些网页加载不出来,是空白

  66. webSettings.setDomStorageEnabled(true);

  67.  
  68. //Android 5.0及以上版本使用WebView不能存储第三方Cookies解决方案

  69. if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

  70. CookieManager.getInstance().setAcceptThirdPartyCookies(mWebView, true);

  71. webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);

  72. }

  73. }

  74.  
  75. /**

  76. * 设置 WebViewClient 和 WebChromeClient

  77. */

  78. private void settingWebViewClient() {

  79. mWebView.setWebViewClient(new WebViewClient() {

  80. @Override

  81. public void onPageStarted(WebView view, String url, Bitmap favicon) {

  82. super.onPageStarted(view, url, favicon);

  83. Logger.d("onPageStarted");

  84. }

  85.  
  86. @Override

  87. public void onPageFinished(WebView view, String url) {

  88. super.onPageFinished(view, url);

  89. Logger.d("onPageFinished");

  90. }

  91.  
  92. // 链接跳转都会走这个方法

  93. @Override

  94. public boolean shouldOverrideUrlLoading(WebView view, String url) {

  95. Logger.d("url: ", url);

  96. view.loadUrl(url);// 强制在当前 WebView 中加载 url

  97. return true;

  98. }

  99.  
  100. @Override

  101. public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {

  102. handler.proceed();

  103. super.onReceivedSslError(view, handler, error);

  104. }

  105. });

  106.  
  107. mWebView.setWebChromeClient(new WebChromeClient() {

  108. @Override

  109. public void onProgressChanged(WebView view, int newProgress) {

  110. super.onProgressChanged(view, newProgress);

  111. Logger.d("current progress: " + newProgress);

  112. //更新进度条

  113. web_bar.setProgress(newProgress);

  114.  
  115. if (newProgress == 100) {

  116. web_bar.setVisibility(View.GONE);

  117. } else {

  118. web_bar.setVisibility(View.VISIBLE);

  119. }

  120. }

  121.  
  122. @Override

  123. public void onReceivedTitle(WebView view, String title) {

  124. super.onReceivedTitle(view, title);

  125. Logger.d("标题:" + title);

  126. }

  127. });

  128. }

  129.  
  130.  
  131. /**

  132. * 同步 webview 的Cookie

  133. */

  134. private void syncCookie(String url) {

  135. boolean b = CookieUtils.syncCookie(url);

  136. Logger.d("设置 cookie 结果: " + b);

  137. }

  138.  
  139. /**

  140. * 对安卓返回键的处理。如果webview可以返回,则返回上一页。如果webview不能返回了,则退出当前webview

  141. */

  142. @Override

  143. public boolean onKeyDown(int keyCode, KeyEvent event) {

  144. if (keyCode == KeyEvent.KEYCODE_BACK && mWebView.canGoBack()) {

  145. mWebView.goBack();// 返回前一个页面

  146. return true;

  147. }

  148. return super.onKeyDown(keyCode, event);

  149. }

  150.  
  151. @Override

  152. protected void onResume() {

  153. super.onResume();

  154. Logger.d("onResume " + url);

  155. //同步 cookie 到 webview

  156. syncCookie(url);

  157. webSettings.setJavaScriptEnabled(true);

  158. }

  159.  
  160. @Override

  161. protected void onStop() {

  162. super.onStop();

  163. webSettings.setJavaScriptEnabled(false);

  164. }

  165. }

  166. 复制代码

Activity 的布局文件:

 
  1. android:layout_width="match_parent"

  2. android:layout_height="match_parent">

  3.  
  4. android:id="@+id/web_view"

  5. android:layout_width="match_parent"

  6. android:layout_height="match_parent" />

  7.  
  8. android:id="@+id/web_bar"

  9. style="?android:attr/progressBarStyleHorizontal"

  10. android:layout_width="match_parent"

  11. android:layout_height="wrap_content"

  12. android:layout_marginBottom="-7dp"

  13. android:layout_marginTop="-7dp"

  14. android:indeterminate="false"

  15. />

  16. 复制代码

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

三、H5端代码(Vue实现)

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

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

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

 
  1. //从Cookie中取出登录信息并存入 vuex 中

  2. getCookieAndStore() {

  3. let userInfo = {

  4. "unitType": CookieUtils.getCookie("unitType"),

  5. "unitId": CookieUtils.getCookie("unitId"),

  6. "refresh_token": CookieUtils.getCookie("refresh_token"),

  7. "userId": CookieUtils.getCookie("userId"),

  8. "access_token": CookieUtils.getCookie("access_token"),

  9. "login_name": CookieUtils.getCookie("login_name"),

  10. };

  11. this.$store.commit("setUserInfo", userInfo);

  12. }

  13. 复制代码

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

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

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

 
  1. /**

  2. * 操作cookie的工具类

  3. */

  4. export default {

  5.  
  6. /**

  7. * 设置Cookie

  8. * @param key

  9. * @param value

  10. */

  11. setCookie(key, value) {

  12. let exp = new Date();

  13. exp.setTime(exp.getTime() + 3 * 24 * 60 * 60 * 1000); //3天过期

  14. document.cookie = key + '=' + value + ';expires=' + exp + ";path=/";

  15.  
  16. },

  17.  
  18. /**

  19. * 移除Cookie

  20. * @param key

  21. */

  22. removeCookie(key) {

  23. setCookie(key, '', -1);//这里只需要把Cookie保质期退回一天便可以删除

  24. },

  25.  
  26. /**

  27. * 获取Cookie

  28. * @param key

  29. * @returns {*}

  30. */

  31. getCookie(key) {

  32. let cookieArr = document.cookie.split('; ');

  33. for (let i = 0; i < cookieArr.length; i++) {

  34. let arr = cookieArr[i].split('=');

  35. if (arr[0] === key) {

  36. return arr[1];

  37. }

  38. }

  39. return false;

  40. }

  41. }

  42. 复制代码

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

你可能感兴趣的:(Android进阶)