Xamarin.Forms webview 进行身份验证,导航到新页面(Shell)并需要再次重新验证?

Posted

技术标签:

【中文标题】Xamarin.Forms webview 进行身份验证,导航到新页面(Shell)并需要再次重新验证?【英文标题】:Xamarin.Forms webview authenticate, navigate to new page (Shell) and need to re-auth again? 【发布时间】:2020-02-28 12:05:26 【问题描述】:

我在 Shell Xamarin.Forms 应用中有一个 webview。我请求一个安全页面并被转发到我的公司 SSO(单点登录),我通过了该页面并可以看到安全内容。

网络视图:

<WebView x:Name="web1" HorizontalOptions="CenterAndExpand"
                 VerticalOptions="FillAndExpand" HeightRequest="1000" WidthRequest="1000"/>

然后,当我从主菜单导航到新的 Shell 页面或通过点击列表视图中的项目(认为 RSS 标题列表,点击阅读文章)时,该页面具有几乎相同的 WebView 标签xaml 页面,将 Source 设置为页面构造函数上的安全页面或覆盖 OnAppearing,期望会话/cookie 仍然处于活动状态,我改为再次转发到登录页面。

有没有人知道我可以确保我的应用程序(iosandroid)中的所有 webviews 使用相同的会话,这样用户只需登录一次。

我尝试在 app.xaml.cs 文件中创建 webview 并使用 Content.Children.Add(App.Web1) 将其添加到我的页面中,这意味着我在所有页面上都使用相同的 webview,因此相同会议!?但这似乎也不起作用。

非常感谢任何和所有帮助。

谢谢。

【问题讨论】:

您可以尝试通过在本机项目中创建 webview 的自定义渲染器来设置会话/cookie。有关详细信息,请参阅这些线程:Xamarin WebView Cookie to seamlessly authenticate user without re-entering credentials 和 Setting Cookies in a WebView。 试过这个。在我删除新 web 视图中的 cookie 之前,我已经查看了它们,它们是已经存在的 auth cookie! (仍然不起作用) - 然后我删除它们,从过去成功的呼叫中添加新的,我仍然被转发到登录服务。 【参考方案1】:

使用cookieJs配合普通的cookie插入方式可以解决前端设置cookie的问题:

CookieJs:

!function(e)var n;if("function"==typeof define&&define.amd&&(define(e),n=!0),"object"==typeof exports&&(module.exports=e(),n=!0),!n)var t=window.Cookies,o=window.Cookies=e();o.noConflict=function()return window.Cookies=t,o(function()function e()for(var e=0,n=;e<arguments.length;e++)var t=arguments[e];for(var o in t)n[o]=t[o]return nfunction n(e)return e.replace(/(%[0-9A-Z]2)+/g,decodeURIComponent)return function t(o)function r()function i(n,t,i)if("undefined"!=typeof document)"number"==typeof(i=e(path:"/",r.defaults,i)).expires&&(i.expires=new Date(1*new Date+864e5*i.expires)),i.expires=i.expires?i.expires.toUTCString():"";tryvar c=JSON.stringify(t);/^[[]/.test(c)&&(t=c)catch(e)t=o.write?o.write(t,n):encodeURIComponent(String(t)).replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),n=encodeURIComponent(String(n)).replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape);var f="";for(var u in i)i[u]&&(f+="; "+u,!0!==i[u]&&(f+="="+i[u].split(";")[0]));return document.cookie=n+"="+t+ffunction c(e,t)if("undefined"!=typeof document)for(var r=,i=document.cookie?document.cookie.split("; "):[],c=0;c<i.length;c++)var f=i[c].split("="),u=f.slice(1).join("=");t||'"'!==u.charAt(0)||(u=u.slice(1,-1));tryvar a=n(f[0]);if(u=(o.read||o)(u,a)||n(u),t)tryu=JSON.parse(u)catch(e)if(r[a]=u,e===a)breakcatch(e)return e?r[e]:rreturn r.set=i,r.get=function(e)return c(e,!1),r.getJSON=function(e)return c(e,!0),r.remove=function(n,t)i(n,"",e(t,expires:-1)),r.defaults=,r.withConverter=t,r(function()));

示例:

iOS 渲染器:

