あるSEのつぶやき・改

ITやシステム開発などの技術に関する話題を、取り上げたりしています。

Xamarin.FormsのiOSとAndroidでブラウザのCookieを取得する方法

はじめに

Xamarin.Forms で、内部ブラウザ(WebView)で表示したサイトで Cookie をセットし、その値を画面に表示する方法を調べてみました。

サンプルアプリ

以下の画面遷移を行う iOS と Android のサンプルアプリを作ってみました。

  • 初期画面
    • Cookie がセットされたらこの画面で表示する
  • WebView 画面
    • Cookie を保存する

開発環境は、Visual Studio Community 2017 になります。

アプリ概要

内部ブラウザで設定された Cookie は、Android はAndroid.WebKit.CookieManager、iOS はNSHttpCookieStorage.SharedStorage.Cookies を使用することで、Cookie の値を取得することができます。

また、Android と iOS では実装が異なるので、DependencyService という仕組みを使用することで Xamarin.Forms で動作するようにしています。

実装

Web サイト

内部ブラウザで表示する Web サイトのソースは以下のようになります。

<html>
<body>
<h1>Cookie Test</h1>

<input type="button" onclick="document.cookie='key1=value1;expires=' + (new Date('2018/12/31 00:00:00')).toString();" value="Set Cookie">
<br /><br />
<input type="button" onclick="alert(document.cookie);" value="Get Cookie">

</body>
</html>

単純に「Set Cookie」ボタンで Cookie を保存し、「Get Cookie」ボタンで Cookie の値を表示するだけです。

Xamarin.Forms プロジェクト

画面遷移を行うために、App.xaml.csを以下のように修正します。

        public App()
        {
            InitializeComponent();

            //ナビゲーションするように設定
            MainPage = new NavigationPage(new MainPage());
        }

DependencyService は iOS と Android で共通のインターフェースを使用することで、処理を1つにまとめることができます。

参考サイトでは、Splat という DI コンテナを使用していましたが複雑になるため、サンプルアプリでは DependencyService を使用しています。

共通のインターフェースは、IPlatformCookieStore.cs に記述します。

using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Http;

namespace CookieAccess
{
    public interface IPlatformCookieStore
    {
        IEnumerable<Cookie> CurrentCookies { get; }
    }
}

MainPage.xaml は以下のようになります。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:CookieAccess"
             x:Class="CookieAccess.MainPage">

    <StackLayout HorizontalOptions="Center" VerticalOptions="Center">

        <Label x:Name="label"  Text="Welcome to Xamarin.Forms!" />

        <Button x:Name="buttonNavi" Clicked="buttonNavi_Clicked" Text="Move to webpage" />
        
        <Button x:Name="buttonCookie" Clicked="buttonCookie_Clicked" Text="Show cookies" />
    </StackLayout>

</ContentPage>

コードビハインドのMainPage.xaml.csは、以下のようになります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace CookieAccess
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
            
        }

        private void buttonCookie_Clicked(object sender, EventArgs e)
        {
            //DependencyServiceを使用して、iOS/Androidで異なるコードを実行しCookieを取得する
            var cookies = DependencyService.Get<IPlatformCookieStore>().CurrentCookies;

            StringBuilder buf = new StringBuilder();

            foreach (var cookie in cookies)
            {
                buf.Append(cookie.Name + "=" + cookie.Value + ",");
            }

            //Cookieの値を表示する
            label.Text = buf.ToString();
        }

        private void buttonNavi_Clicked(object sender, EventArgs e)
        {
            //ページ遷移する
            Navigation.PushAsync(new WebPage());
        }
    }
}

Web サイトを表示する WebPage.xamlは以下のようになります。www.example.comをご自分のドメインに書き換えてください。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="CookieAccess.WebPage">
    <ContentPage.Padding>
        <!--iOS用にパディングを確保-->
        <OnPlatform x:TypeArguments="Thickness">
            <On Platform="iOS" Value="0, 10, 10, 0" />
        </OnPlatform>
    </ContentPage.Padding>
    <ContentPage.Content>
        <StackLayout>
            <WebView Source="https://www.example.com"  
                     VerticalOptions="FillAndExpand" 
                     HorizontalOptions="FillAndExpand"/>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

Xamarin.Android プロジェクト

Xamarin.Android プロジェクトでも、IPlatformCookieStore インターフェースを実装したクラスを作成します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;

