C# 自動ログインの実装　HttpClient

#はじめに#

以前にWebDriverを使ってChromeを使って自動ログインの実装を試したのですが少しテンポが悪い。
そこでMicrosoftのHttpClientを使っていろいろと情報を探していたがHTMLの基礎知識が足りず、またHttpClient自体も最新のHTML状況を加味してバージョンアップというかどんどん高機能化がすすみ以前の使い方を参照しても役に立たなかっためこちらで紹介します。


＜主な処理＞
**step1**
ログインは、**POST要求**という処理でformタグ内のinputタブの値を取り出してサーバに送りログイン処理をおこなうためPOST要求を送る処理とそのときのクッキー情報（クライアントを認識するためのバッジのようなもの）を
取得する。
（修正）
はじめhandrerを使ってUseCookieをONにしていたが既定値がtrueのため省略できた。
また、リダイレクトの機能をよく理解していなかったのでPostAsyncを２回使っていたが、必要ないと分かったので削除。

**step2**
ページ情報がほしいＵＲＬへリクエスト送信。

(ログインの継続)
一度のログイン情報はHttpClientがnewされるまで継続するため、一度ログインしてしまえば、同じHttpClientを使っている限り途中で処理が途切れることはありません。


**メインの処理**

```sample.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.IO;
//using AngleSharp.Html.Dom;
//using AngleSharp.Html.Parser;
using System.Text.RegularExpressions;


        private static HttpClient client;

        static void Main(string[] args)
        {
            //ログインページではなくHTML内の$.ajaxのurlを参照
            string url_post = "https://pl.sega-mj.com/players/MjmidLogin";
            //ログイン後にジャンプするページ
            string url_history = "https://pl.sega-mj.com/playdata_view/showHistory";
            string USER_ID = "mjm54XXXXXX";
            string PASSWORD = "cTF8hjXXXX";
            string HTMLtext = "";

            try
            {
               //クライアント接続開始
                client = new HttpClient();
            }catch(Exception ex)
            {
                Console.WriteLine("【接続エラー】：" + ex.Message);
            }

            string HTMLtext = "";
            try
            {
                //POST要求（ログインしてクッキー取得）
                Task<bool> task_login = PostRequest(url_post, USER_ID, PASSWORD);

                //URLでHTMLを取得する。
                if (task_login.Result)
                {
                    Task<string> task_get = GetRequest(url_history);
                    HTMLtext = task_get.Result;
                }

            }
            catch (Exception ex)
            {
                Console.WriteLine("【応答エラー】：" + ex.Message);
            }

/*******(説明省略)************************************************************************

            //HTML構造解析
             string resultstr = AnalizeHtml(50, HTMLtext);

            // ファイルの保存（別タスクで非同期処理　続けて処理をおこなうときはWait()を削除する）
            Task task1 = Task.Run(() =>{  SaveFile(resultstr);  });
            task1.Wait();

****************************************************************************************/

            //End待機
            //Thread.Sleep(1000);
            Console.ReadKey();

        }

```


**ユーザで定義した関数 「ログイン処理」**

```Method.cs

       //POST要求（ログイン）
        async static Task<bool> PostRequest(string url_post, string user, string password)
        {
            //POSTで送る内容作成。mContent.Add(属性の値, 属性の名前) 
            MultipartFormDataContent mContent = new MultipartFormDataContent();
            mContent.Add(new StringContent(user) ,  "mjm_id");
            mContent.Add(new StringContent(password) ,  "password");

            //ヘッダ情報確認
            Console.WriteLine("【PostRequest Headers】" + mContent.Headers);

            //POSTしてレスポンスの要求。（要求先、要求内容）
            HttpResponseMessage response = await client.PostAsync(url_post, mContent);
            Console.WriteLine("【PostRequest Response】 " + response.StatusCode.ToString());
            
            if (response.StatusCode == HttpStatusCode.OK)
            {
                //応答ステータスがOKならHTML文字列を取得する。
                string contentstr = await response.Content.ReadAsStringAsync();
                Console.WriteLine("【PostRequest HTMLcontent】" + contentstr);
                Console.WriteLine("【PostRequest Cookie】" + response.Headers.GetValues("Set-Cookie").First() );
            }
            return true;

        }


```

**ユーザで定義した関数 「HTML取得」**

