ログインを実装する:Mac+.NET 5+Identity+JWT+VS Code

ログインを実装する:Mac+.NET 5+Identity+JWT+VS Code

以前学習したログインの小さなサンプルを共有します。

最終更新 2021/10/18 16:51
Jarry
読了目安 8 分
カテゴリ
ASP.NET Core
タグ
.NET C# ASP.NET Core Web API 認証

以前学習したログインの小サンプルを共有します。コードに不十分な点があれば、ぜひご指摘ください!

ツール: VS Code とそのプラグインを使用して開発。軽量でありながらコマンドライン入力を減らせます。VS との競合はありません。

一、プラグインで WebApi プロジェクトを作成

原文は動画です。原文をクリックして確認できます

二、プラグインを利用してプロジェクトに必要な NuGet パッケージをダウンロード

三、コード作成

① 新しい User エンティティ

/// <summary>
/// ログインユーザーエンティティクラス。IdentiyフレームワークのIdentityUserクラスを継承
/// </summary>
public class AppUser:IdentityUser
{
    // さらに3つのフィールドを拡張
    public DateTime DateCreated { get; set; }
    public DateTime DateModified { get; set; }

    public string FullName { get; set; }
}

② 新しいコンテキストクラス

public class AppDBContext : IdentityDbContext<AppUser, IdentityRole, string>
{
    public AppDBContext(DbContextOptions options) : base(options)
    {
    }
}

③ Startup でコンテキストクラスを依存関係注入

services.AddDbContext<AppDBContext>(options =>
{
    options.UseMySql(Configuration.GetConnectionString("DefaultConnection"), MySqlServerVersion.LatestSupportedServerVersion);
});
// AddEntityFrameworkStores はユーザーとパスワード間のサービスを作成するために使用
services.AddIdentity<AppUser, IdentityRole>(opt => { }).AddEntityFrameworkStores<AppDBContext>();

④ ターミナルで CodeFirst を使ってデータテーブルを生成

dotnet ef migrations add init
dotnet ef  database update

⑤ JWT の設定

ConfigureServices メソッド内でサービスの設定

services.AddAuthentication(x =>
{
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
    .AddJwtBearer(options =>
    {
        // JWT のキーは複雑に設定する必要がある
        var key = Encoding.ASCII.GetBytes(Configuration["JWTConfig:Key"]);
        var issure = Configuration["JWTConfig:Issuer"];   // 発行者
        var audience = Configuration["JWTConfig:Audience"];  // 対象者
        options.TokenValidationParameters = new TokenValidationParameters()
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = true, // true に設定する場合、ValidIssure プロパティも設定する必要がある。そうしないと JWT 検証が通過しない
            ValidateAudience = true, // 同上、ValidAudience プロパティも設定する必要がある
            RequireExpirationTime = true,
            ValidateLifetime=true,   //  トークン有効期限のバッファ時間はデフォルトで5分。有効期限にはこの5分のバッファを加算する必要がある
            //  上記 ValidateIssuer を false に設定する場合、以下の2つのプロパティは不要
            ValidIssuer = issure,
            ValidAudience = audience,

        };
    });
// 複数ロールの場合、このように設定できる [Authorize(Policy ="PolicyGroup")] アクションメソッドでは簡略化可能
services.AddAuthorization(options =>
{
    options.AddPolicy("PolicyGroup", policy => policy.RequireRole("Admin", "User"));
});

Configure メソッド内でサービスの使用

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseSwagger();
        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "SwaggerDemo v1"));
    }

    app.UseHttpsRedirection();
    app.UseCors("any");
    app.UseRouting();
    // 順序に注意
    app.UseAuthentication();
    app.UseAuthorization();

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

⑥ Swagger の設定

ConfigureServices メソッド内でサービスの設定

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "SwaggerDemo", Version = "v1", Description = "Demo API for showing Swagger" });

    // 以下の2ステップで Swagger 上に「鍵」を表示
    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,  // ヘッダーに配置
        Description = "ここにトークンを直接入力してください。Bearer とスペースの形式は不要です。",
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        BearerFormat = "JWT",
        Scheme = "bearer"
    });
    c.AddSecurityRequirement(new OpenApiSecurityRequirement{
        {
            new OpenApiSecurityScheme{
                Reference=new OpenApiReference{
                    Type=ReferenceType.SecurityScheme,
                    Id="Bearer"
                }
            },Array.Empty<string>()
        }
    });

    // Swagger インターフェースコメントの表示
    // 注意: vscode ユーザーはプロジェクトの csproj ファイルでコメントドキュメントを生成するプロパティを手動設定する必要がある
    // 詳細はプロジェクトファイルの PropertyGroup を参照
    var fileName = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var filePath = Path.Combine(AppContext.BaseDirectory, fileName);
    c.IncludeXmlComments(filePath);
});

