WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

HttpClient で ASP.NET Web API にアクセス

by WebSurfer 29. September 2019 12:44

Web API に HttpClient を使ってアクセスし、認証トークンを取得して、JSON 形式のデータを POST 送信するサンプルを書きます。

HttpClient で ASP.NET Web API にアクセス

アクセス先の Web API は先の記事「ASP.NET MVC に Web API 追加」に書いたもので、具体的には、Visual Studio 2015 のテンプレートで生成した既存の MVC5 プロジェクトに、別途 Web API プロジェクトを作って必要なパッケージ、コードを追加したものです。

既存の MVC5 プロジェクトは ASP.NET Identity を利用してクッキーベースのユーザー認証を行っています。

追加した Web API のユーザー認証はクッキーベースとするのではなく、Web API で推奨されているトークンベースとしています。

MVC5 側はクッキーベースで、Web API 側はトークンベースで独立して認証が働きます。なお、ユーザ情報はどちらも ASP.NET Identity から得ています。

その他、運用上は SSL の実装は必須ということで、SSL 通信を強制するためのフィルターを追加しています。

HttpClient を使ったアプリから Web API にアクセスするには Microsoft のドキュメント Call a Web API From a .NET Client (C#) で紹介されている Microsoft.AspNet.WebApi.Client を使うのが便利だそうですが、ここではそれは使わないで実装してみました。

基本的には先の記事「HttpClient で WCF サービスを呼出」の実装例とほぼ同じです。

ただし、認証トークンを取得するには、ユーザー情報を application/x-www-form-urlencoded 形式で POST しなければなりませんが、そこのところが上の記事とは異なります。

認証トークンを得るためには、先の記事「ASP.NET Web API の認証」で書きましたように、grant_type, username, password 情報をトークンエンドポイント /Token に POST します。

認証に成功すると access_token, token_type, expires_in, userName, .issued, .expires という情報が JSON 文字列として返ってきます。その中の access_token が認証トークンです。

Web API にアクセスする際は要求ヘッダに Authorization: Bearer に続けて空白一文字+有効な認証トークンを設定してやれば認証が通ります。

認証トークンの取得と JSON 形式のデータを POST 送信する Windows Forms アプリのサンプルコードは以下の通りです。説明はコードの中にコメントで入れましたので、それを見てください。

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Net.Http;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;

namespace WindowsFormsApplication2
{
    public partial class Form5 : Form
    {
        // socket 浪費防止のため static のして使い回す
        private static HttpClient client;

        private string loginUrl = "トークン要求先 URL";
        private string apiUrl = "Web API の URL";
        private string email = "ユーザー ID";
        private string passsword = "パスワード";
        private int id = 6;
        private string name = "ガッチャマンの息子";

        // 認証トークン
        private string token = "";

        public Form5()
        {
            InitializeComponent();

            if (client == null)
            {
                client = new HttpClient();
            }

            this.textBox1.Text = email;
            this.textBox2.Text = passsword;
            this.textBox3.Text = id.ToString();
            this.textBox4.Text = name;
        }

        // 認証トークンの取得
        private async void button1_Click(object sender, 
                                                EventArgs e)
        {
            if (!string.IsNullOrEmpty(this.token)) return;

            // 認証に必要なユーザー情報は
            // application/x-www-form-urlencoded 形式で POST
            // 送信する。
            // そのために送信するユーザー情報から以下のように
            // Dictionary<string, string> オブジェクトを生成し、
            var param = new Dictionary<string, string>
            {
                { "grant_type", "password"},
                { "username", this.textBox1.Text },
                { "password", this.textBox2.Text }
            };

            // それを引数に FormUrlEncodedContent オブジェクト
            // を生成・初期化して PostAsync で送信する。
            // Content-Type: application/x-www-form-urlencoded
            // は自動的に設定されるとのこと
            var content = new FormUrlEncodedContent(param);
            var response = 
                 await client.PostAsync(this.loginUrl, content);

            // 応答コンテンツを Stream として取得
            using (Stream responseStream = 
                await response.Content.ReadAsStreamAsync())
            {
                // JSON シリアライザの初期化
                var ser = 
                  new DataContractJsonSerializer(typeof(Token));

                // 応答コンテンツを逆シリアル化して C# のオブジ
                // ェクトを取得
                Token auth = 
                         (Token)ser.ReadObject(responseStream);

                // 認証トークンを token フィールドに設定
                this.token = auth.access_token;
            }
        }

        // JSON 形式のデータを POST 送信
        private async void button2_Click(object sender, 
                                                EventArgs e)
        {
            if (string.IsNullOrEmpty(this.token)) return;

            // POST 送信を指定
            var request = 
                new HttpRequestMessage(HttpMethod.Post, apiUrl);

            // POST 送信する JSON 文字列
            string postData = "";

            // Hero オブジェクトを生成しそれを JSON 文字列に
            // シリアライズする
            Hero postHero = new Hero
            {
                Id = int.Parse(this.textBox3.Text),
                Name = this.textBox4.Text
            };

            // シリアライズは DataContractJsonSerializer を使う
            using (MemoryStream stream = new MemoryStream())
            {
                var ser = 
                  new DataContractJsonSerializer(typeof(Hero));
                ser.WriteObject(stream, postHero);
                stream.Position = 0;
                using (var reader = new StreamReader(stream))
                {
                    postData = reader.ReadToEnd();
                }
            }

            // Content-Type: application/json; charset=utf-8 が
            // 要求ヘッダに必要。それを POST 送信する JSON 文字
            // 列と共にここで設定
            request.Content = new StringContent(postData, 
                                            Encoding.UTF8, 
                                            "application/json");

            // 認証トークンを要求ヘッダに設定
            request.Headers.Add("Authorization", 
                                "Bearer " + this.token);

            // JSON 文字列を SendAsync で POST 送信する
            var response = await client.SendAsync(request);

            // 応答コンテンツを Stream として取得
            using (Stream responseStream = 
                await response.Content.ReadAsStreamAsync())
            {
                // JSON シリアライザの初期化
                var ser = new DataContractJsonSerializer(
                                           typeof(List<Hero>));

                // 応答コンテンツを逆シリアル化して C# のオブジ
                // ェクトを取得
                List<Hero> heros = 
                    (List<Hero>)ser.ReadObject(responseStream);

                string result = "";
                foreach (Hero hero in heros)
                {
                    result += string.Format("{0}: {1}\r\n", 
                                            hero.Id, hero.Name);
                }
                this.textBox5.Text = result;
            }
        }
    }

    // 以下のクラス定義を public partial class Form5 : Form の
    // 上に持ってくるとデザイン画面が開かないので注意

    // トークン要求に対し応答として返ってくるデータ
    // access_token, token_type, expires_in は OAuth2 で定めら
    // れているもの。userName は informational。他に .issued,
    // .expires というデータも返ってくるが . がプロパティの識
    // 別子として使えないので以下には設定しない
    [DataContract]
    public class Token
    {
        [DataMember]
        public string access_token { get; set; }

        [DataMember]
        public string token_type { get; set; }

        [DataMember]
        public int expires_in { get; set; }

        [DataMember]
        public string userName { get; set; }
    }

    // Web API に POST 送信するデータ
    [DataContract]
    public class Hero
    {
        [DataMember]
        public int Id { get; set; }

        [DataMember]
        public string Name { get; set; }
    }
}

上記のコードの実行結果がこの記事の上���ある画像です。認証トークンを取得した後、Id と Name から作成した JSON 文字列を Web API に POST 送信し、返ってきた JSON 文字列を一番下のテキストボックスに表示したところです。

Tags: ,

Web API

応答ヘッダが 64KB を超えるとエラー

by WebSurfer 2. September 2019 12:00

HttpClient を使った HPPT 通信で、応答ヘッダが 64KB を超えると WebException 例外がスローされるという話を備忘録として書いておきます。

(元の話は Teratail のスレッド「GetAsync処理時のメッセージの長さが制限の解消方法について」のもので、実際に自分が経験した訳ではなく聞いた話です)

同様な問題は HttpWebResponse / HttpWebRequest を使った時から起こっていた問題だそうで、応答ヘッダが 64KB を超えると WebException がスローされ、以下のエラーメッセージが出るそうです。

"接続が切断されました: メッセージの長さが制限を超えています。"

英文では、

"The underlying connection was closed: The message length limit was exceeded."

応答ヘッダが 64KB を超えるというのはレアなのか、日本語のエラーメッセージでググっても参考になる記事はヒットしませんでした。

でも、英語圏まで検索範囲を広げる(英文でググる)と HttpWebResponse / HttpWebRequest でこの問題に遭遇した人はいるようで、WebException: "The message length limit was exceeded" 他の記事がヒットします。

その記事に書いてある解決策は、HttpWebRequest の MaximumResponseHeadersLength プロパティを -1 (無制限) に設定することだそうです。(未検証・未確認です)

HttpClient を使う場合は、.NET Framework 4.7.1 以降ですが、HttpClientHandler クラスMaxResponseHeadersLength プロパティを使って応答ヘッダのサイズの許容最大値を設定できるそうです。

// Create an HttpClientHandler object
HttpClientHandler handler = new HttpClientHandler();
handler.MaxResponseHeadersLength = 128;  // 128KB

// Create an HttpClient object
HttpClient client = new HttpClient(handler);

.NET 4.5 では MaxResponseHeadersLength プロパティは使えず .NET 4.7.1 で使えるようになったということは、HttpWebRequest / HttpWebResponse で起こっていた問題に対応できないことを指摘されて追加したのかもしれませんね。(想像です)

Tags: , , ,

.NET Framework

HttpClient でファイルアップロード

by WebSurfer 11. August 2019 14:00

HttpClient クラス を使った Windows Forms アプリで multipart/form-data 形式にてファイルをアップロードする方法を書きます。

HttpClient でファイルをアップロード

multipart/form-data 形式で送信するには MultipartFormDataContent クラスを利用します。

MultipartFormDataContent クラスのインスタンスを生成し、それに HttpContent クラスの派生クラスを multipart の各パートとして Add します。

この記事では文字列とファイルを別々のパートとして送信するサンプルを書きます。

その場合、試用する HttpContent クラスの派生クラスは、文字列を送信する場合は StringContent クラスを、ファイルを送信する場合は StreamContent クラスを使うのがよさそうです。

下のサンプルコードを見てください、StringContent クラスとStringContent クラスを初期化してヘッダ情報を設定し、それぞれを MultipartFormDataContent オブジェクトに Add し、さらにそれを HttpRequestMessage オブジェクトの Content プロパティに設定しています。

using System;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;

namespace WindowsFormsApplication2
{
  public partial class Form3 : Form
  {
    // socket 浪費問題対応
    private static HttpClient httpClient;

    public Form3()
    {
      InitializeComponent();
      this.textBox1.Text = "";
      this.label1.Text = "";
    }

    // ファイル選択に OpenFileDialog 利用
    private void button1_Click(object sender, EventArgs e)
    {
      OpenFileDialog openFileDialog1 = new OpenFileDialog();

      openFileDialog1.InitialDirectory = 
                               @"C:\Users\surfe\Pictures";
      openFileDialog1.Filter = 
           "jpeg files (*.jpg)|*.jpg|All files (*.*)|*.*";
      openFileDialog1.FilterIndex = 1;
      openFileDialog1.RestoreDirectory = true;

      if (openFileDialog1.ShowDialog() == DialogResult.OK)
      {
        this.textBox1.Text = openFileDialog1.FileName;
      }
    }

    // HttpWebRequest を利用
    private void button2_Click(object sender, EventArgs e)
    {
      // ・・・コードは省略・・・
    }

    // HttpClient を利用
    private async void button3_Click(object s, EventArgs e)
    {
      if (string.IsNullOrEmpty(this.textBox1.Text)) return;

      this.label1.Text = "";

      if (httpClient == null)
      {
        httpClient = new HttpClient();
      }

      //送信するファイルへのパス
      string filePath = this.textBox1.Text;
      string fileName = Path.GetFileName(filePath);

      string url = @"送信先 Web サーバーの URL";

      MultipartFormDataContent content = 
                              new MultipartFormDataContent();
            
      // ファイルのみでなく文字列も送信してみる
      string strData = "これは、テストです。";
      StringContent stringContent = new StringContent(strData);
      stringContent.Headers.ContentDisposition = 
               new ContentDispositionHeaderValue("form-data")
      {
        Name = "comment"
      };
      content.Add(stringContent);

      // アップロードするファイル
      using (FileStream fs = new FileStream(filePath, 
                                            FileMode.Open, 
                                            FileAccess.Read))
      {
        StreamContent streamContent = new StreamContent(fs);
        streamContent.Headers.ContentDisposition = 
               new ContentDispositionHeaderValue("form-data")
        {
          Name = "upfile",
          FileName = fileName
        };                
        streamContent.Headers.ContentType = 
                    new MediaTypeHeaderValue("image/jpeg");
        content.Add(streamContent);

        // メソッド (POST) と送信先の URL 指定
        HttpRequestMessage request = 
                 new HttpRequestMessage(HttpMethod.Post, url);
        request.Content = content;

        // ここでファイルを HTTP ストリームに書き込むので、
        // 以下は using の { } 内にないとファイルが読めな
        // いというエラーになる
        HttpResponseMessage response = 
                           await httpClient.SendAsync(request);

        // 応答のコンテンツを Stream として取得
        using (Stream responseStream = 
                   await response.Content.ReadAsStreamAsync())
        {
          using (StreamReader sr = 
              new StreamReader(responseStream, Encoding.UTF8))
          {
            this.label1.Text = sr.ReadToEnd();
          }
        }
      }            
    }
  }
}

上記のコードを実行して、HttpClient を使ってファイルを送信し、返ってきた応答を表示したのが上に表示した画像です。

下の画像は Fiddler でのキャプチャ結果で、送信ヘッダとコンテンツを表示しています。送信ヘッダに指定されている boundary で StringContent とStringContent の部分が分けられ、各パートに Content-Disposition などのヘッダ情報が付与されているのが分かるでしょうか?

送信ヘッダとコンテンツ

最後になりましたが HttpClient を使う際の注意点として重要なことを書いておきます。それは、using 句を使うなどしてHttpClient の初期化と Dispose を繰り返すと socket が浪費されるという問題があるということです。詳しくは以下の記事を見てください。static にして使い回すのが良いとのことです。

YOU'RE USING HTTPCLIENT WRONG AND IT IS DESTABILIZING YOUR SOFTWARE

Tags: ,

Upload Download

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  October 2019  >>
MoTuWeThFrSaSu
30123456
78910111213
14151617181920
21222324252627
28293031123
45678910

View posts in large calendar