C#でのユーザー認証と認可の実装方法を徹底解説

C#を使用したアプリケーション開発では、ユーザーの認証と認可は重要な要素です。本記事では、C#およびASP.NET Coreを用いてユーザー認証と認可を実装する方法を、基本から応用まで詳しく解説します。これにより、安全で信頼性の高いアプリケーションを構築するための知識を習得できるでしょう。

目次

ユーザー認証と認可の基本概念

ユーザー認証と認可は、セキュリティ対策の基本です。認証はユーザーの身元確認を行い、認可はそのユーザーに対するアクセス権を決定します。これらの概念を理解することは、アプリケーションのセキュリティを確保するための第一歩です。

認証とは

認証は、ユーザーが自分であることを証明するプロセスです。一般的には、ユーザー名とパスワードを使用して行われますが、他にもバイオメトリクスや多要素認証などの方法があります。

認可とは

認可は、認証されたユーザーが何をすることが許可されているかを決定するプロセスです。これは、役割ベースのアクセス制御(RBAC)やポリシーベースのアクセス制御(PBAC)を使用して実現されます。

C#における認証の実装

C#では、ASP.NET Core Identityを使用して認証機能を簡単に実装できます。これにより、ユーザーの登録、ログイン、パスワード管理などの基本的な認証機能を提供できます。

ASP.NET Core Identityのセットアップ

ASP.NET Core Identityは、NuGetパッケージとして提供されています。以下のコマンドを使用してインストールします。

dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore

プロジェクトにインストールしたら、スタートアップファイルに必要なサービスを追加します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddDefaultIdentity<IdentityUser>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddControllersWithViews();
}

ユーザーモデルの設定

ユーザー情報を保持するために、IdentityUserクラスを継承したカスタムユーザーモデルを作成します。

public class ApplicationUser : IdentityUser
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

これを使って、データベースにユーザー情報を保存します。

認証用のコントローラー作成

ユーザー登録、ログイン、ログアウトのためのコントローラーを作成します。

public class AccountController : Controller
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;

    public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
    {
        _userManager = userManager;
        _signInManager = signInManager;
    }

    [HttpPost]
    public async Task<IActionResult> Register(RegisterViewModel model)
    {
        if (ModelState.IsValid)
        {
            var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
            var result = await _userManager.CreateAsync(user, model.Password);
            if (result.Succeeded)
            {
                await _signInManager.SignInAsync(user, isPersistent: false);
                return RedirectToAction("Index", "Home");
            }
        }
        return View(model);
    }

    [HttpPost]
    public async Task<IActionResult> Login(LoginViewModel model)
    {
        if (ModelState.IsValid)
        {
            var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
            if (result.Succeeded)
            {
                return RedirectToAction("Index", "Home");
            }
        }
        return View(model);
    }

    [HttpPost]
    public async Task<IActionResult> Logout()
    {
        await _signInManager.SignOutAsync();
        return RedirectToAction("Index", "Home");
    }
}

このようにして、ASP.NET Core Identityを用いた基本的な認証機能を実装できます。

C#における認可の実装

C#では、ASP.NET Coreを使用して認可機能を実装することができます。認可は、ポリシーベースの認可と役割ベースの認可の2つの主要な方法で実現されます。

役割ベースの認可(RBAC)

役割ベースのアクセス制御は、ユーザーに役割を割り当て、その役割に基づいてアクセス権を決定する方法です。以下のコードは、役割ベースの認可を実装する方法を示しています。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddDefaultIdentity<IdentityUser>()
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddControllersWithViews();

    services.AddAuthorization(options =>
    {
        options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // 省略...

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

これにより、「Admin」役割を持つユーザーのみがアクセスできるポリシーを作成します。

ポリシーベースの認可(PBAC)

ポリシーベースのアクセス制御は、特定の条件に基づいてアクセス権を決定する方法です。以下の例では、特定の要求を満たすユーザーのみがアクセスできるように設定します。

services.AddAuthorization(options =>
{
    options.AddPolicy("Over18Only", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(18)));
});

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }
    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }
}

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
    {
        if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth))
        {
            return Task.CompletedTask;
        }

        var birthDate = Convert.ToDateTime(context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth).Value);
        int age = DateTime.Today.Year - birthDate.Year;

        if (birthDate > DateTime.Today.AddYears(-age))
        {
            age--;
        }

        if (age >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

このコードにより、18歳以上のユーザーのみが特定のリソースにアクセスできるようになります。

データベースとの連携

ユーザー情報をデータベースに保存し、認証に利用することで、アプリケーションのセキュリティと効率を向上させることができます。以下では、ASP.NET Core IdentityとEntity Framework Coreを使用してデータベースとの連携方法を説明します。

データベースコンテキストの設定

まず、データベースコンテキストを設定します。これにより、データベースとアプリケーション間の通信が可能になります。

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }
}