public class DefaultWebViewRenderer : ViewRenderer<CustomWebView, WKWebView>, IWKScriptMessageHandler, IWKNavigationDelegate
    
        const string javascriptFunction = "function invokeCSharpAction(data)window.webkit.messageHandlers.invokeAction.postMessage(data);";        
        const string cookieJs = "!function(e)var n;if(\"function\"==typeof define&&define.amd&&(define(e),n=!0),\"object\"==typeof exports&&(module.exports=e(),n=!0),!n)var t=window.Cookies,o=window.Cookies=e();o.noConflict=function()return window.Cookies=t,o(function()function e()for(var e=0,n=;e<arguments.length;e++)var t=arguments[e];for(var o in t)n[o]=t[o]return nfunction n(e)return e.replace(/(%[0-9A-Z]2)+/g,decodeURIComponent)return function t(o)function r()function i(n,t,i)if(\"undefined\"!=typeof document)\"number\"==typeof(i=e(path:\"/\",r.defaults,i)).expires&&(i.expires=new Date(1*new Date+864e5*i.expires)),i.expires=i.expires?i.expires.toUTCString():\"\";tryvar c=JSON.stringify(t);/^[\\\\[]/.test(c)&&(t=c)catch(e)t=o.write?o.write(t,n):encodeURIComponent(String(t)).replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),n=encodeURIComponent(String(n)).replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent).replace(/[\\(\\)]/g,escape);var f=\"\";for(var u in i)i[u]&&(f+=\"; \"+u,!0!==i[u]&&(f+=\"=\"+i[u].split(\";\")[0]));return document.cookie=n+\"=\"+t+ffunction c(e,t)if(\"undefined\"!=typeof document)for(var r=,i=document.cookie?document.cookie.split(\"; \"):[],c=0;c<i.length;c++)var f=i[c].split(\"=\"),u=f.slice(1).join(\"=\");t||'\"'!==u.charAt(0)||(u=u.slice(1,-1));tryvar a=n(f[0]);if(u=(o.read||o)(u,a)||n(u),t)tryu=JSON.parse(u)catch(e)if(r[a]=u,e===a)breakcatch(e)return e?r[e]:rreturn r.set=i,r.get=function(e)return c(e,!1),r.getJSON=function(e)return c(e,!0),r.remove=function(n,t)i(n,\"\",e(t,expires:-1)),r.defaults=,r.withConverter=t,r(function()));";
        WKUserContentController userController;
        public DefaultWebViewRenderer()
        

        
        protected override void OnElementChanged(ElementChangedEventArgs<CustomWebView> e)
        
            base.OnElementChanged(e);
            if (Control == null && e.OldElement == null)
            
                userController = new WKUserContentController();
                var cookieValue = string.Empty; //Whatever you want
                var jsScript = new WKUserScript(new NSString(cookieJs), WKUserScriptInjectionTime.AtDocumentStart, false);
                var yourCookie = new WKUserScript(new NSString("Cookies.set('CookieKey','" + cookieValue + "',expires : 30, domain : '.yourdomain.com' )"), WKUserScriptInjectionTime.AtDocumentStart, false);                  
                userController.AddUserScript(jsScript);
                userController.AddUserScript(yourCookie);                    
                var script = new WKUserScript(new NSString(JavaScriptFunction), WKUserScriptInjectionTime.AtDocumentEnd, false);
                userController.AddUserScript(script);
                userController.AddScriptMessageHandler(this, "invokeAction");
                var config = new WKWebViewConfiguration  UserContentController = userController ;
                var webView = new WKWebView(Frame, config);

                SetNativeControl(webView);
            
            if (e.OldElement != null)
            
                userController.RemoveAllUserScripts();
                userController.RemoveScriptMessageHandler("invokeAction");
            
            if (e.NewElement != null)
            
                if (Element.Source is UrlWebViewSource urlSource)
                
                    var url = new NSUrl(urlSource.Url);
                    var storage = NSHttpCookieStorage.SharedStorage;
                    storage.AcceptPolicy = NSHttpCookieAcceptPolicy.Always;
                    var request = new NSMutableUrlRequest(url);
                    Control.LoadRequest(request);
                
                else if (Element.Source is htmlWebViewSource htmlSource)
                
                    Control.LoadHtmlString(htmlSource.Html, null);
                
            
        

        public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
        
            Element.JavascriptBridgeInvoked(message.Body.ToString());
        
    

