Android原生同步登入狀態到H5網頁避免二次登入
本文解決的問題是目前流行的 Android/IOS 原生應用內嵌 WebView 網頁時,原生與H5頁面登入狀態的同步。
大多數混合開發應用的登入都是在原生頁面中,這就牽扯到一個問題,如何把登入狀態傳給H5頁面呢?總不能開啟網頁時再從網頁中登入一次系統吧… 兩邊登入狀態的同步是必須的。
100
多位經驗豐富的開發者參與,在 Github 上獲得了近 1000
個 star
的全棧全平臺開源專案想了解或參與嗎?
專案地址: github.com/cachecats/c…
一、同步原理
其實同步登入狀態就是把登入後伺服器返回的 token
、 userId
等登入資訊傳給H5網頁,在傳送請求時將必要的校驗資訊帶上。只不過純H5開發是自己有一個登入頁,登入之後儲存在 Cookie 或其他地方;混合開發中H5網頁自己不維護登入頁,而是由原生維護,開啟 webview 時將登入資訊傳給網頁。
實現的方法有很多,可以用原生與 JS 的通訊機制把登入資訊傳送給H5,關於原生與 JS 雙向通訊,我之前寫了一篇詳解文章,不熟悉的同學可以看看:
這裡我們用另一種更簡單的方法,通過安卓的 CookieManager
把 cookie
直接寫入 webview 中。
二、安卓端程式碼
這是安卓開發需要做的。
先說一下步驟:
- 準備一個物件
UserInfo
,用來接收服務端返回的資料。 - 登入成功後把
UserInfo
格式化為 json 字串存入SharedPreferences
中。 - 開啟 webview 時從
SharedPreferences
取出上一步儲存的UserInfo
。 - 新建一個
Map
將UserInfo
以鍵值對的格式儲存起來,便於下一步儲存為 cookie。 - 將
UserInfo
中的資訊通過CookieManager
儲存到 cookie 中。
看似步驟很多,其實就是得到服務端返回的資料,再通過 CookieManager
儲存到 cookie 中這麼簡單,只不過中間需要做幾次資料轉換。
我們按照上面的步驟一步步看程式碼。 UserInfo
物件就不貼了,都是些基本的資訊。
將 UserInfo 儲存到 SharedPreferences
登入介面請求成功後,會拿到 UserInfo
物件。在成功回撥裡通過下面一行程式碼儲存 UserInfo
到 SharedPreferences
。
//將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(); //載入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> 複製程式碼
上面兩個檔案複製過去就能用,進度條的顏色可以任意定製。
三、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
您的鼓勵是我們前行最大的動力,歡迎點贊,歡迎送小星星:sparkles: ~
