🔖

ASP.NET+Vue.jsでフォーム認証を使う場合

2022/09/06に公開

ASP.NET+Vue.jsでフォーム認証を使う場合

従来から運用している.NETFramework4.8のシステムがありますが、今回サーバ側とフロントエンド側で完全にプロジェクトを分けることが必要となりました
フロントエンドはvue.jsで作成します

サーバ側の準備

プロジェクト作成

VisualStudioで.NETFrameworkのASP.NET WebアプリケーションのWeb APIを作成します


フォーム認証の追加

Web.configにフォーム認証の設定を記載します
system.web内に記載します
細かく指定できますが、今回は最低限の設定とします

Web.config
<system.web>
  <compilation debug="true" targetFramework="4.8" />
  <httpRuntime targetFramework="4.8" />
  <!-- ここから追加 -->
  <authentication mode="Forms">
    <forms></forms>
  </authentication>
  <!-- ここまで -->
</system.web>

ログインコントローラーの作成

API Controllerを下記の内容で作成します
本来はIDやパスワードを受け取り、データベースなどからデータを取得し、認証処理を行いますが今回はテストの為、アクセスした瞬間認証するようにしています

LoginController.cs
using System.Web.Http;
using System.Web.Security;

namespace WebApplication1.Controllers
{
    public class LoginController : ApiController
    {
        public bool Get()
        {
            FormsAuthentication.SetAuthCookie("login_name", false);
            return true;
        }
    }
}

ログインユーザーのみ使用可能なコントローラーの作成

認証されていない場合表示されないAPI Controllerを下記の内容で作成します
認証されていればユーザー名(今回は"login_name"という文字列)を返します

UserController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace WebApplication1.Controllers
{
    [Authorize]
    public class UserController : ApiController
    {
        public string Get()
        {
            return User.Identity.Name;
        }
    }
}

動作確認

ブラウザで/api/userにアクセスします
すると認証を受けていないので下記のような画面になります

次に/api/loginにアクセスします
これで認証されました

再度/api/userにアクセスします
認証を受けているので「login_name」と表示されます

フロントエンドの準備

プロジェクトの作成

今回フロントエンドはvueで作成しますが、vue-cliではなくcreate-vueを使用します
下記コマンドでプロジェクトを作成します

npm init vue@latest

各設定は下記のようにしました

フォルダに移動しnpm installしておきます

疎通確認用コード追加

まずはアクセスできるかのテストの為、src/main.tsにサーバ側のログインAPIにアクセスするように記載します
あくまで動作確認の為のコードなので処理は適当です
ログインして、ユーザー名を取得するだけの簡単な処理です

main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'

import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.mount('#app')

// 以下追加
fetch('https://localhost:44308/api/login').then(() => {
    fetch('https://localhost:44308/api/user').then((res) => {
        console.log(res);
    });
});

動作確認

下記コマンドでテスト用サーバが起動します

npm run dev

http://localhost:3000にアクセスします
画面が表示されると同時にサーバ側のAPIにアクセスされます
この時開発ツールを確認するとCORSエラーが発生していることが確認できます

簡単に説明するとAjax通信ではオリジン(ドメインやポート)が異なる場合、アクセスできません
今回はドメインは同じlocalhostですが、ポートが44308と3000で異なるため別オリジンということになります
また、コンソールには表示されませんがサーバ側の認証にはクッキーを使用しています
認証クッキーに関しても別ドメインで共有するようにサーバ側の設定変更が必要となります
そうしないと、/api/loginにアクセスしても認証が受けられず、/api/userの内容が正しく表示されません

CORSおよび別ドメインとの認証クッキーの共有に対応する

CORSに対応する

nugetでMicrosoft.AspNet.WebApi.Corsをインストールします
App_Start/WebApiConfig.csにCORSを許可するようにコードを追加します
単純にCORSに対応するだけならEnableCorsAttributeの第一引数も*にしていいのですが、今回は認証クッキーの共有をするためにオリジンを指定する必要があるのでhttp://localhost:3000を指定しています

