zoukankan      html  css  js  c++  java
  • Android原生同步登录状态到H5网页避免二次登录

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

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

    100 多位经验丰富的开发者参与,在 Github 上获得了近 1000 个 star 的全栈全平台开源项目想了解或参与吗?
    项目地址:https://github.com/cachecats/coderiver

    一、同步原理

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

    //将UserData存储到SP
    SPUtils.putUserData(context, result.getData());
    
    • 1
    • 2

    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


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

    在这里插入图片描述

  • 相关阅读:
    Scheduler踩坑记录
    关于RedisTemplate的map存储踩坑记录
    关于HashMap的加载因子相关理解
    Mybatis 分页插件PageHelper 遇坑
    Linux 下 Mysql忘记密码重置
    Eclipse MAT和jvisualvm分析内存溢出
    使用jdk自带工具jvisualvm 分析内存dump文件
    EUREKA 删除 or 强制下线/上线 实例
    Idea 远程调试jenkins 项目
    spring 事务传播行为类型
  • 原文地址:https://www.cnblogs.com/yelanggu/p/10980084.html
Copyright © 2011-2022 走看看