Android原生同步登錄狀態(tài)到H5網(wǎng)頁避免二次登錄

本文解決的問題是目前流行的 Android/IOS 原生應用內(nèi)嵌 WebView 網(wǎng)頁時,原生與H5頁面登錄狀態(tài)的同步。

大多數(shù)混合開發(fā)應用的登錄都是在原生頁面中,這就牽扯到一個問題,如何把登錄狀態(tài)傳給H5頁面呢?總不能打開網(wǎng)頁時再從網(wǎng)頁中登錄一次系統(tǒng)吧… 兩邊登錄狀態(tài)的同步是必須的。

100 多位經(jīng)驗豐富的開發(fā)者參與,在 Github 上獲得了近 1000star 的全棧全平臺開源項目想了解或參與嗎?
項目地址:https://github.com/cachecats/coderiver

一、同步原理

其實同步登錄狀態(tài)就是把登錄后服務器返回的 token 、userId 等登錄信息傳給H5網(wǎng)頁,在發(fā)送請求時將必要的校驗信息帶上。只不過純H5開發(fā)是自己有一個登錄頁,登錄之后保存在 Cookie 或其他地方;混合開發(fā)中H5網(wǎng)頁自己不維護登錄頁,而是由原生維護,打開 webview 時將登錄信息傳給網(wǎng)頁。

實現(xiàn)的方法有很多,可以用原生與 JS 的通信機制把登錄信息發(fā)送給H5,關于原生與 JS 雙向通信,我之前寫了一篇詳解文章,不熟悉的同學可以看看:

Android webview 與 js(Vue) 交互

這里我們用另一種更簡單的方法,通過安卓的 CookieManagercookie 直接寫入 webview 中。

二、安卓端代碼

這是安卓開發(fā)需要做的。

先說一下步驟:

  1. 準備一個對象 UserInfo ,用來接收服務端返回的數(shù)據(jù)。
  2. 登錄成功后把 UserInfo 格式化為 json 字符串存入 SharedPreferences 中。
  3. 打開 webview 時從 SharedPreferences 取出上一步保存的 UserInfo
  4. 新建一個 MapUserInfo 以鍵值對的格式保存起來,便于下一步保存為 cookie。
  5. UserInfo 中的信息通過 CookieManager 保存到 cookie 中。

看似步驟很多,其實就是得到服務端返回的數(shù)據(jù),再通過 CookieManager 保存到 cookie 中這么簡單,只不過中間需要做幾次數(shù)據(jù)轉換。

我們按照上面的步驟一步步看代碼。UserInfo 對象就不貼了,都是些基本的信息。

將 UserInfo 保存到 SharedPreferences

登錄接口請求成功后,會拿到 UserInfo 對象。在成功回調(diào)里通過下面一行代碼保存 UserInfoSharedPreferences。

//將UserData存儲到SP
SPUtils.putUserData(context, result.getData());

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);
    }

    /**
     * 獲取用戶數(shù)據(jù)
     *
     * @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 ,調(diào)用時直接打開這個 Activity 并將網(wǎng)頁的 url 地址傳入即可。在 Activity 的 onResume 生命周期方法中執(zhí)行同步 cookie 的邏輯。為什么在 onResume 中執(zhí)行?防止App 從后臺切到前臺 webview 重新加載沒有拿到 cookie,可能放在 onCreate 大多數(shù)情況下也沒有問題,但放到 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 存入的數(shù)據(jù)。

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 在執(zhí)行動畫等操作,會造成資源浪費(CPU、電量)
       // 在 onStop 和 onResume 里分別把 setJavaScriptEnabled() 給設置成 false 和 true 即可
       webSettings.setJavaScriptEnabled(true);

       //設置自適應屏幕,兩者合用
       webSettings.setUseWideViewPort(true); //將圖片調(diào)整到適合webview的大小
       webSettings.setLoadWithOverviewMode(true); // 縮放至屏幕的大小

       //縮放操作
       webSettings.setSupportZoom(true); //支持縮放,默認為true。是下面那個的前提。
       webSettings.setBuiltInZoomControls(true); //設置內(nèi)置的縮放控件。若為false,則該WebView不可縮放
       webSettings.setDisplayZoomControls(false); //隱藏原生的縮放控件

       //其他細節(jié)操作
       webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); //沒有網(wǎng)絡時加載緩存
       //webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); //關閉webview中緩存
       webSettings.setAllowFileAccess(true); //設置可以訪問文件
       webSettings.setJavaScriptCanOpenWindowsAutomatically(true); //支持通過JS打開新窗口
       webSettings.setLoadsImagesAutomatically(true); //支持自動加載圖片
       webSettings.setDefaultTextEncodingName("utf-8");//設置編碼格式

       //不加的話有些網(wǎng)頁加載不出來,是空白
       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實現(xiàn))

相比之下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);
}

把這個方法放到盡可能早的執(zhí)行到的頁面的生命周期方法中,比如 created()、mounted()、或 activated()。因為我的頁面中用到了 <keep-alive>,所以為了確保每次進來都能拿到信息,把上面的方法放到了 activated() 中。

上面用到了一個工具類 :CookieUtils,代碼如下:

主要是根據(jù)名字取出 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保質(zhì)期退回一天便可以刪除
  },

  /**
   * 獲取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;
  }
}

以上就是用最簡單的方法同步安卓原生登錄狀態(tài)到H5網(wǎng)頁中的方法。如果你有更便捷的方式,歡迎在評論區(qū)交流。


全棧全平臺開源項目 CodeRiver

CodeRiver 是一個免費的項目協(xié)作平臺,愿景是打通 IT 產(chǎn)業(yè)上下游,無論你是產(chǎn)品經(jīng)理、設計師、程序員或是測試,還是其他行業(yè)人員,只要有好的創(chuàng)意、想法,都可以來 CodeRiver 免費發(fā)布項目,召集志同道合的隊友一起將夢想變?yōu)楝F(xiàn)實!

CodeRiver 本身還是一個大型開源項目,致力于打造全棧全平臺企業(yè)級精品開源項目。涵蓋了 React、Vue、Angular、小程序、ReactNative、Android、Flutter、Java、Node 等幾乎所有主流技術棧,主打代碼質(zhì)量。

目前已經(jīng)有近 100 名優(yōu)秀開發(fā)者參與,github 上的 star 數(shù)量將近 1000 個。每個技術棧都有多位經(jīng)驗豐富的大佬坐鎮(zhèn),更有兩位架構師指導項目架構。無論你想學什么語言處于什么技術水平,相信都能在這里學有所獲。

通過 高質(zhì)量源碼 + 博客 + 視頻,幫助每一位開發(fā)者快速成長。

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


您的鼓勵是我們前行最大的動力,歡迎點贊,歡迎送小星星? ~

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容