Configure メソッド内でサービスの使用

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        // Swagger ミドルウェアの使用
        app.UseSwagger();
        // ここでの v1 は上記の c.SwaggerDoc("v1") と一致する必要がある
        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "SwaggerDemo v1"));
    }

    app.UseHttpsRedirection();
    app.UseCors("any");
    app.UseRouting();
    // 順序に注意
    app.UseAuthentication();
    app.UseAuthorization();

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

⑦ UserController を作成し、コンストラクタでログインサービスを注入

private readonly UserManager<AppUser> _userManger;  // ユーザーサービス
private readonly SignInManager<AppUser> _signInManger;  // ログインサービス

private readonly RoleManager<IdentityRole> _roleManger; // ロールサービス
private readonly JWTConfig _jwtConfig;  // 設定フレームワークで設定ファイルをエンティティクラスに注入
public UserController(ILogger<UserController> logger, UserManager<AppUser> userManager,
        SignInManager<AppUser> signInManager, IOptions<JWTConfig> jwtConfig, RoleManager<IdentityRole> roleManger)
{
    this._logger = logger;
    this._userManger = userManager;
    this._signInManger = signInManager;
    this._jwtConfig = jwtConfig.Value;
    this._roleManger = roleManger;
}

ユーザー登録

/// <summary>
/// ユーザー登録
/// AddAndUpdateUserrRegisterModel は DTO で受け取りオブジェクト
/// AllowAnonymous は権限検証が不要
/// 作成者 xxxx
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("RegisterUser")]
public async Task<Object> RegisterUser(AddAndUpdateUserrRegisterModel model)
{
    try
    {
        // 登録時にロールが含まれているか確認
        if (model.Roles is null || model.Roles.Count <= 0)
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "ロールは空にできません"));
        }
        // ユーザーが登録するロールが存在するかループで確認。ロール作成メソッド AddRole()
        foreach (var item in model.Roles)
        {
            if (!await _roleManger.RoleExistsAsync(item))
            {
                return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "作成対象のロールが存在しません"));
            }
        }
        // ユーザークラスを生成
        var user = new AppUser()
        {
            UserName = model.Email,
            FullName = model.FullName,
            Email = model.Email,
            DateCreated = DateTime.Now,
            DateModified = DateTime.UtcNow
        };
        // ユーザー登録
        var result = await _userManger.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            // 登録成功後、一時的に作成したユーザーを取得
            var tempUser = await _userManger.FindByEmailAsync(model.Email);
            // 作成したユーザーにロールを追加
            foreach (var role in model.Roles)
            {
                await _userManger.AddToRoleAsync(tempUser, role); // ロール追加
            }
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Ok, "ユーザーは正常に登録されました!", null));
        }
        // ユーザー作成失敗時の返却
        return await Task.FromResult(string.Join(",", result.Errors.Select(x => x.Description).ToArray()));
    }
    catch (System.Exception ex)
    {
        // 例外時の返却
        return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, ex.Message, null));
    }
}

ログイン

/// <summary>
/// トークン生成
/// 作成者 xxxx
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
private string GenarateToken(AppUser user, List<string> roles)
{
    var jwtTokenHandle = new JwtSecurityTokenHandler();
    var key = Encoding.ASCII.GetBytes(_jwtConfig.Key);

    // Subject の設定
    var claims = new List<Claim>()
    {
        new Claim(JwtRegisteredClaimNames.NameId,user.Id),
        new Claim(JwtRegisteredClaimNames.Email,user.Email),
        new Claim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString())
    };
    foreach (var role in roles)
    {
        claims.Add(new Claim(ClaimTypes.Role,role));
    }
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        // 複数ロール
        Subject=new ClaimsIdentity(claims),

        // 単一ロール
        // Subject = new ClaimsIdentity(new[]
        // {
        //     new System.Security.Claims.Claim(JwtRegisteredClaimNames.NameId,user.Id),
        //     new System.Security.Claims.Claim(JwtRegisteredClaimNames.Email,user.Email),
        //     new System.Security.Claims.Claim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString())
        //     // new System.Security.Claims.Claim(ClaimTypes.Role,"role")
        // }),

        // 有効期限 12時間
        Expires = DateTime.UtcNow.AddSeconds(6),
        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
        Audience = _jwtConfig.Audience,  // ここで設定しないと UnAuthorized が返る
        Issuer = _jwtConfig.Issuer // 同上
    };
    // トークン作成
    var token = jwtTokenHandle.CreateToken(tokenDescriptor);
    return jwtTokenHandle.WriteToken(token);
}
/// <summary>
/// ユーザーログイン
/// LoginModel はログイン用DTO
/// 作成者 xxxx
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost]
public async Task<object> Login(LoginModel model)
{
    try
    {
        if (!ModelState.IsValid)
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "パラメータが無効です!", null));
        }
        var result = await _signInManger.PasswordSignInAsync(model.UserName, model.Password, false, false);
        if (result.Succeeded)
        {
            // 成功したらユーザーを取得
            var appUser = await _userManger.FindByNameAsync(model.UserName);
            var roles = (await _userManger.GetRolesAsync(appUser)).ToList();
            // await _userManger.GetRolesAsync(appUser);
            var user = new UserDto(appUser.FullName, appUser.Email, appUser.UserName, appUser.DateCreated, roles)
            {
                // トークン生成
                Token = GenarateToken(appUser,roles)
            };
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Ok, "ログイン成功", user));
        }
        else
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "ログイン失敗", null));
        }

    }
    catch (System.Exception ex)
    {
        return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, ex.Message, null));
    }
}

