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…


您的鼓励是咱们前行最大的动力,欢迎点赞,欢迎送小星星✨ ~

相关文章
相关标签/搜索