八、实现登录页面 :Blazor Server + MudBlazor + Asp.Net Core Web Api + MongoDB + Asp.Net Core Identity身份验证
在Pages文件夹中新增登录页面Login.razor:新增登录页面对应的cs文件,Login.razor.cs:stringstringobjectstringstring_Imports.razor中加入组件,但Login.razor.cs里的组件仍然显示错误:找不到该变量;!!!注意:以上代码需要加到app.UseRouting();后面。
一、本教程实现效果

二、Blazor Server应用代码
-
新增用于验证的包 Blazored.FluentValidation和本地存储服务包Blazored.LocalStorage,TeachingBlazorApp.csproj代码:
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <PackageReference Include="Blazored.FluentValidation" Version="2.1.0" /> <PackageReference Include="MudBlazor" Version="6.11.1" /> <PackageReference Include="Blazored.LocalStorage" Version="4.4.0"/> </ItemGroup> </Project> -
在Pages文件夹中新增登录页面Login.razor:
Login.razor
@page "/login" @using TeachingBlazorApp.Shared @layout MainLayout @using Microsoft.AspNetCore.Authorization @using TeachingBlazorApp.Static.Icons @using Microsoft.AspNetCore.Components.Forms @attribute [AllowAnonymous] @inject Microsoft.Extensions.Localization.IStringLocalizer<Login> _localizer <PageTitle>Login</PageTitle> <EditForm Model="@_tokenModel" OnValidSubmit="SubmitAsync"> <Blazored.FluentValidation.FluentValidationValidator @ref="_fluentValidationValidator" /> <MudGrid> <MudItem xs="12"> <div class="d-flex justify-center"> <MudIcon Icon="@TeachingBlazorApp.Static.Icons.CustomIcons.TeachingBlazorLogo" ViewBox="0 0 510 510" Style="@($"color:{TeachingBlazorApp.Static.Colors.CustomColors.LogoColor};")" class="login-logo" /> </div> </MudItem> <MudItem xs="12"> <div class="d-flex justify-center"> <MudText Typo="Typo.h4">@_localizer["Sign In"]</MudText> </div> </MudItem> <MudItem xs="12"> <div class="d-flex justify-center"> <MudText>@_localizer["Description"]</MudText> </div> </MudItem> <MudItem xs="12"> <div class="d-flex justify-center"> <MudText>@_localizer["Don't have an account?"] <MudLink Href="/register">@_localizer["Register here"] </MudLink> </MudText> </div> </MudItem> <MudItem xs="12"> <MudTextField T="string" Label="@_localizer["E-mail"]" @bind-Value="ReadOnlyEmail" ReadOnly="true" Variant="Variant.Outlined" /> </MudItem> <MudItem xs="12"> <MudTextField Label="@_localizer["Password"]" Variant="Variant.Outlined" @bind-Value="ReadOnlyPWD" ReadOnly="true" InputType="@_passwordInput" Adornment="Adornment.End" AdornmentIcon="@_passwordInputIcon" OnAdornmentClick="TogglePasswordVisibility" /> </MudItem> <MudItem xs="12" Class="d-flex justify-space-between align-center"> <MudCheckBox T="bool" Label="@_localizer["Remember me?"]" Color="Color.Dark" Class="ml-n1"></MudCheckBox> <MudLink Href="/account/forgot-password">@_localizer["Forgot password?"]</MudLink> </MudItem> <MudItem xs="12" Class="d-flex justify-center"> <MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Disabled="@(!Validated)" Color="@TeachingBlazorApp.Static.Colors.CustomColors.ButtonColor" Size="Size.Large" Style="width: 100%;"> @_localizer["Sign In"]</MudButton> </MudItem> </MudGrid> </EditForm> <style> .login-logo { width: 100px; height: 100px; } </style> -
新增登录页面对应的cs文件,Login.razor.cs:
Login.razor.cs
using Blazored.FluentValidation; using Microsoft.AspNetCore.Components.Authorization; using MudBlazor; using System.Security.Claims; using System.Text.RegularExpressions; using TeachingBlazorApp.Infrastructure.Models.Requests.Identity; namespace TeachingBlazorApp.Pages.Authentication { public partial class Login { // Refer to BlazorHero private FluentValidationValidator? _fluentValidationValidator; private bool Validated => _fluentValidationValidator.Validate(options => { options.IncludeAllRuleSets(); }); private TokenRequest _tokenModel = new(); private bool _passwordVisibility; private InputType _passwordInput = InputType.Password; private string _passwordInputIcon = Icons.Material.Filled.VisibilityOff; public string ReadOnlyEmail { get; set; } = "SuperUser@163.com"; public string ReadOnlyPWD { get; set; } = "123Pa$$word!"; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { var state = await _stateProvider.GetAuthenticationStateAsync(); var authenticationState = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); if (state.User.Identity.Name != new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())).User.Identity.Name) { _navigationManager.NavigateTo("/home"); } } } private async Task SubmitAsync() { _tokenModel.Email = ReadOnlyEmail; _tokenModel.Password = ReadOnlyPWD; var result = await _authenticationManager.Login(_tokenModel); if (!result.Succeeded) { foreach (var message in result.Messages) { _snackBar.Add(message, Severity.Error); } } } void TogglePasswordVisibility() { if (_passwordVisibility) { _passwordVisibility = false; _passwordInputIcon = Icons.Material.Filled.VisibilityOff; _passwordInput = InputType.Password; } else { _passwordVisibility = true; _passwordInputIcon = Icons.Material.Filled.Visibility; _passwordInput = InputType.Text; } } } } -
新增权限常量类ApplicationClaimTypes:
namespace TeachingBlazorApp.Infrastructure.Constants.Permission { public static class ApplicationClaimTypes { public const string Permission = "Permission"; } } -
新增存储常量类StorageConstants:
namespace TeachingBlazorApp.Infrastructure.Constants.Storage { public static class StorageConstants { public static class Local { public static string Preference = "clientPreference"; public static string AuthToken = "authToken"; public static string RefreshToken = "refreshToken"; public static string UserImageURL = "userImageURL"; } public static class Server { public static string Preference = "serverPreference"; //TODO - add } } } -
新增用户状态服务TeachingBlazorStateProvider:
using Blazored.LocalStorage; using Microsoft.AspNetCore.Components.Authorization; using System.Diagnostics; using System.Net.Http.Headers; using System.Security.Claims; using System.Text.Json; using TeachingBlazorApp.Infrastructure.Constants.Permission; using TeachingBlazorApp.Infrastructure.Constants.Storage; namespace TeachingBlazorApp.Infrastructure.Managers.Identity.Authentication { public class TeachingBlazorStateProvider : AuthenticationStateProvider { private readonly HttpClient _httpClient; private readonly ILocalStorageService _localStorage; public TeachingBlazorStateProvider( HttpClient httpClient, ILocalStorageService localStorage) { _httpClient = httpClient; _localStorage = localStorage; } public async Task StateChangedAsync() { var authState = Task.FromResult(await GetAuthenticationStateAsync()); NotifyAuthenticationStateChanged(authState); } public void MarkUserAsLoggedOut() { var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity()); var authState = Task.FromResult(new AuthenticationState(anonymousUser)); NotifyAuthenticationStateChanged(authState); } public async Task<ClaimsPrincipal> GetAuthenticationStateProviderUserAsync() { var state = await this.GetAuthenticationStateAsync(); var authenticationStateProviderUser = state.User; return authenticationStateProviderUser; } public ClaimsPrincipal AuthenticationStateUser { get; set; } public override async Task<AuthenticationState> GetAuthenticationStateAsync() { try { var savedToken = await _localStorage.GetItemAsync<string>(StorageConstants.Local.AuthToken); if (string.IsNullOrWhiteSpace(savedToken)) { return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", savedToken); var state = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(GetClaimsFromJwt(savedToken), "jwt"))); AuthenticationStateUser = state.User; return state; } catch (InvalidOperationException e) { Debug.WriteLine("Local Storage cannot get savedToken: " + e); return null; } } private IEnumerable<Claim> GetClaimsFromJwt(string jwt) { var claims = new List<Claim>(); var payload = jwt.Split('.')[1]; var jsonBytes = ParseBase64WithoutPadding(payload); var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes); if (keyValuePairs != null) { keyValuePairs.TryGetValue(ClaimTypes.Role, out var roles); if (roles != null) { if (roles.ToString().Trim().StartsWith("[")) { var parsedRoles = JsonSerializer.Deserialize<string[]>(roles.ToString()); claims.AddRange(parsedRoles.Select(role => new Claim(ClaimTypes.Role, role))); } else { claims.Add(new Claim(ClaimTypes.Role, roles.ToString())); } keyValuePairs.Remove(ClaimTypes.Role); } keyValuePairs.TryGetValue(ApplicationClaimTypes.Permission, out var permissions); if (permissions != null) { if (permissions.ToString().Trim().StartsWith("[")) { var parsedPermissions = JsonSerializer.Deserialize<string[]>(permissions.ToString()); claims.AddRange(parsedPermissions.Select(permission => new Claim(ApplicationClaimTypes.Permission, permission))); } else { claims.Add(new Claim(ApplicationClaimTypes.Permission, permissions.ToString())); } keyValuePairs.Remove(ApplicationClaimTypes.Permission); } claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()))); } return claims; } private byte[] ParseBase64WithoutPadding(string payload) { payload = payload.Trim().Replace('-', '+').Replace('_', '/'); var base64 = payload.PadRight(payload.Length + (4 - payload.Length % 4) % 4, '='); return Convert.FromBase64String(base64); } } } -
在_Imports.razor中加入用户状态服务,本地存储服务和导航管理服务:
@using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.JSInterop @using MudBlazor @using TeachingBlazorApp @using TeachingBlazorApp.Infrastructure.Managers @using TeachingBlazorApp.Infrastructure.Managers.Identity.Authentication @inject HealthCheckManager _healthCheckManager; @inject ISnackbar _snackBar; @inject IAuthenticationManager _authenticationManager; @inject TeachingBlazorStateProvider _stateProvider; @inject NavigationManager _navigationManager; -
新增用于登录的实体类,TokenRequest.cs和TokenResponse.cs:
using System.ComponentModel.DataAnnotations; namespace TeachingBlazorApp.Infrastructure.Models.Requests.Identity { public class TokenRequest { [Required] public string Email { get; set; } [Required] public string Password { get; set; } } }using System; namespace TeachingBlazorApp.Infrastructure.Models.Responses.Identity { public class TokenResponse { public string Token { get; set; } public string RefreshToken { get; set; } public string UserImageURL { get; set; } public DateTime RefreshTokenExpiryTime { get; set; } } } -
新增路由类TokenEndpoints.cs:
namespace TeachingBlazorApp.Infrastructure.Routes { public static class TokenEndpoints { public static string Get = "api/identity/token"; public static string Refresh = "api/identity/token/refresh"; } } -
新增包装类接口IResult.cs和Result.cs:
using System.Collections.Generic; namespace TeachingBlazorApp.Infrastructure.Utils.Wrapper { public interface IResult { List<string> Messages { get; set; } bool Succeeded { get; set; } } public interface IResult<out T> : IResult { T Data { get; } } }using System.Collections.Generic; using System.Threading.Tasks; namespace TeachingBlazorApp.Infrastructure.Utils.Wrapper { public class Result : IResult { public Result() { } public List<string> Messages { get; set; } = new List<string>(); public bool Succeeded { get; set; } public static IResult Fail() { return new Result { Succeeded = false }; } public static IResult Fail(string message) { return new Result { Succeeded = false, Messages = new List<string> { message } }; } public static IResult Fail(List<string> messages) { return new Result { Succeeded = false, Messages = messages }; } public static Task<IResult> FailAsync() { return Task.FromResult(Fail()); } public static Task<IResult> FailAsync(string message) { return Task.FromResult(Fail(message)); } public static Task<IResult> FailAsync(List<string> messages) { return Task.FromResult(Fail(messages)); } public static IResult Success() { return new Result { Succeeded = true }; } public static IResult Success(string message) { return new Result { Succeeded = true, Messages = new List<string> { message } }; } public static Task<IResult> SuccessAsync() { return Task.FromResult(Success()); } public static Task<IResult> SuccessAsync(string message) { return Task.FromResult(Success(message)); } } public class Result<T> : Result, IResult<T> { public Result() { } public T Data { get; set; } public new static Result<T> Fail() { return new Result<T> { Succeeded = false }; } public new static Result<T> Fail(string message) { return new Result<T> { Succeeded = false, Messages = new List<string> { message } }; } public new static Result<T> Fail(List<string> messages) { return new Result<T> { Succeeded = false, Messages = messages }; } public new static Task<Result<T>> FailAsync() { return Task.FromResult(Fail()); } public new static Task<Result<T>> FailAsync(string message) { return Task.FromResult(Fail(message)); } public new static Task<Result<T>> FailAsync(List<string> messages) { return Task.FromResult(Fail(messages)); } public new static Result<T> Success() { return new Result<T> { Succeeded = true }; } public new static Result<T> Success(string message) { return new Result<T> { Succeeded = true, Messages = new List<string> { message } }; } public static Result<T> Success(T data) { return new Result<T> { Succeeded = true, Data = data }; } public static Result<T> Success(T data, string message) { return new Result<T> { Succeeded = true, Data = data, Messages = new List<string> { message } }; } public static Result<T> Success(T data, List<string> messages) { return new Result<T> { Succeeded = true, Data = data, Messages = messages }; } public new static Task<Result<T>> SuccessAsync() { return Task.FromResult(Success()); } public new static Task<Result<T>> SuccessAsync(string message) { return Task.FromResult(Success(message)); } public static Task<Result<T>> SuccessAsync(T data) { return Task.FromResult(Success(data)); } public static Task<Result<T>> SuccessAsync(T data, string message) { return Task.FromResult(Success(data, message)); } } } -
新增分页包装类PaginatedResult.cs:
using System; using System.Collections.Generic; namespace TeachingBlazorApp.Infrastructure.Utils.Wrapper { public class PaginatedResult<T> : Result { public PaginatedResult(List<T> data) { Data = data; } public List<T> Data { get; set; } internal PaginatedResult(bool succeeded, List<T> data = default, List<string> messages = null, int count = 0, int page = 1, int pageSize = 10) { Data = data; CurrentPage = page; Succeeded = succeeded; PageSize = pageSize; TotalPages = (int)Math.Ceiling(count / (double)pageSize); TotalCount = count; } public static PaginatedResult<T> Failure(List<string> messages) { return new PaginatedResult<T>(false, default, messages); } public static PaginatedResult<T> Success(List<T> data, int count, int page, int pageSize) { return new PaginatedResult<T>(true, data, null, count, page, pageSize); } public int CurrentPage { get; set; } public int TotalPages { get; set; } public int TotalCount { get; set; } public int PageSize { get; set; } public bool HasPreviousPage => CurrentPage > 1; public bool HasNextPage => CurrentPage < TotalPages; } } -
新增包装拓展类ResultExtensions.cs:
using System.Diagnostics; using System.Net.Http; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using TeachingBlazorApp.Infrastructure.Utils.Wrapper; using IResult = TeachingBlazorApp.Infrastructure.Utils.Wrapper.IResult; namespace TeachingBlazorApp.Infrastructure.Extensions { internal static class ResultExtensions { /// <summary> /// Process http response to return object /// </summary> /// <param name="response"></param> /// <typeparam name="T"></typeparam> /// <returns>Object</returns> internal static async Task<IResult<T>> ToResult<T>(this HttpResponseMessage response) { try { var responseAsString = await response.Content.ReadAsStringAsync(); var responseObject = JsonSerializer.Deserialize<Result<T>>(responseAsString, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, ReferenceHandler = ReferenceHandler.Preserve }); return responseObject; } catch (Exception e) { Debug.WriteLine("ResultExtensions ToResult e = " + e); } return null; } internal static async Task<IResult> ToResult(this HttpResponseMessage response) { var responseAsString = await response.Content.ReadAsStringAsync(); var responseObject = JsonSerializer.Deserialize<Result>(responseAsString, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, ReferenceHandler = ReferenceHandler.Preserve }); return responseObject; } internal static async Task<PaginatedResult<T>> ToPaginatedResult<T>(this HttpResponseMessage response) { var responseAsString = await response.Content.ReadAsStringAsync(); var responseObject = JsonSerializer.Deserialize<PaginatedResult<T>>(responseAsString, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return responseObject; } } } -
新增验证管理类AuthenticateManager.cs,用于发送用户登录请求和处理Asp.Net Web Api应用返回的报文:
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.Localization; using System; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Security.Claims; using System.Threading.Tasks; using TeachingBlazorApp.Infrastructure.Extensions; using TeachingBlazorApp.Infrastructure.Models.Requests.Identity; using TeachingBlazorApp.Infrastructure.Models.Responses.Identity; using TeachingBlazorApp.Infrastructure.Routes; using TeachingBlazorApp.Infrastructure.Utils.Wrapper; using IResult = TeachingBlazorApp.Infrastructure.Utils.Wrapper.IResult; namespace TeachingBlazorApp.Infrastructure.Managers.Identity.Authentication { public class AuthenticationManager : IAuthenticationManager { private readonly HttpClient _httpClient; // private readonly ILocalStorageService _localStorage; private readonly AuthenticationStateProvider _authenticationStateProvider; private readonly IStringLocalizer<AuthenticationManager> _localizer; public AuthenticationManager( HttpClient httpClient, AuthenticationStateProvider authenticationStateProvider, IStringLocalizer<AuthenticationManager> localizer) { _httpClient = httpClient; _authenticationStateProvider = authenticationStateProvider; _localizer = localizer; } public async Task<IResult> Login(TokenRequest model) { var response = await _httpClient.PostAsJsonAsync(TokenEndpoints.Get, model); var result = await response.ToResult<TokenResponse>(); if (result.Succeeded) { var token = result.Data.Token; var refreshToken = result.Data.RefreshToken; var userImageURL = result.Data.UserImageURL; _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return await Result.SuccessAsync(); } else { return await Result.FailAsync(result.Messages); } } } } -
新增全局变量,MudBlazor的组件ISnackbar和验证管理类AuthenticateManager.cs,_Imports.razor代码:
@using CoolBlazor.Infrastructure.Managers.Identity.Authentication @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.JSInterop @using MudBlazor @using TeachingBlazorApp @using TeachingBlazorApp.Infrastructure.Managers @inject HealthCheckManager _healthCheckManager; @inject ISnackbar _snackBar; @inject IAuthenticationManager _authenticationManager; -
_Imports.razor中加入组件,但Login.razor.cs里的组件仍然显示错误:找不到该变量;
- 解决方法:在Program.cs里注册验证服务,MudBlazor第三方包和注册Localization,Program.cs代码:
!!!注意:
app.UseAuthentication();
app.UseAuthorization();以上代码需要加到app.UseRouting();后面
builder.Services.AddLocalization(options => { options.ResourcesPath = "Resources"; }); builder.Services.AddScoped<IAuthenticationManager, AuthenticationManager>(); // Add third party libraries builder.Services.AddMudServices(); // JWT app.UseAuthentication(); app.UseAuthorization();using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using MudBlazor.Services; using TeachingBlazorApp.Infrastructure.Managers; using TeachingBlazorApp.Infrastructure.Managers.Identity.Authentication; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); builder.Services.AddLocalization(options => { options.ResourcesPath = "Resources"; }); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration["WebApi"] ?? "http://localhost:5211") }); // Register Services builder.Services.AddScoped<HealthCheckManager>(); builder.Services.AddScoped<IAuthenticationManager, AuthenticationManager>(); // Add third party libraries builder.Services.AddMudServices(); var app = builder.Build(); if (!app.Environment.IsDevelopment()) { // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); // JWT app.UseAuthentication(); app.UseAuthorization(); app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); app.Run(); -
运行后报错解决:
InvalidOperationException: FluentValidationValidator requires a cascading parameter of type EditContext. For example, you can use FluentValidationValidator inside an EditForm.
解决方法:声明fluentValidationValidator为空;
private FluentValidationValidator? _fluentValidationValidator; -
运行后点击主页右上角的LOGIN HERE,即可看到登录页面:

三、Web Api应用代码
-
在TeachingWebApi.csproj里添加所需要的包:
<PackageReference Include="AspNetCore.Identity.Mongo" Version="8.3.3"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7"/> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7"/> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.7.0"/>目前安装的包:
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.7"/> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/> <PackageReference Include="MongoDB.Driver" Version="2.27.0"/> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7"/> <PackageReference Include="MongoDB.EntityFrameworkCore" Version="8.0.3"/> <PackageReference Include="AspNetCore.Identity.Mongo" Version="8.3.3"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7"/> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7"/> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.7.0"/> </ItemGroup> </Project> -
appSettings.json和appSettings.Development.json中加入AppConfiguration用于进行JWT加密:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "TeachingAppDatabase": { "BooksCollectionName": "books", "UsersCollectionName": "users", "RolesCollectionName": "roles", "ConnectionString": "Fill it with your MongoDB connection string", "DatabaseName": "teaching_blazor_app" }, "AppConfiguration": { "Secret": "S0M3RAN0MS3CR3T!1!MAG1C!1!123456789", "BehindSSLProxy": false, "ProxyIP": "", "ApplicationUrl": "" }, "AllowedHosts": "*" } -
新增AppConfiguration类,以存放配置文件中的AppConfiguration域的值:
namespace TeachingWebApi.Config { public class AppConfiguration { public string Secret { get; set; } public bool BehindSSLProxy { get; set; } public string ProxyIP { get; set; } public string ApplicationUrl { get; set; } } } -
新增处理用户登录的接口Controller,TokenController.cs:
using CoolWebApi.Services.Identity; using Microsoft.AspNetCore.Mvc; using TeachingWebApi.Models.Requests.Identity; namespace TeachingWebApi.Controllers.Identity { [Route("api/identity/token")] [ApiController] public class TokenController : ControllerBase { private readonly ITokenService _tokenService; public TokenController(ITokenService tokenService) { _tokenService = tokenService; } /// <summary> /// Get Token (Email, Password) /// </summary> /// <param name="model"></param> /// <returns>Status 200 OK</returns> [HttpPost] public async Task<ActionResult> Get(TokenRequest model) { var response = await _tokenService.LoginAsync(model); return Ok(response); } } } -
新增TeachingWebApi中的包装类服务IResult.cs,Result.cs和PaginatedResult.cs:
-
IResult.cs:
using System; namespace TeachingWebApi.Utils.Wrapper { public interface IResult { List<string> Messages { get; set; } bool Succeeded { get; set; } } public interface IResult<out T> : IResult { T Data { get; } } } -
Result.cs:
using System; namespace TeachingWebApi.Utils.Wrapper { public class Result : IResult { public Result() { } public List<string> Messages { get; set; } = new List<string>(); public bool Succeeded { get; set; } public static IResult Fail() { return new Result { Succeeded = false }; } public static IResult Fail(string message) { return new Result { Succeeded = false, Messages = new List<string> { message } }; } public static IResult Fail(List<string> messages) { return new Result { Succeeded = false, Messages = messages }; } public static Task<IResult> FailAsync() { return Task.FromResult(Fail()); } public static Task<IResult> FailAsync(string message) { return Task.FromResult(Fail(message)); } public static Task<IResult> FailAsync(List<string> messages) { return Task.FromResult(Fail(messages)); } public static IResult Success() { return new Result { Succeeded = true }; } public static IResult Success(string message) { return new Result { Succeeded = true, Messages = new List<string> { message } }; } public static Task<IResult> SuccessAsync() { return Task.FromResult(Success()); } public static Task<IResult> SuccessAsync(string message) { return Task.FromResult(Success(message)); } } public class Result<T> : Result, IResult<T> { public Result() { } public T Data { get; set; } public new static Result<T> Fail() { return new Result<T> { Succeeded = false }; } public new static Result<T> Fail(string message) { return new Result<T> { Succeeded = false, Messages = new List<string> { message } }; } public new static Result<T> Fail(List<string> messages) { return new Result<T> { Succeeded = false, Messages = messages }; } public new static Task<Result<T>> FailAsync() { return Task.FromResult(Fail()); } public new static Task<Result<T>> FailAsync(string message) { return Task.FromResult(Fail(message)); } public new static Task<Result<T>> FailAsync(List<string> messages) { return Task.FromResult(Fail(messages)); } public new static Result<T> Success() { return new Result<T> { Succeeded = true }; } public new static Result<T> Success(string message) { return new Result<T> { Succeeded = true, Messages = new List<string> { message } }; } public static Result<T> Success(T data) { return new Result<T> { Succeeded = true, Data = data }; } public static Result<T> Success(T data, string message) { return new Result<T> { Succeeded = true, Data = data, Messages = new List<string> { message } }; } public static Result<T> Success(T data, List<string> messages) { return new Result<T> { Succeeded = true, Data = data, Messages = messages }; } public new static Task<Result<T>> SuccessAsync() { return Task.FromResult(Success()); } public new static Task<Result<T>> SuccessAsync(string message) { return Task.FromResult(Success(message)); } public static Task<Result<T>> SuccessAsync(T data) { return Task.FromResult(Success(data)); } public static Task<Result<T>> SuccessAsync(T data, string message) { return Task.FromResult(Success(data, message)); } } } -
PaginatedResult.cs:
using System; using System.Collections.Generic; namespace TeachingWebApi.Utils.Wrapper { public class PaginatedResult<T> : Result { public PaginatedResult(List<T> data) { Data = data; } public List<T> Data { get; set; } internal PaginatedResult(bool succeeded, List<T> data = default, List<string> messages = null, int count = 0, int page = 1, int pageSize = 10) { Data = data; CurrentPage = page; Succeeded = succeeded; PageSize = pageSize; TotalPages = (int)Math.Ceiling(count / (double)pageSize); TotalCount = count; } public static PaginatedResult<T> Failure(List<string> messages) { return new PaginatedResult<T>(false, default, messages); } public static PaginatedResult<T> Success(List<T> data, int count, int page, int pageSize) { return new PaginatedResult<T>(true, data, null, count, page, pageSize); } public int CurrentPage { get; set; } public int TotalPages { get; set; } public int TotalCount { get; set; } public int PageSize { get; set; } public bool HasPreviousPage => CurrentPage > 1; public bool HasNextPage => CurrentPage < TotalPages; } }
-
-
新增TokenRequest.cs和TokenResponse.cs:
-
TokenRequest.cs:
using System; using System.ComponentModel.DataAnnotations; namespace TeachingWebApi.Models.Requests.Identity { public class TokenRequest { [Required] public string Email { get; set; } [Required] public string Password { get; set; } } } -
TokenResponse.cs:
using System; namespace TeachingWebApi.Models.Responses.Identity { public class TokenResponse { public string Token { get; set; } public string RefreshToken { get; set; } public string UserImageURL { get; set; } public DateTime RefreshTokenExpiryTime { get; set; } } }
-
-
新增ITokenService.cs和TokenService.cs:
-
ITokenService.cs:
using TeachingWebApi.Models.Requests.Identity; using TeachingWebApi.Models.Responses.Identity; using TeachingWebApi.Utils.Wrapper; namespace TeachingWebApi.Services.Identity { public interface ITokenService : IService { Task<Result<TokenResponse>> LoginAsync(TokenRequest model); } } -
TokenService.cs:
using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Localization; using TeachingWebApi.Config; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using System.Xml; using Microsoft.IdentityModel.Tokens; using TeachingWebApi.Utils.Wrapper; using TeachingWebApi.Models.Responses.Identity; using TeachingWebApi.Models.Requests.Identity; using TeachingWebApi.Models.Identity; using System.IdentityModel.Tokens.Jwt; using Microsoft.Extensions.Options; namespace TeachingWebApi.Services.Identity.impl { public class TokenService : ITokenService { private const string InvalidErrorMessage = "Invalid email or password."; private readonly UserManager<TeachingBlazorUser> _userManager; private readonly RoleManager<TeachingBlazorRole> _roleManager; private readonly SignInManager<TeachingBlazorUser> _signInManager; private readonly IStringLocalizer<TokenService> _localizer; private readonly IHttpContextAccessor _httpContextAccessor; private readonly AppConfiguration _appConfiguration; public TokenService( UserManager<TeachingBlazorUser> userManager, RoleManager<TeachingBlazorRole> roleManager, IOptions<AppConfiguration> appConfiguration, SignInManager<TeachingBlazorUser> signInManager, IHttpContextAccessor httpContextAccessor, IStringLocalizer<TokenService> localizer) { _userManager = userManager; _roleManager = roleManager; _signInManager = signInManager; _appConfiguration = appConfiguration.Value; _httpContextAccessor = httpContextAccessor; _localizer = localizer; } public async Task<Result<TokenResponse>> LoginAsync(TokenRequest model) { var user = await _userManager.FindByEmailAsync(model.Email); if (user == null) { return await Result<TokenResponse>.FailAsync(_localizer["User Not Found."]); } if (!user.IsActive) { return await Result<TokenResponse>.FailAsync(_localizer["User Not Active. Please contact the administrator."]); } if (!user.EmailConfirmed) { return await Result<TokenResponse>.FailAsync(_localizer["E-Mail not confirmed."]); } var passwordValid = await _userManager.CheckPasswordAsync(user, model.Password); if (!passwordValid) { return await Result<TokenResponse>.FailAsync(_localizer["Invalid Credentials."]); } user.RefreshToken = GenerateRefreshToken(); user.RefreshTokenExpiryTime = DateTime.Now.AddDays(7); await _userManager.UpdateAsync(user); var claims = await GetClaimsAsync(user); var token = await GenerateJwtAsync(user, claims); var response = new TokenResponse { Token = token, RefreshToken = user.RefreshToken, UserImageURL = user.ProfilePictureDataUrl }; return await Result<TokenResponse>.SuccessAsync(response); } private async Task<string> GenerateJwtAsync(TeachingBlazorUser user, IEnumerable<Claim> claims) { var token = GenerateEncryptedToken(GetSigningCredentials(), claims); return token; } private async Task<IEnumerable<Claim>> GetClaimsAsync(TeachingBlazorUser user) { var userClaims = _userManager.GetClaimsAsync(user).Result; var roles = await _userManager.GetRolesAsync(user); var roleClaims = new List<Claim>(); var permissionClaims = new List<Claim>(); foreach (var role in roles) { roleClaims.Add(new Claim(ClaimTypes.Role, role)); var thisRole = await _roleManager.FindByNameAsync(role); var allPermissionsForThisRoles = await _roleManager.GetClaimsAsync(thisRole); permissionClaims.AddRange(allPermissionsForThisRoles); } var claims = new List<Claim> { new(ClaimTypes.NameIdentifier, user.Id.ToString()), new(ClaimTypes.Email, user.Email), new(ClaimTypes.Name, user.FirstName), new(ClaimTypes.Surname, user.LastName), new(ClaimTypes.MobilePhone, user.PhoneNumber ?? string.Empty) }; var unionClaims = claims .Union(userClaims) .Union(roleClaims) .Union(permissionClaims); return unionClaims; } private string GenerateRefreshToken() { var randomNumber = new byte[32]; using var rng = RandomNumberGenerator.Create(); rng.GetBytes(randomNumber); return Convert.ToBase64String(randomNumber); } private string GenerateEncryptedToken(SigningCredentials signingCredentials, IEnumerable<Claim> claims) { DateTime now = DateTime.Now; var token = new JwtSecurityToken( claims: claims, expires: DateTime.UtcNow.AddDays(2), // notBefore: now, // Current User Test signingCredentials: signingCredentials); var tokenHandler = new JwtSecurityTokenHandler(); var encryptedToken = tokenHandler.WriteToken(token); return encryptedToken; } private ClaimsPrincipal GetPrincipalFromExpiredToken(string token) { var tokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_appConfiguration.Secret)), ValidateIssuer = false, ValidateAudience = false, RoleClaimType = ClaimTypes.Role, ClockSkew = TimeSpan.Zero, ValidateLifetime = false }; // String2XmlReader StringReader strRdr = new StringReader(token); XmlReader rdr = XmlReader.Create(strRdr); var tokenHandler = new JwtSecurityTokenHandler(); var principal = tokenHandler.ValidateToken(rdr, tokenValidationParameters, out var securityToken); if (securityToken is not JwtSecurityToken jwtSecurityToken || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) { throw new SecurityTokenException(_localizer["Invalid token"]); } return principal; } private SigningCredentials GetSigningCredentials() { if (string.IsNullOrEmpty(_appConfiguration.Secret)) throw new Exception("JWT secret not configured"); var secret = Encoding.UTF8.GetBytes(_appConfiguration.Secret!); return new SigningCredentials(new SymmetricSecurityKey(secret), SecurityAlgorithms.HmacSha256); } } }
-
-
在Program.cs里注册TokenService.cs和Localization服务,并且读取配置文件中AppConfiguration的内容:
builder.Services.AddLocalization(opts => { opts.ResourcesPath = "Resources"; }); builder.Services.Configure<AppConfiguration>(builder.Configuration.GetSection("AppConfiguration")); builder.Services.AddScoped<ITokenService, TokenService>();Program.cs所有代码:
using AspNetCore.Identity.Mongo; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using MongoDB.Driver; using TeachingWebApi.Config; using TeachingWebApi.Data.Seeder; using TeachingWebApi.Models.Identity; using TeachingWebApi.Services; using TeachingWebApi.Services.Identity; using TeachingWebApi.Services.Identity.impl; using TeachingWebApi.Utils.Data; using TeachingWebApi.Utils.Extensions; var builder = WebApplication.CreateBuilder(args); // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddControllers(); builder.Services.AddLocalization(opts => { opts.ResourcesPath = "Resources"; }); builder.Services.Configure<TeachingAppDatabaseSettings>( builder.Configuration.GetSection("TeachingAppDatabase")); var MongoDbConnectionString = builder.Configuration.GetSection("TeachingAppDatabase").Get<TeachingAppDatabaseSettings>().ConnectionString; var MongoDbUsersCollection = builder.Configuration.GetSection("TeachingAppDatabase").Get<TeachingAppDatabaseSettings>().UsersCollectionName; var MongoDbRolesCollection = builder.Configuration.GetSection("TeachingAppDatabase").Get<TeachingAppDatabaseSettings>().RolesCollectionName; builder.Services.Configure<AppConfiguration>(builder.Configuration.GetSection("AppConfiguration")); builder.Services.AddSingleton<BooksService>(); builder.Services.AddScoped<ITokenService, TokenService>(); builder.Services.AddTransient<MongoDbContext>(); builder.Services.AddTransient<MongoIdentityDbContext>(); // MongoDB with Identity builder.Services.AddIdentityMongoDbProvider<TeachingBlazorUser, TeachingBlazorRole>(identity => { // Password settings. identity.Password.RequireDigit = false; identity.Password.RequireLowercase = false; identity.Password.RequireNonAlphanumeric = false; identity.Password.RequireUppercase = false; identity.Password.RequiredLength = 1; identity.Password.RequiredUniqueChars = 0; // Lockout settings. identity.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); identity.Lockout.MaxFailedAccessAttempts = 5; identity.Lockout.AllowedForNewUsers = true; // User settings. identity.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; identity.User.RequireUniqueEmail = false; }, mongo => { mongo.ConnectionString = MongoDbConnectionString; mongo.UsersCollection = MongoDbUsersCollection; mongo.RolesCollection = MongoDbRolesCollection; } ) .AddEntityFrameworkStores<MongoIdentityDbContext>() .AddDefaultTokenProviders(); builder.Services .AddTransient<DatabaseSeeder>() .AddDbContext<MongoIdentityDbContext>(options => options .UseMongoDB(new MongoClient(MongoDbConnectionString), "teaching_blazor_app")); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.Initialize(builder.Configuration); app.UseHttpsRedirection(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello From ASP.NET Core Web API"); }); endpoints.MapGet("/Resource1", async context => { await context.Response.WriteAsync("Hello From ASP.NET Core Web API Resource1"); }); endpoints.MapControllerRoute( name: "Admin", pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); app.Run(); record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } -
Swagger测试,使用之前自动生成的SuperUser账号信息:

-
同时运行TeachingBlazorApp和TeachingWebApi应用,使用Login页面登录并看到成功登录的提示:

更多推荐
所有评论(0)