using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Android.Webkit;

//Dependency属性
[assembly: Xamarin.Forms.Dependency(typeof(CookieAccess.Droid.DroidCookieStore))]
namespace CookieAccess.Droid
{
    class DroidCookieStore : IPlatformCookieStore
    {
        private readonly string _url = "https://www.example.com";
        private readonly object _refreshLock = new object();

        public IEnumerable<Cookie> CurrentCookies
        {
            get { return RefreshCookies(); }
        }


        private IEnumerable<Cookie> RefreshCookies()
        {

            lock (_refreshLock)
            {
                // .GetCookie returns ALL cookies related to the URL as a single, long
                // string which we have to split and parse
                var allCookiesForUrl = CookieManager.Instance.GetCookie(_url);

                if (string.IsNullOrWhiteSpace(allCookiesForUrl))
                {
                    Console.WriteLine(string.Format("No cookies found for '{0}'. Exiting.", _url));
                    yield return new Cookie("none", "none");
                }
                else
                {
                    Console.WriteLine(string.Format("\r\n===== CookieHeader : '{0}'\r\n", allCookiesForUrl));

                    var cookiePairs = allCookiesForUrl.Split(' ');
                    foreach (var cookiePair in cookiePairs.Where(cp => cp.Contains("=")))
                    {
                        // yeah, I know, but this is a quick-and-dirty, remember? ;)
                        var cookiePieces = cookiePair.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
                        if (cookiePieces.Length >= 2)
                        {
                            cookiePieces[0] = cookiePieces[0].Contains(":")
                              ? cookiePieces[0].Substring(0, cookiePieces[0].IndexOf(":"))
                              : cookiePieces[0];

                            // strip off trailing ';' if it's there (some implementations
                            // on droid have it, some do not)
                            cookiePieces[1] = cookiePieces[1].EndsWith(";")
                              ? cookiePieces[1].Substring(0, cookiePieces[1].Length - 1)
                              : cookiePieces[1];

                            yield return new Cookie()
                            {
                                Name = cookiePieces[0],
                                Value = cookiePieces[1]
                            };
                        }
                    }
                }
            }
        }
    }
}

Xamarin.iOS プロジェクト

Xamarin.iOS プロジェクトでは、IPlatformCookieStore インターフェースを実装したクラスを作成します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;

using Foundation;
using UIKit;

//Dependency属性
[assembly: Xamarin.Forms.Dependency(typeof(CookieAccess.iOS.IOSCookieStore))]
namespace CookieAccess.iOS
{
    class IOSCookieStore : IPlatformCookieStore
    {
        private readonly object _refreshLock = new object();

        public IEnumerable<Cookie> CurrentCookies
        {
            get { return RefreshCookies(); }
        }


        private IEnumerable<Cookie> RefreshCookies()
        {

            lock (_refreshLock)
            {
                foreach (var cookie in NSHttpCookieStorage.SharedStorage.Cookies)
                {
                    yield return new Cookie
                    {
                        Name = cookie.Name,
                        Value = cookie.Value,
                    };
                }
            }
        }
    }
}

動作確認

Android

初期表示の画面になります。

f:id:fnyablog:20181016220013p:plain:w320

Web サイトで Cookie を設定します。

f:id:fnyablog:20181016220124p:plain:w320

画面に Cookie の値が正しく表示されました。

f:id:fnyablog:20181016220200p:plain:w320

iOS (iPhone)

初期表示の画面になります。

f:id:fnyablog:20181016220340j:plain:w320

Web サイトで Cookie を設定します。

f:id:fnyablog:20181016220400j:plain:w320

画面に Cookie の値が正しく表示されました。

f:id:fnyablog:20181016220420j:plain:w320

おわりに

内部ブラウザ(WebView)で設定した Cookie の値を、Xamarin.Forms の iOS と Android で取得する方法を見てきました。

これにより、OAuth などの認証結果を Cookie に保存して、Xamarin.Forms で活用することができるようになりますね。

最初は、内部ブラウザではなく、iOS なら Safari、Android なら Chrome といった外部ブラウザで値を取得しようとしたのですが、うまくいきませんでした。

セキュリティ的には外部ブラウザの方法が勧められていますが、Cookie では値の受け渡しができないようです。

外部ブラウザとの値の受け渡しは今後の課題としたいと思います。

参考サイト