WebApiConfig.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using System.Web.Http.Cors;

namespace WebApplication1
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API の設定およびサービス
            // CORSの設定を追加(http://localhost:3000からのアクセスならheaderとmethodはすべて許可する)
            config.EnableCors(new EnableCorsAttribute("http://localhost:3000", "*", "*"));
            // Web API ルート
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

認証クッキーを共有する

Web.ConfigのformsにcookieSameSite="None" requireSSL="true"を追加します

<system.web>
    <compilation debug="true" targetFramework="4.8" />
    <httpRuntime targetFramework="4.8" />
	  <!-- ここから追加 -->
	  <authentication mode="Forms">
		  <forms cookieSameSite="None" requireSSL="true"></forms> <!-- cookieSameSite="None" requireSSL="true" を追加 -->
	  </authentication>
	  <!-- ここまで -->
  </system.web>

<system.webServer>に下記を追加します

<httpProtocol>
  <customHeaders>
    <add name="Access-Control-Allow-Credentials" value="true" />
  </customHeaders>
</httpProtocol>

最終的なWeb.configは下記のようになります

Web.config
<?xml version="1.0" encoding="utf-8"?>
<!--
  ASP.NET アプリケーションの構成方法の詳細については、
  https://go.microsoft.com/fwlink/?LinkId=301879 を参照してください
  -->
<configuration>
  <appSettings>
    <add key="webpages:Version" value="3.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
  </appSettings>
  <system.web>
    <compilation debug="true" targetFramework="4.8" />
    <httpRuntime targetFramework="4.8" />
	  <!-- ここから追加 -->
	  <authentication mode="Forms">
		  <forms cookieSameSite="None" requireSSL="true"></forms>  <!-- cookieSameSite="None" requireSSL="true" を追加 -->
	  </authentication>
	  <!-- ここまで -->
  </system.web>
  
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="Antlr3.Runtime" publicKeyToken="eb42632606e9261f" />
        <bindingRedirect oldVersion="0.0.0.0-3.5.0.2" newVersion="3.5.0.2" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Newtonsoft.Json" culture="neutral" publicKeyToken="30ad4fe6b2a6aeed" />
        <bindingRedirect oldVersion="0.0.0.0-12.0.0.0" newVersion="12.0.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Optimization" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="1.1.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="WebGrease" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="0.0.0.0-1.6.5135.21930" newVersion="1.6.5135.21930" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-5.2.7.0" newVersion="5.2.7.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Http" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.2.9.0" newVersion="5.2.9.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Net.Http.Formatting" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.2.9.0" newVersion="5.2.9.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
  <system.codedom>
    <compilers>
      <compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:1659;1699;1701" />
      <compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:41008 /define:_MYTYPE=\"Web\" /optionInfer+" />
    </compilers>
  </system.codedom>
<system.webServer>
    <handlers>
      <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
      <remove name="OPTIONSVerbHandler" />
      <remove name="TRACEVerbHandler" />
      <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
    </handlers>
  <!-- ここから追加 -->
	<httpProtocol>
		<customHeaders>
			<add name="Access-Control-Allow-Credentials" value="true" />
		</customHeaders>
	</httpProtocol>
  <!-- ここまで -->
  </system.webServer></configuration>

フロントエンドの変更

fetchの引数に{ credentials: 'include'}を追加します

main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'

import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.mount('#app')

// 以下追加
fetch('https://localhost:44308/api/login', { credentials: 'include'}).then(() => {
    fetch('https://localhost:44308/api/user', { credentials: 'include'}).then((res) => {
        console.log(res);
    });
});

再度動作確認

http://localhost:3000にアクセスします
開発ツールのNetworkタブで正常に通信ができていることが確認できます

ユーザー名(login_name)も取得できています

まとめ

今回の記事ではあくまで動作確認の為、細かい処理は省きました
実際に開発する際はCORS対応で追加した
config.EnableCors(new EnableCorsAttribute("http://localhost:3000", "", ""));
のhttp://localhost:3000をWeb.Configで設定できるようにするなど適宜変更する必要があります

Discussion