次に、スタートアップファイルにデータベース接続文字列を設定します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddDefaultIdentity<ApplicationUser>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddControllersWithViews();
}

ユーザーモデルのカスタマイズ

ユーザーモデルをカスタマイズして、必要なプロパティを追加します。以下は、ApplicationUserクラスに追加プロパティを含めた例です。

public class ApplicationUser : IdentityUser
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

このクラスを使用して、ユーザー情報をデータベースに保存します。

マイグレーションの作成とデータベースの更新

次に、マイグレーションを作成し、データベースを更新します。以下のコマンドを使用してマイグレーションを作成します。

dotnet ef migrations add InitialCreate

その後、以下のコマンドを実行してデータベースを更新します。

dotnet ef database update

これにより、必要なテーブルがデータベースに作成されます。

ユーザー情報の保存と取得

ユーザー情報を保存するためには、UserManagerクラスを使用します。以下は、ユーザー登録時にユーザー情報を保存する例です。

public async Task<IActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser
        {
            UserName = model.Email,
            Email = model.Email,
            FirstName = model.FirstName,
            LastName = model.LastName
        };
        var result = await _userManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            await _signInManager.SignInAsync(user, isPersistent: false);
            return RedirectToAction("Index", "Home");
        }
    }
    return View(model);
}

ユーザー情報を取得するためには、UserManagerクラスのFindByIdAsyncメソッドなどを使用します。

トークンベースの認証

トークンベースの認証は、セッション情報をトークンとしてクライアントに渡し、各リクエストでそのトークンを利用して認証を行う方式です。ここでは、JWT(JSON Web Token)を用いたトークンベースの認証の実装方法を説明します。

JWT認証の設定

まず、必要なパッケージをインストールします。

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

次に、スタートアップファイルにJWT認証を設定します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddDefaultIdentity<ApplicationUser>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = Configuration["Jwt:Issuer"],
            ValidAudience = Configuration["Jwt:Issuer"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
        };
    });

    services.AddControllersWithViews();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // 省略...

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

JWTの生成

ユーザーがログインすると、JWTを生成してクライアントに渡します。以下は、そのためのコントローラーメソッドです。

[HttpPost]
public async Task<IActionResult> Login(LoginViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = await _userManager.FindByEmailAsync(model.Email);
        if (user != null && await _userManager.CheckPasswordAsync(user, model.Password))
        {
            var tokenHandler = new JwtSecurityTokenHandler();
            var key = Encoding.ASCII.GetBytes(Configuration["Jwt:Key"]);
            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(new Claim[]
                {
                    new Claim(ClaimTypes.Name, user.Id.ToString())
                }),
                Expires = DateTime.UtcNow.AddHours(1),
                SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
            };
            var token = tokenHandler.CreateToken(tokenDescriptor);
            return Ok(new
            {
                token = tokenHandler.WriteToken(token)
            });
        }
    }
    return Unauthorized();
}

JWTの検証

クライアントから送られてくるJWTを検証するためには、認証属性をコントローラーまたはアクションに追加します。

[Authorize]
[HttpGet]
public IActionResult ProtectedEndpoint()
{
    return Ok("This is a protected endpoint.");
}

これにより、JWTが有効でない場合、アクセスが拒否されます。

OAuth 2.0とOpenID Connectの実装

外部サービスとの連携により、ユーザーの認証を簡素化し、セキュリティを向上させることができます。ここでは、OAuth 2.0とOpenID Connectを用いた認証の実装方法を説明します。

OAuth 2.0とOpenID Connectの違い

OAuth 2.0は、リソースへのアクセスを第三者に許可するためのプロトコルであり、認可の役割を担います。一方、OpenID Connectは、OAuth 2.0をベースにした認証プロトコルであり、ユーザーの身元を確認するために使用されます。

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

まず、必要なNuGetパッケージをインストールします。

dotnet add package Microsoft.AspNetCore.Authentication.Cookies
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

スタートアップファイルの設定