```Method2.cs
        //URLからHTMLをゲット（ログインには使用しない）
        async static Task<string> GetRequest(string url)
        {
            HttpResponseMessage response = await client.GetAsync(url);
            string contentstr = response.StatusCode.ToString();
            Console.WriteLine("【GetRequest Response】" + contentstr);

            if (response.StatusCode == System.Net.HttpStatusCode.OK)
            {
                contentstr = await response.Content.ReadAsStringAsync();
            }
            else if (response.StatusCode == HttpStatusCode.Redirect)
            {
                //StatusCode リダイレクトのときの処理
                var r = client.GetAsync(url).Result;
                Uri uri = new Uri(new Uri(url), r.Headers.Location);
                contentstr = client.GetAsync(uri).Result.Content.ReadAsStringAsync().Result;
            }

            return contentstr;
        }

```

上のスクリプトを作るうえで元のサイトの構造も知っておかないといけません。下のHTMLの意味は、
**$("#login").click(function()**　→　id=loginのボタンがクリックしたときの処理
**var data = $("#loginForm").serialize();**　→　id=loginFromのinputタグを結合してdataに代入（ここが一番重要）
この**serialize()**は、上の**MultipartFormDataContent**にあたる処理です。単一の情報を送る方法は山ほどあったけど、serialize化されたdataを送る方法を見つけるのに苦労しました。


```sample.html
<script>
		$("#login").click(function(){

				var data = $("#loginForm").serialize();
				//alert(data);

        		$.ajax({
        			   type: "POST", 　　<!-- C# PostAsync()がこれのこと-->
        			   url: "https://pl.sega-mj.com/players/MjmidLogin",<!-- C# PostAsync()の引数に使用-->
        			   data: data,　　<!-- C# PostAsync()の引数に使用（詳細は下枠）-->
        			   success: function(msg){
            			   var res = msg;
            			   if(res['status'] == 0)
            			   {
                			   location.href = "/mydata_view?ref=login";
            			   }else if( res['status'] == 2 ){
                			   alert("このIDは利用停止になっています。\n※不正行為を確認した為");
                		   } else {
                			   alert("IDまたはパスワードが違います。");
            			   }
        			   },
        			   error: function() {
            			   alert("メンテナンス中の可能性があります。時間をおいて再度アクセスして下さい。")
					   }
        		});

        		return false;
		});


```

上のHTMLの続きに下のようなinputタグの中にdataに代入される値がある。
現在はvalue=""となっているので値は入っていないことが分かる。

```data.html
                        <form class="login_form" action="" method="POST" id="loginForm" >
                                <div class="title">
                                    MJM-ID
                                </div>
                                <div class="unit">
                                    <input type="text" name="mjm_id" value="" maxlength="20" autocomplete="off">
                                </div>
                                <div class="title">
                                    パスワード
                                </div>
                                <div class="unit">
                                    <input type="password" name="password" value="" maxlength="20" autocomplete="off">
                                </div>
                        </form>

```

ユーザ定義のPostRequestの中では下のようにdataに値を代入している。mContentの引数の数でformタブとfilesタブを区別しているらしい。

ちなみに参照サイトではログイン方法が２通りあるためにpasswordというnameのタブが２つ存在するがどちらに入っても良いのかは調査中（formタブのそれぞれにidを振っているのでこれを識別できれば、、、）

```sample1.cs

            string USER_ID = "mjm54XXXXXX";
            string PASSWORD = "cTF8hjXXXX";

            MultipartFormDataContent mContent = new MultipartFormDataContent();
            mContent.Add(new StringContent(USER_ID) ,  "mjm_id");
            mContent.Add(new StringContent(PASSWORD) ,  "password");

            HttpResponseMessage response = await client.PostAsync(url_post, mContent);

```

#開発環境#
ブラウザ：Chrome  91.0.4472.77 (2021/6/2時点の最新)
言語：C# 
使用ソフト：VisualStudio2019v16.6.0
プロジェクト：コンソールアプリ(.NET Framework)
.NET Framework Version 4.8.04084
NuGetパッケージ
 @AngleSharp v0.15.0 (今回は説明省略のため使用していない)


#参照URL#
SEGA NET麻雀MJ　ログイン
https://pl.sega-mj.com/players/login
SEGA NET麻雀MJ　プレイ履歴
https://pl.sega-mj.com/playdata_view/showHistory