zukucode
主にWEB関連の情報を技術メモとして発信しています。

ASP.NET Core SPAサイトにcookieベースのログイン認証を実装する

ASP.NET CoreのSPAプロジェクトにcookie(session)ベースのログイン認証を実装する方法を紹介します。

SPAはページはすべてJavaScriptで実装されているため、ページの情報を保護するのは難しいです。(routerなどで保護してもJavaScriptのソースから見えてしまうため)

そのため、ページにはデザインなどの枠組みだけ実装しておき、機密情報はajaxでデータとして取得するようにします。

ページを保護するのではなく、ajaxでアクセスされた際のデータを保護するように認証処理を実装します。

Startup.csの修正

Startup.csに設定を追記します。

上述したように、ページアクセスに対しての保護は行わないため、未認証時にログイン画面にリダイレクトする処理は行いません。

そのため、未認証時はログイン画面に遷移するのではなくエラーを返すようにします。

(ログイン画面に遷移する処理はクライアント側で行います)

認証保護はコントローラーでURLごとに設定する方法もありますが、今回はアプリケーション全体で認証保護の設定を行うようにします。

ただし、静的ファイル(html, js, css)は認証保護の影響は受けません。

そのためSPAの初回アクセス時などは問題なくhtmlがレスポンスされます。

Startup.cs(一部抜粋)
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Authorization;

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        // cookieベースの認証
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    }).AddCookie(options =>
    {
        options.SlidingExpiration = true;
        options.Events.OnRedirectToLogin = cxt =>
        {
            // ログイン画面に遷移するのではなくエラーを返す
            cxt.Response.StatusCode = StatusCodes.Status401Unauthorized;
            return Task.CompletedTask;
        };
        options.Events.OnRedirectToAccessDenied = cxt =>
        {
            // エラー画面に遷移するのではなくエラーを返す
            cxt.Response.StatusCode = StatusCodes.Status403Forbidden;
            return Task.CompletedTask;
        };
        options.Events.OnRedirectToLogout = cxt => Task.CompletedTask;
    });


    services.AddControllersWithViews(options =>
    {
        // すべてのアクセスに対して認証保護を適用する
        options.Filters.Add(new AuthorizeFilter(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()));
    });


public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
{
    app.UseRouting();

    // app.UseRouting();の下に追加
    app.UseAuthentication();
    app.UseAuthorization();

動作確認

プロジェクトを作成した際の雛形として作成されたWeatherForecastControllerで動作確認を行います。

ここではAPIとしてajaxでアクセスするため、Routeの設定箇所に以下のようにapiを追加します。

こうすると、api/WeatherForecastでアクセスできるようになります。

    [ApiController]
    [Route("[controller]")]
    [Route("api/[controller]")]
    public class WeatherForecastController : ControllerBase

クライアント側で、ajaxのリクエストを投げます。

ここではajaxのライブラリにaxiosを使用します。

const response = axios.get('/api/WeatherForecast');

未認証の状態でアクセスをすると401(Unauthorized)のエラーになることが確認できます。

認証保護の除外

例えばログイン時のAPIなど、未認証の状態でもアクセスしたい場合があります。

その場合は対象のメソッドにAllowAnonymousを設定します。

using Microsoft.AspNetCore.Authorization;


    [HttpGet]
    [AllowAnonymous]
    public IEnumerable<WeatherForecast> Get()

AllowAnonymousを設定後にアクセスするとデータが取得できることを確認できます。

ログイン/ログアウト処理

認証用のコントローラーを作成し、以下のようにログイン処理とログアウト処理を実装します。

「認証判定処理」の部分は実際にはデータベースなどでIDやパスワードを使って判定することになると思います。

AuthController.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;

namespace code.Controllers
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class AuthController : ControllerBase
    {

        public class LoginRequest
        {
            public string login_id { get; set; }
            public string password { get; set; }
        }

        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> Login(LoginRequest request)
        {
            if (認証判定処理(request.login_id, request.password) == false)
            {
                return BadRequest("ユーザー名またはパスワードが違います。");
            }
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.NameIdentifier, user.login_id),
            };
            var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);

            var claimsPrincipal = new ClaimsPrincipal(identity);
            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal);

            return Ok();
        }

        [HttpPost]
        public async Task<IActionResult> Logout()
        {
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

            return Ok();
        }
    }
}

クライアント側からは以下のようにログイン処理をコールします。

const response = await axios.post('/api/auth/Login', { login_id: 入力したログインID, password: 入力したパスワード });

ログイン処理に成功した場合、.AspNetCore.Cookiesという名前で認証cookieがレスポンスされます。

次回以降のリクエストにはこのcookieが自動的に含まれるので、ログイン状態を維持できます。

また、ログアウト処理をした場合、.AspNetCore.Cookiesのcookieが削除されることが確認できます。



関連記事