次に、スタートアップファイルに必要な認証サービスを追加します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options =>
    {
        options.ClientId = Configuration["Authentication:ClientId"];
        options.ClientSecret = Configuration["Authentication:ClientSecret"];
        options.Authority = Configuration["Authentication:Authority"];
        options.ResponseType = "code";
        options.SaveTokens = true;
        options.Scope.Add("profile");
        options.Scope.Add("email");
    });

    services.AddControllersWithViews();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // 省略...

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

認証フローの実装

ユーザーが外部サービスを利用してログインするためのコントローラーを作成します。

public class AccountController : Controller
{
    [HttpGet]
    public IActionResult Login()
    {
        return Challenge(new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectDefaults.AuthenticationScheme);
    }

    [HttpGet]
    public async Task<IActionResult> Logout()
    {
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
        return RedirectToAction("Index", "Home");
    }
}

ユーザー情報の取得

ログイン後に、外部サービスからユーザー情報を取得し、アプリケーションで使用できるようにします。

[Authorize]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        var claims = User.Claims.Select(c => new
        {
            c.Type,
            c.Value
        });

        return View(claims);
    }
}

これにより、ユーザーは外部サービスを使用して認証し、その情報をアプリケーション内で利用できるようになります。

多要素認証の実装

多要素認証(MFA)は、セキュリティを強化するために複数の認証要素を使用する方法です。これにより、不正アクセスのリスクを大幅に低減できます。ここでは、ASP.NET Core Identityを使用した多要素認証の実装方法を説明します。

MFAの設定

まず、ASP.NET Core IdentityにMFAを設定します。以下のコードを使用して、ユーザーに電話番号や電子メールなどの追加情報を求める設定を行います。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddDefaultIdentity<ApplicationUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = true;
        options.Tokens.AuthenticatorTokenProvider = TokenOptions.DefaultAuthenticatorProvider;
        options.Lockout.AllowedForNewUsers = true;
        options.Lockout.MaxFailedAccessAttempts = 5;
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
    })
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

    services.AddControllersWithViews();
}

MFAの登録

ユーザーが多要素認証を有効にするために、電話番号や認証アプリケーションを登録するプロセスを作成します。

public class ManageController : Controller
{
    private readonly UserManager<ApplicationUser> _userManager;

    public ManageController(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

    [HttpGet]
    public async Task<IActionResult> EnableMfa()
    {
        var user = await _userManager.GetUserAsync(User);
        var token = await _userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider);
        // Send the token to the user's phone number

        return View();
    }

    [HttpPost]
    public async Task<IActionResult> VerifyMfa(string token)
    {
        var user = await _userManager.GetUserAsync(User);
        var isTokenValid = await _userManager.VerifyTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider, token);

        if (isTokenValid)
        {
            user.TwoFactorEnabled = true;
            await _userManager.UpdateAsync(user);
            return RedirectToAction("Index", "Home");
        }

        return View();
    }
}

MFAの使用

ユーザーがログインする際に、MFAを使用するプロセスを作成します。

public class AccountController : Controller
{
    private readonly SignInManager<ApplicationUser> _signInManager;

    public AccountController(SignInManager<ApplicationUser> signInManager)
    {
        _signInManager = signInManager;
    }

    [HttpPost]
    public async Task<IActionResult> Login(LoginViewModel model)
    {
        if (ModelState.IsValid)
        {
            var user = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
            if (user.RequiresTwoFactor)
            {
                return RedirectToAction("VerifyMfa");
            }
            else if (user.Succeeded)
            {
                return RedirectToAction("Index", "Home");
            }
        }
        return View(model);
    }

    [HttpGet]
    public IActionResult VerifyMfa()
    {
        return View();
    }

    [HttpPost]
    public async Task<IActionResult> VerifyMfa(string token)
    {
        var result = await _signInManager.TwoFactorSignInAsync(TokenOptions.DefaultPhoneProvider, token, isPersistent: false, rememberClient: false);
        if (result.Succeeded)
        {
            return RedirectToAction("Index", "Home");
        }
        return View();
    }
}

このコードにより、ユーザーがログイン時に二要素認証を使用することができます。

認証と認可のテスト

実装した認証と認可機能が正しく動作することを確認するために、適切なテストを行うことが重要です。ここでは、単体テストと統合テストを用いて、これらの機能をテストする方法を説明します。

単体テストの実装

単体テストでは、個々のメソッドや機能が期待通りに動作するかを確認します。以下のコードは、ユーザー管理機能の単体テストの例です。

public class UserManagerTests
{
    private readonly UserManager<ApplicationUser> _userManager;

