Android原生同步登陸狀態到H5網頁避免二次登陸

本文解決的問題是目前流行的 Android/IOS 原生應用內嵌 WebView 網頁時,原生與H5頁面登陸狀態的同步。html

大多數混合開發應用的登陸都是在原生頁面中,這就牽扯到一個問題,如何把登陸狀態傳給H5頁面呢?總不能打開網頁時再從網頁中登陸一次系統吧… 兩邊登陸狀態的同步是必須的。前端

100 多位經驗豐富的開發者參與,在 Github 上得到了近 1000star 的全棧全平臺開源項目想了解或參與嗎?
項目地址:github.com/cachecats/c…vue

1、同步原理

其實同步登陸狀態就是把登陸後服務器返回的 tokenuserId 等登陸信息傳給H5網頁,在發送請求時將必要的校驗信息帶上。只不過純H5開發是本身有一個登陸頁,登陸以後保存在 Cookie 或其餘地方;混合開發中H5網頁本身不維護登陸頁,而是由原生維護,打開 webview 時將登陸信息傳給網頁。java

實現的方法有不少,能夠用原生與 JS 的通訊機制把登陸信息發送給H5,關於原生與 JS 雙向通訊,我以前寫了一篇詳解文章,不熟悉的同窗能夠看看:android

Android webview 與 js(Vue) 交互git

這裏咱們用另外一種更簡單的方法,經過安卓的 CookieManagercookie 直接寫入 webview 中。程序員

2、安卓端代碼

這是安卓開發須要作的。github

先說一下步驟:web

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

看似步驟不少,其實就是獲得服務端返回的數據,再經過 CookieManager 保存到 cookie 中這麼簡單,只不過中間須要作幾回數據轉換。vuex

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

將 UserInfo 保存到 SharedPreferences

登陸接口請求成功後,會拿到 UserInfo 對象。在成功回調裏經過下面一行代碼保存 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);
    }

    /** * 獲取用戶數據 * * @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();
       
       //同步 cookie 到 webview
       syncCookie(url);

       //加載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 個。每一個技術棧都有多位經驗豐富的大佬坐鎮,更有兩位架構師指導項目架構。不管你想學什麼語言處於什麼技術水平,相信都能在這裏學有所獲。

經過 高質量源碼 + 博客 + 視頻,幫助每一位開發者快速成長。

項目地址:github.com/cachecats/c…


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

相關文章
相關標籤/搜索