ASP.NET Core + React + Typescriptで認証付きWebアプリのひな型を作る③サーバーサイドに認証機能を追加する

43503 ワード

概要

前回までで、サーバーサイドの情報を受け取ってクライアントサイドで表示することができるようになりました。最低限の動作が確認できるようになったので、認証機能を追加していこうと思います。

認証機能追加の流れは以下です

  • 環境確認・追加
  • 必要なパッケージのインストール

環境確認・追加

Extensionsを開きます

↓以下2パッケージが入っている事を確認して、なければインストールしておきます

Nuget GalleryはNugetパッケージをインストールする際に、SQLiteはSQLiteのデータを確認するために使用します。SQLiteはインストールしなくてもアプリの構築自体は可能ですが、データを閲覧したい際などに便利です

必要なパッケージのインストール

拡張機能の設定後は、必要なパッケージをインストールしていきます。
F1を押下して、Nugetパッケージマネージャーを開きます

パッケージをインストールします。

インストールするパッケージは、以下です

  • 認証機能
    • Microsoft.AspNetCore.Authentication.JwtBearer
    • Microsoft.AspNetCore.Identity.EntityFrameworkCore
    • Microsoft.AspNetCore.Identity.UI
  • EntityFramework関連
    • Microsoft.EntityFrameworkCore.Design
    • Microsoft.EntityFrameworkCore.Sqlite

インストールできているかの確認方法はいろいろありますが、↓のようにプロジェクトファイルを見てみて上記のパッケージが入っていればOKです。

コード追加・変更

準備ができたら認証関連のコードを追加していきます。
以下のファイルを追加or変更します

ファイル名 変更内容 役割
ApplicationUser.cs 新規追加 ユーザー管理テーブル用モデルクラス
ApplicationDbContext.cs 新規追加 データ(今回の場合はSQLite)を操作するためのコンテキストクラス
AccountController.cs 新規追加 ログイン・ユーザー登録処理・結果取得を行うコントローラー
LoginModel.cs 新規追加 ログイン処理時にコントローラーにデータを渡すためのモデルクラス
RegisterModel.cs 新規追加 ユーザー登録時にコントローラーにデータを渡すためのモデルクラス
UserModel.cs 新規追加 ログイン・ユーザー登録後にコントローラーから結果を返す際に使用するクラス
Startup.cs 変更 認証関連の処理を追加する
appsettings.json 変更 接続文字列を追加する

ApplicationUser.cs

ApplicationUser.cs
using Microsoft.AspNetCore.Identity;

namespace server_app.Models
{
    public class ApplicationUser : IdentityUser
    {
    }
}

ApplicationDbContext.cs

ApplicationDbContext.cs
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using server_app.Models;

namespace server_app.Data
{
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
    }
}

AccountController.cs

AccountController.cs
using System.Security.Claims;
using System.Threading.Tasks;
using server_app.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace server_app.Controllers
{
    [AllowAnonymous]
    [ApiController]
    [Route("[controller]")]
    public class AccountController : ControllerBase
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly  SignInManager<ApplicationUser> _signinManager;
        public AccountController(UserManager<ApplicationUser> userManager
            ,SignInManager<ApplicationUser> signinManager
             )
        {
            _signinManager = signinManager;
            _userManager = userManager;
        }

        [HttpPost("login")]
        public async Task<ActionResult<UserModel>> Login(LoginModel loginModel)
        {
            
            var user = await _userManager.FindByEmailAsync(loginModel.Email);

            if(user == null) return Unauthorized();

            var result = await _signinManager.CheckPasswordSignInAsync(user, loginModel.Password, false);

            if(result.Succeeded)
            {
                return CreateUserObject(user);
            }

            return Unauthorized();
        }

        [HttpPost("register")]
        public async Task<ActionResult<UserModel>> Register(RegisterModel registerModel)
        {
            if(await _userManager.Users.AnyAsync(x => x.Email == registerModel.Email))
            {
                ModelState.AddModelError("email", "Email taken");
                return ValidationProblem();
            }
            if(await _userManager.Users.AnyAsync(x => x.UserName == registerModel.Username))
            {
                ModelState.AddModelError("username", "Username taken");
                return ValidationProblem();
            }

            var user = new ApplicationUser
            {
                Email = registerModel.Email,
                UserName = registerModel.Username
            };

            var result = await _userManager.CreateAsync(user, registerModel.Password);

            if(result.Succeeded)
            {
                return CreateUserObject(user);
            }

            return BadRequest("Problem regist User");
        }
        private UserModel CreateUserObject(ApplicationUser user)
        {
            return new UserModel
            {
                    Username = user.UserName
            };
        }
    }
}

LoginModel.cs

LoginModel.cs
namespace server_app.Models
{
    public class LoginModel
    {
        public string Email {get; set;}
        public string Password {get; set;}
        
    }
}

RegisterModel.cs

RegisterModel.cs
using System.ComponentModel.DataAnnotations;

namespace server_app.Models
{
    public class RegisterModel
    {
        [Required]
        [EmailAddress]
        public string Email { get; set;}
        [Required]
        [RegularExpression("^[a-zA-Z0-9.?/-]{8,24}$", ErrorMessage ="Password must be complex")]
        public string Password {get; set; }
        [Required]
        public string Username { get; set; }

        
    }
}

UserModel.cs

UserModel.cs
namespace server_app.Models
{
    public class UserModel
    {
        //public string Token { get; set; }
        public string Username {get; set; }
        
    }
}

Startup.cs

Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity.UI;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
+using server_app.Data;
+using server_app.Models;



namespace server_app
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
+            services.AddDbContext<ApplicationDbContext>(opt =>
+            {
+             opt.UseSqlite(Configuration.GetConnectionString("DefaultConnection"));
+            } );


            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "server_app", Version = "v1" });
            });

+             services.AddDefaultIdentity<ApplicationUser>(
+                 options => {
+                                options.SignIn.RequireConfirmedAccount = false;
+                             }
+                 )
+                 .AddEntityFrameworkStores<ApplicationDbContext>();

            services.AddCors(o => o.AddPolicy(MyAllowSpecificOrigins, builder =>
            {
                builder.AllowAnyOrigin()    // Allow CORS Recest from all Origin
                       .AllowAnyMethod()    // Allow All Http method
                       .AllowAnyHeader();   // Allow All request header
            }));

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "server_app v1"));
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseCors(MyAllowSpecificOrigins);   // Add For CORS

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

appsettings.json

接続文字列を追加します

appsettings.json
{
+  "ConnectionStrings": {
+    "DefaultConnection":"Data Source=database.db"
+  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

データベース更新

コードの追加が出来たので、データベースを生成します。
以下のコマンドを実行します

dotnet ef migrations add InitialCreate
dotnet ef database update

成功すると以下のようなメッセージが表示されます

以下の様にDBが生成されます

サーバーサイドで、「ユーザー登録」と、「ID・パスワードを入力するとOK/NGを返す」機能の実装が出来ました。サーバーサイドだけを見ても、ここまでの内容では「認証機能」としてはまだ不完全ですが、クライアントからアクセスして操作するための最低限の機能が出来たので、次回はクライアントを更新してクライアント側から「ユーザー登録」・「ログイン結果の取得」機能を実装していきます。