    public UserManagerTests()
    {
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseInMemoryDatabase(databaseName: "TestDatabase")
            .Options;
        var context = new ApplicationDbContext(options);
        _userManager = new UserManager<ApplicationUser>(
            new UserStore<ApplicationUser>(context),
            null, null, null, null, null, null, null, null
        );
    }

    [Fact]
    public async Task CreateUser_ShouldSucceed()
    {
        var user = new ApplicationUser { UserName = "test@example.com", Email = "test@example.com" };
        var result = await _userManager.CreateAsync(user, "Test@123");

        Assert.True(result.Succeeded);
    }
}

このテストでは、ユーザーの作成が成功することを確認しています。

統合テストの実装

統合テストでは、アプリケーション全体の機能が連携して正しく動作するかを確認します。以下のコードは、認証と認可の統合テストの例です。

public class AuthenticationTests : IClassFixture<WebApplicationFactory<Startup>>
{
    private readonly HttpClient _client;

    public AuthenticationTests(WebApplicationFactory<Startup> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task Login_ShouldReturnSuccess()
    {
        var loginModel = new { Email = "test@example.com", Password = "Test@123" };
        var response = await _client.PostAsJsonAsync("/Account/Login", loginModel);

        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        Assert.Contains("Login successful", content);
    }

    [Fact]
    public async Task AccessProtectedResource_WithoutAuthentication_ShouldReturnUnauthorized()
    {
        var response = await _client.GetAsync("/ProtectedResource");

        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
    }

    [Fact]
    public async Task AccessProtectedResource_WithAuthentication_ShouldReturnSuccess()
    {
        var loginModel = new { Email = "test@example.com", Password = "Test@123" };
        var loginResponse = await _client.PostAsJsonAsync("/Account/Login", loginModel);
        loginResponse.EnsureSuccessStatusCode();

        var response = await _client.GetAsync("/ProtectedResource");

        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        Assert.Contains("This is a protected resource", content);
    }
}

このテストでは、ログイン機能と保護されたリソースへのアクセスをテストしています。

応用例: エンタープライズアプリケーションへの適用

認証と認可は、エンタープライズアプリケーションにおいて特に重要です。ここでは、企業向けアプリケーションにおける認証と認可の応用例を紹介します。

シングルサインオン(SSO)の実装

シングルサインオン(SSO)は、ユーザーが一度のログインで複数のアプリケーションにアクセスできるようにする機能です。これにより、ユーザーエクスペリエンスが向上し、管理コストが削減されます。ASP.NET Coreでは、OAuth 2.0やOpenID Connectを使用してSSOを実装できます。

Azure Active Directoryを利用したSSO

Azure Active Directory(Azure AD)を利用することで、Microsoft 365や他のAzureサービスと統合されたSSOを実現できます。以下は、その設定例です。

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"));

    services.AddControllersWithViews();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // 省略...

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

この設定により、Azure ADを使用したSSOが可能になります。

役割ベースのアクセス制御(RBAC)の適用

エンタープライズアプリケーションでは、異なるユーザーに対して異なるアクセス権を設定する必要があります。RBACを使用することで、管理者、従業員、ゲストなどの役割に基づいてアクセス権を管理できます。

RBACの実装例

以下のコードは、管理者のみがアクセスできる管理ページを設定する例です。

[Authorize(Roles = "Admin")]
public class AdminController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

このコードにより、「Admin」役割を持つユーザーのみが管理ページにアクセスできます。

カスタムポリシーの実装

エンタープライズアプリケーションでは、より細かいアクセス制御が必要な場合があります。カスタムポリシーを使用することで、特定の条件に基づいたアクセス制御が可能になります。

カスタムポリシーの例

以下のコードは、特定の部門に所属するユーザーのみがアクセスできるリソースを設定する例です。

services.AddAuthorization(options =>
{
    options.AddPolicy("HRDepartmentOnly", policy =>
        policy.RequireClaim("Department", "HR"));
});

[Authorize(Policy = "HRDepartmentOnly")]
public class HRController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

このコードにより、「Department」クレームが「HR」のユーザーのみがHR部門のページにアクセスできます。

まとめ

本記事では、C#およびASP.NET Coreを用いたユーザー認証と認可の実装方法について詳しく解説しました。基本概念から始まり、具体的な実装方法、トークンベース認証、OAuth 2.0とOpenID Connect、多要素認証、さらにはエンタープライズアプリケーションへの応用例まで網羅しました。これらの知識を活用することで、より安全で信頼性の高いアプリケーションを構築できるでしょう。ぜひ実際のプロジェクトで試してみてください。

コメント

コメントする

目次