Android 渲染器:

public class DefaultWebViewRenderer : WebViewRenderer

    const string JavaScriptFunction = "function invokeCSharpAction(data)jsBridge.invokeAction(data);";
    public DefaultWebViewRenderer(Context context) : base(context)
    
    
    protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
    
        base.OnElementChanged(e);
        if (Control != null && e.NewElement is CustomWebView webView)
        
            Control.SetWebViewClient(new WebViewTestClient());
            Control.ClearSslPreferences();
            Control.Settings.SetAppCacheEnabled(false); 
            Control.Settings.DatabaseEnabled = true;
            Control.Settings.DomStorageEnabled = true;
            Control.Settings.AllowFileAccessFromFileURLs = true;
            Control.Settings.AllowUniversalAccessFromFileURLs = true;
            Control.Settings.CacheMode = Android.Webkit.CacheModes.NoCache;                
            Control.Settings.AllowContentAccess = true;
            Control.Settings.AllowFileAccess = true;
            Control.Settings.JavaScriptEnabled = true;
            Control.Settings.JavaScriptCanOpenWindowsAutomatically = true;
            var cookieManager = CookieManager.Instance;
            cookieManager.SetAcceptCookie(true);
            cookieManager.SetAcceptThirdPartyCookies(Control, true);
            try
            
                Control.SetDownloadListener(new DownloadListener());
            
            catch (Exception ex)
            
                Console.WriteLine(ex);
            
            if (e.OldElement != null)
            
                Control.RemoveJavascriptInterface("jsBridge");
            
            Control.AddJavascriptInterface(new JSBridge(this), "jsBridge");
            InjectJS(JavaScriptFunction);
            if (webView.Source is UrlWebViewSource webSource)
            
                Control.LoadUrl(webSource.Url);
            
        
    

    private void InjectJS(string script)
    
        if (Control != null)
        
            Control.LoadUrl(string.Format("javascript: 0", script));
        
    

public class WebViewTestClient : WebViewClient

    public override void OnPageStarted(Android.Webkit.WebView view, string url, Bitmap favicon)
    
         var cookieJs = "!function(e)var n;if(\"function\"==typeof define&&define.amd&&(define(e),n=!0),\"object\"==typeof exports&&(module.exports=e(),n=!0),!n)var t=window.Cookies,o=window.Cookies=e();o.noConflict=function()return window.Cookies=t,o(function()function e()for(var e=0,n=;e<arguments.length;e++)var t=arguments[e];for(var o in t)n[o]=t[o]return nfunction n(e)return e.replace(/(%[0-9A-Z]2)+/g,decodeURIComponent)return function t(o)function r()function i(n,t,i)if(\"undefined\"!=typeof document)\"number\"==typeof(i=e(path:\"/\",r.defaults,i)).expires&&(i.expires=new Date(1*new Date+864e5*i.expires)),i.expires=i.expires?i.expires.toUTCString():\"\";tryvar c=JSON.stringify(t);/^[\\\\[]/.test(c)&&(t=c)catch(e)t=o.write?o.write(t,n):encodeURIComponent(String(t)).replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),n=encodeURIComponent(String(n)).replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent).replace(/[\\(\\)]/g,escape);var f=\"\";for(var u in i)i[u]&&(f+=\"; \"+u,!0!==i[u]&&(f+=\"=\"+i[u].split(\";\")[0]));return document.cookie=n+\"=\"+t+ffunction c(e,t)if(\"undefined\"!=typeof document)for(var r=,i=document.cookie?document.cookie.split(\"; \"):[],c=0;c<i.length;c++)var f=i[c].split(\"=\"),u=f.slice(1).join(\"=\");t||'\"'!==u.charAt(0)||(u=u.slice(1,-1));tryvar a=n(f[0]);if(u=(o.read||o)(u,a)||n(u),t)tryu=JSON.parse(u)catch(e)if(r[a]=u,e===a)breakcatch(e)return e?r[e]:rreturn r.set=i,r.get=function(e)return c(e,!1),r.getJSON=function(e)return c(e,!0),r.remove=function(n,t)i(n,\"\",e(t,expires:-1)),r.defaults=,r.withConverter=t,r(function()));";
        var cookieValue = string.Empty; //Whatever you want
        view.EvaluateJavascript(cookieJs, null);
        view.EvaluateJavascript("Cookies.set('CookieKey','" + cookieValue + "',expires : 30, domain : '.yourdomain.com' )", null);
        base.OnPageStarted(view, url, favicon);
        view.ClearCache(true);
    
    public override void OnPageFinished(Android.Webkit.WebView view, string url)
    
        base.OnPageFinished(view, url);
        view.ClearCache(true);
    
    public override void OnReceivedSslError(Android.Webkit.WebView view, SslErrorHandler handler, SslError error)
    
        handler.Proceed();
        base.OnReceivedSslError(view, handler, error);
    

