首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Android原生同步登录状态到H5网页避免二次登录

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

作者头像
solocoder
发布于 2022-04-06 13:33:34
发布于 2022-04-06 13:33:34
2.3K0
举报
文章被收录于专栏:大前端客栈大前端客栈

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

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

一、同步原理

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

代码语言:javascript
复制
//将UserData存储到SP
SPUtils.putUserData(context, result.getData());

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

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

代码语言:javascript
复制
public class SPUtils {
    /**
     * 保存在手机里面的文件名
     */
    public static final String FILE_NAME = "share_data";
    
<span class="hljs-comment">/**
 * 存储用户信息
 *
 * <span class="hljs-doctag">@param</span> context
 * <span class="hljs-doctag">@param</span> userData
 */</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">putUserData</span><span class="hljs-params">(Context context, UserData userData)</span> </span>{
    SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
            Context.MODE_PRIVATE);
    SharedPreferences.Editor editor = sp.edit();

    Gson gson = <span class="hljs-keyword">new</span> Gson();
    String json = gson.toJson(userData, UserData.class);
    editor.putString(SPConstants.USER_DATA, json);
    SharedPreferencesCompat.apply(editor);
}

<span class="hljs-comment">/**
 * 获取用户数据
 *
 * <span class="hljs-doctag">@param</span> context
 * <span class="hljs-doctag">@return</span>
 */</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> UserData <span class="hljs-title">getUserData</span><span class="hljs-params">(Context context)</span> </span>{
    SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
            Context.MODE_PRIVATE);
    String json = sp.getString(SPConstants.USER_DATA, <span class="hljs-string">""</span>);
    Gson gson = <span class="hljs-keyword">new</span> Gson();
    UserData userData = gson.fromJson(json, UserData.class);
    <span class="hljs-keyword">return</span> userData;
}

}

取出 UserInfo 并保存到 cookie 中

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

代码语言:javascript
复制
@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。

代码语言:javascript
复制
public class CookieUtils {

<span class="hljs-comment">/**
 * 将cookie同步到WebView
 *
 * <span class="hljs-doctag">@param</span> url WebView要加载的url
 * <span class="hljs-doctag">@return</span> true 同步cookie成功,false同步cookie失败
 * <span class="hljs-doctag">@Author</span> JPH
 */</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">syncCookie</span><span class="hljs-params">(String url)</span> </span>{
    <span class="hljs-keyword">if</span> (Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.LOLLIPOP) {
        CookieSyncManager.createInstance(MyApplication.getAppContext());
    }
    CookieManager cookieManager = CookieManager.getInstance();

    Map&lt;String, String&gt; cookieMap = getCookieMap();
    <span class="hljs-keyword">for</span> (Map.Entry&lt;String, String&gt; entry : cookieMap.entrySet()) {
        String cookieStr = makeCookie(entry.getKey(), entry.getValue());
        cookieManager.setCookie(url, cookieStr);
    }
    String newCookie = cookieManager.getCookie(url);
    <span class="hljs-keyword">return</span> TextUtils.isEmpty(newCookie) ? <span class="hljs-keyword">false</span> : <span class="hljs-keyword">true</span>;
}

<span class="hljs-comment">/**
 * 组装 Cookie 里需要的值
 *
 * <span class="hljs-doctag">@return</span>
 */</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Map&lt;String, String&gt; <span class="hljs-title">getCookieMap</span><span class="hljs-params">()</span> </span>{

    UserData userData = SPUtils.getUserData(MyApplication.getAppContext());
    String accessToken = userData.getAccessToken();
    Map&lt;String, String&gt; headerMap = <span class="hljs-keyword">new</span> HashMap&lt;&gt;();
    headerMap.put(<span class="hljs-string">"access_token"</span>, accessToken);
    headerMap.put(<span class="hljs-string">"login_name"</span>, userData.getLoginName());
    headerMap.put(<span class="hljs-string">"refresh_token"</span>, userData.getRefreshToken());
    headerMap.put(<span class="hljs-string">"remove_token"</span>, userData.getRemoveToken());
    headerMap.put(<span class="hljs-string">"unitId"</span>, userData.getUnitId());
    headerMap.put(<span class="hljs-string">"unitType"</span>, userData.getUnitType() + <span class="hljs-string">""</span>);
    headerMap.put(<span class="hljs-string">"userId"</span>, userData.getUserId());

    <span class="hljs-keyword">return</span> headerMap;
}

<span class="hljs-comment">/**
 * 拼接 Cookie 字符串
 *
 * <span class="hljs-doctag">@param</span> key
 * <span class="hljs-doctag">@param</span> value
 * <span class="hljs-doctag">@return</span>
 */</span>
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> String <span class="hljs-title">makeCookie</span><span class="hljs-params">(String key, String value)</span> </span>{
    Date date = <span class="hljs-keyword">new</span> Date();
    date.setTime(date.getTime() + <span class="hljs-number">3</span> * <span class="hljs-number">24</span> * <span class="hljs-number">60</span> * <span class="hljs-number">60</span> * <span class="hljs-number">1000</span>);  <span class="hljs-comment">//3天过期</span>
    <span class="hljs-keyword">return</span> key + <span class="hljs-string">"="</span> + value + <span class="hljs-string">";expires="</span> + date + <span class="hljs-string">";path=/"</span>;
}

}

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

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

ProgressWebviewActivity封装

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

