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