public class DownloadListener : Java.Lang.Object, Android.Webkit.IDownloadListener

    public DownloadListener()
    
    
    public void OnDownloadStart(string url, string userAgent, string contentDisposition, string mimetype, long contentLength)
    
        if (string.IsNullOrEmpty(url) || !Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out _))
            return;
        try
        
            if (HasPermissions())
            
                Android.Net.Uri contentUri = Android.Net.Uri.Parse(url);
                DownloadManager.Request request = new DownloadManager.Request(contentUri);
                request.SetMimeType(mimetype);
                var cookies = Android.Webkit.CookieManager.Instance.GetCookie(url);
                request.AddRequestHeader("cookie", cookies);
                request.AddRequestHeader("User-Agent", userAgent);
                request.SetDescription("Downloading file...");
                request.SetTitle(Android.Webkit.URLUtil.GuessFileName(url, contentDisposition, mimetype));
                request.AllowScanningByMediaScanner();
                request.SetNotificationVisibility(Android.App.DownloadVisibility.VisibleNotifyCompleted);
                request.SetDestinationInExternalPublicDir(Android.OS.Environment.DirectoryDownloads, ".pdf");
                Android.App.DownloadManager dm = (Android.App.DownloadManager)CrossCurrentActivity.Current.Activity.GetSystemService(Android.Content.Context.DownloadService);
                dm.Enqueue(request);
            
        
        catch (Exception ex)
        
            Console.WriteLine(ex);
        
    
    private static bool HasPermissions()
    
        if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.M)
        
            string[] WriteLocation =
            
                Android.Manifest.Permission.WriteExternalStorage,
                Android.Manifest.Permission.ReadExternalStorage
            ;
            var perm = CrossCurrentActivity.Current.AppContext.CheckSelfPermission(Android.Manifest.Permission.WriteExternalStorage);
            if (perm != (int)Android.Content.PM.Permission.Granted)
            
                CrossCurrentActivity.Current.Activity.RequestPermissions(WriteLocation, 2);
                return false;
            
            else
            
                return true;
            
        
        else
        
            return true;
        
    

public class JSBridge : Java.Lang.Object

    private readonly WeakReference<DefaultWebViewRenderer> hybridWebViewRenderer;
    public JSBridge(DefaultWebViewRenderer hybridRenderer)
    
        hybridWebViewRenderer = new WeakReference<DefaultWebViewRenderer>(hybridRenderer);
    
    [JavascriptInterface]
    [Export("invokeAction")]
    public void InvokeAction(string data)
    
        if (hybridWebViewRenderer != null && hybridWebViewRenderer.TryGetTarget(out DefaultWebViewRenderer hybridRenderer) && hybridRenderer != null && hybridRenderer.Element is CustomWebView webView)
        
            webView.JavascriptBridgeInvoked(data);
        
    

【讨论】:

我不能在前端设置设置 HttpOnly cookie 吗?此外,我认为这可能归结为 Xamarin.Forms 4.5 更新,该更新删除了 WebViewRenderer 并替换为 WKWebViewRenderer...值得研究。

以上是关于Xamarin.Forms webview 进行身份验证,导航到新页面(Shell)并需要再次重新验证?的主要内容,如果未能解决你的问题,请参考以下文章

Xamarin.Forms webview 进行身份验证,导航到新页面(Shell)并需要再次重新验证?

Xamarin Forms:捏缩放和滚动不适用于 WebView 内容

在 Xamarin.Forms PCL 中使用自定义 WebView 渲染器处理超链接

XAML WebView绑定到不在Xamarin Forms中的字符串

Xamarin.Forms (UWP) - 如何获取 WebView 的 DOM 作为 HTML 字符串?

Xamarin.Forms自定义用户界面控件实现一个HybridWebView(混合webview)