代码语言:javascript
复制
/**
* 带进度条的 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);
   <span class="hljs-comment">//对WebView进行必要配置</span>
   settingWebView();
   settingWebViewClient();
   
   <span class="hljs-comment">//同步 cookie 到 webview</span>
   syncCookie(url);

   <span class="hljs-comment">//加载url地址</span>
   mWebView.loadUrl(url);

   }
   /**
    * 对 webview 进行必要的配置
    */
   private void settingWebView() {
       webSettings = mWebView.getSettings();
       //如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript
       // 若加载的 html 里有JS 在执行动画等操作,会造成资源浪费(CPU、电量)
       // 在 onStop 和 onResume 里分别把 setJavaScriptEnabled() 给设置成 false 和 true 即可
       webSettings.setJavaScriptEnabled(true);
   <span class="hljs-comment">//设置自适应屏幕,两者合用</span>
   webSettings.setUseWideViewPort(<span class="hljs-keyword">true</span>); <span class="hljs-comment">//将图片调整到适合webview的大小</span>
   webSettings.setLoadWithOverviewMode(<span class="hljs-keyword">true</span>); <span class="hljs-comment">// 缩放至屏幕的大小</span>

   <span class="hljs-comment">//缩放操作</span>
   webSettings.setSupportZoom(<span class="hljs-keyword">true</span>); <span class="hljs-comment">//支持缩放,默认为true。是下面那个的前提。</span>
   webSettings.setBuiltInZoomControls(<span class="hljs-keyword">true</span>); <span class="hljs-comment">//设置内置的缩放控件。若为false,则该WebView不可缩放</span>
   webSettings.setDisplayZoomControls(<span class="hljs-keyword">false</span>); <span class="hljs-comment">//隐藏原生的缩放控件</span>

   <span class="hljs-comment">//其他细节操作</span>
   webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); <span class="hljs-comment">//没有网络时加载缓存</span>
   <span class="hljs-comment">//webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); //关闭webview中缓存</span>
   webSettings.setAllowFileAccess(<span class="hljs-keyword">true</span>); <span class="hljs-comment">//设置可以访问文件</span>
   webSettings.setJavaScriptCanOpenWindowsAutomatically(<span class="hljs-keyword">true</span>); <span class="hljs-comment">//支持通过JS打开新窗口</span>
   webSettings.setLoadsImagesAutomatically(<span class="hljs-keyword">true</span>); <span class="hljs-comment">//支持自动加载图片</span>
   webSettings.setDefaultTextEncodingName(<span class="hljs-string">"utf-8"</span>);<span class="hljs-comment">//设置编码格式</span>

   <span class="hljs-comment">//不加的话有些网页加载不出来,是空白</span>
   webSettings.setDomStorageEnabled(<span class="hljs-keyword">true</span>);

   <span class="hljs-comment">//Android 5.0及以上版本使用WebView不能存储第三方Cookies解决方案</span>
   <span class="hljs-keyword">if</span> (android.os.Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.LOLLIPOP) {
       CookieManager.getInstance().setAcceptThirdPartyCookies(mWebView, <span class="hljs-keyword">true</span>);
       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");
           }
       <span class="hljs-meta">@Override</span>
       <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onPageFinished</span><span class="hljs-params">(WebView view, String url)</span> </span>{
           <span class="hljs-keyword">super</span>.onPageFinished(view, url);
           Logger.d(<span class="hljs-string">"onPageFinished"</span>);
       }

       <span class="hljs-comment">// 链接跳转都会走这个方法</span>
       <span class="hljs-meta">@Override</span>
       <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">shouldOverrideUrlLoading</span><span class="hljs-params">(WebView view, String url)</span> </span>{
           Logger.d(<span class="hljs-string">"url: "</span>, url);
           view.loadUrl(url);<span class="hljs-comment">// 强制在当前 WebView 中加载 url</span>
           <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
       }

       <span class="hljs-meta">@Override</span>
       <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onReceivedSslError</span><span class="hljs-params">(WebView view, SslErrorHandler handler, SslError error)</span> </span>{
           handler.proceed();
           <span class="hljs-keyword">super</span>.onReceivedSslError(view, handler, error);
       }
   });

   mWebView.setWebChromeClient(<span class="hljs-keyword">new</span> WebChromeClient() {
       <span class="hljs-meta">@Override</span>
       <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onProgressChanged</span><span class="hljs-params">(WebView view, <span class="hljs-keyword">int</span> newProgress)</span> </span>{
           <span class="hljs-keyword">super</span>.onProgressChanged(view, newProgress);
           Logger.d(<span class="hljs-string">"current progress: "</span> + newProgress);
           <span class="hljs-comment">//更新进度条</span>
           web_bar.setProgress(newProgress);

           <span class="hljs-keyword">if</span> (newProgress == <span class="hljs-number">100</span>) {
               web_bar.setVisibility(View.GONE);
           } <span class="hljs-keyword">else</span> {
               web_bar.setVisibility(View.VISIBLE);
           }
       }

       <span class="hljs-meta">@Override</span>
       <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onReceivedTitle</span><span class="hljs-params">(WebView view, String title)</span> </span>{
           <span class="hljs-keyword">super</span>.onReceivedTitle(view, title);
           Logger.d(<span class="hljs-string">"标题:"</span> + 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 的布局文件:

代码语言:javascript
复制
<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 啦。

代码语言:javascript
复制
//从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 中对应的值。

代码语言:javascript
复制
/**
 * 操作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网页中的方法。如果你有更便捷的方式,欢迎在评论区交流。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019/03/14 ,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 一、同步原理
  • 二、安卓端代码
    • 将 UserInfo 保存到 SharedPreferences
    • 取出 UserInfo 并保存到 cookie 中
    • ProgressWebviewActivity封装
  • 三、H5端代码(Vue实现)
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档