ロール追加

まず、上記のユーザーログインで生成されたトークンを Swagger に設定し、Admin ロールのみアクセス可能なインターフェースにアクセスします。

/// <summary>
/// ロール追加
/// [Authorize(Roles ="Admin")]  Admin ロールのユーザーのみアクセス可能
/// 作成者 xxx
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[Authorize(Roles ="Admin")]
[HttpPost("AddRole")]
public async Task<object> AddRole(AddRoleModel model)
{
    try
    {
        if (model is null || string.IsNullOrWhiteSpace(model.Role))
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "ロールは空にできません"));
        }
        // 【AspNetRoles】テーブル内でロールが存在するか確認
        if (await _roleManger.RoleExistsAsync(model.Role))
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Ok, "ロールは既に存在します"));
        }
        var role = new IdentityRole()
        {
            Name = model.Role,
        };
        // ロール作成
        var result = await _roleManger.CreateAsync(role);
        if (result.Succeeded)
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Ok, "ロール作成成功!"));
        }
        else
        {
            return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error, "ロール作成失敗!"));
        }

    }
    catch (System.Exception)
    {
        return await Task.FromResult(new ResponseModel(Enums.ResponseCode.Error));
    }

}

トークン権限の検証に失敗した場合の対処方法は?ここで ASP.NET Core 5.0 に新たに追加されたインターフェース【IAuthorizationMiddlewareResultHandler】を使用して権限検証を処理できます。以下のコードをご覧ください。

/// <summary>
/// これは ASP.NET Core 5 で新たに追加された認証失敗の処理方法です。リクエストコンテキストを直接公開できるので、非常に便利になりました!!!
/// 作成者 xxx
/// </summary>
public class AuthorizationHandleMiddleWare : IAuthorizationMiddlewareResultHandler
{
    private readonly AuthorizationMiddlewareResultHandler authorizationHandleMiddleWare =new();
    public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
    {
        // トークンが無効または存在しない場合、authorizeResult.Challenged が true になる
        if(authorizeResult.Challenged)
        {
            // todo コンテキストから user オブジェクトを取得した後、ここで token を確認し、トークンが期限切れかどうかを区別できる
            var a=context.Request.Headers["Authorization"];
            context.Response.StatusCode=(int)HttpStatusCode.OK;
            await context.Response.WriteAsJsonAsync(new ResponseModel(Enums.ResponseCode.UnAuthorized,"認証されていません。トークンが有効か確認してください!"));
            return ;
        }
        // トークン検証は通過したが、アクセスするリソースに権限がない場合、authorizeResult.Forbidden が true になる
        if(authorizeResult.Forbidden)
        {
            context.Response.StatusCode=(int)HttpStatusCode.OK;
            await context.Response.WriteAsJsonAsync(new ResponseModel(Enums.ResponseCode.ForBidden,"このリソースにアクセスする権限がありません!"));
            return ;
        }
        await authorizationHandleMiddleWare.HandleAsync(next,context,policy,authorizeResult);
    }
}

さらに、ConfigureService でサービスを登録する必要があります。

// .NET 5 で新たに追加された権限検証ミドルウェア。ここで依存関係注入する。詳細は AuthorizationHandleMiddleWare.cs ファイルを参照
services.AddSingleton<IAuthorizationMiddlewareResultHandler,AuthorizationHandleMiddleWare>();

以上がログインの簡単なデモです。詳細なコードは Gitee をご参照ください:https://gitee.com/holyace/together/tree/JarryGu_develop/framework/JwtLoginDemo

さらに探索

関連読書

その他の記事
同じカテゴリ / 同じタグ 2022/06/22

ASP.NET Core WebAPI でローカリゼーションを実装する(単一リソースファイル)

Microsoft のデフォルトは、1 つのクラスに複数のリソースファイルを対応させる方法であり、使用がやや面倒です。本記事では、単一リソースファイルの使用方法を紹介します。つまり、プロジェクト全体のすべてのクラスが 1 セットの多言語リソースファイルに対応します。

続きを読む