在學習ASP.NET Core Blazor程式設計系列二十二——登入(1)至學習ASP.NET Core Blazor程式設計系列二十六——登入(5)
系列文章中學習了使用AuthenticationStateProvider實現模擬登入。今天的文章實現JWT登入,使用WebAPI介面來實現通過JWT令牌登入。
2.在Visual Studio 2022的解決方案資源管理器中,滑鼠左鍵選中「Api」資料夾,右鍵單擊,在彈出選單中選擇「新增—>新建項」,在彈出對話方塊中,選擇「API控制器-空」,並將控制器命名為「AuthController」。如下圖。並新增如下程式碼:
using BlazorAppDemo.Models;
using BlazorAppDemo.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Linq;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace BlazorAppDemo.Api
{
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly IJWTHelper jwtHelper;
public AuthController(IJWTHelper _IJWTHelper)
{
this.jwtHelper = _IJWTHelper;
}
[HttpPost("Login")]
public async Task<ActionResult<UserToken>> Login(UserInfo userInfo)
{
//Demo用,更好的做法是查詢使用者表來實現
if (userInfo.UserName == "admin" && userInfo.Password == "111111")
{
return BuildToken(userInfo);
}
else
{
UserToken userToken = new UserToken()
{
StatusCode = System.Net.HttpStatusCode.Unauthorized,
IsSuccess = false
};
return userToken;
}
}
/// <summary>
/// 建立Token
/// </summary>
/// <param name="userInfo"></param>
/// <returns></returns>
private UserToken BuildToken(UserInfo userInfo)
{
string jwtToken = jwtHelper.CreateJwtToken<UserInfo>(userInfo);
//建立UserToken,回傳使用者端
UserToken userToken = new UserToken()
{
StatusCode = System.Net.HttpStatusCode.OK,
Token = jwtToken,
ExpireTime = DateTime.Now.AddMinutes(30),
IsSuccess= true
};
return userToken;
}
}
}
3.在Visual Studio 2022的解決方案資源管理器中,滑鼠左鍵選中「Models」資料夾,右鍵單擊,在彈出選單中選擇「新增—>類」,在彈出對話方塊中,將類命名為「UserToken」。並新增如下程式碼:
using System.Net;
namespace BlazorAppDemo.Models
{
public class UserToken
{
public bool IsSuccess { get ; set; }
public HttpStatusCode StatusCode { get; set; }
public string Token { get; set; }
public DateTime ExpireTime { get; set; }
}
}
4.在Visual Studio 2022的解決方案資源管理器中,滑鼠左鍵選中「Utils」資料夾,右鍵單擊,在彈出選單中選擇「新增—>類」,在彈出對話方塊中,將類命名為「TokenManager」。並新增如下程式碼:
using BlazorAppDemo.Models;
using System.Collections.Concurrent;
namespace BlazorAppDemo.Utils
{
public class TokenManager
{
private const string TOKEN = "authToken";
private static readonly ConcurrentDictionary<string, UserToken> tokenManager;
static TokenManager()
{
tokenManager=new ConcurrentDictionary<string, UserToken>();
}
public static ConcurrentDictionary<string, UserToken> Instance { get { return tokenManager; } }
public static string Token { get { return TOKEN; } }
}
}
using BlazorAppDemo.Models;
namespace BlazorAppDemo.Auth
{
public interface IAuthService
{
Task<UserToken> LoginAsync(UserInfo userInfo);
Task<UserToken> LogoutAsync();
}
}
using BlazorAppDemo.Models;
using BlazorAppDemo.Utils;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Collections.Concurrent;
using System.Net.Http;
using System.Text;
namespace BlazorAppDemo.Auth
{
public class AuthService : IAuthService
{
private readonly HttpClient httpClient;
private readonly AuthenticationStateProvider authenticationStateProvider;
private readonly IConfiguration configuration;
private readonly Api.AuthController authController;
private readonly string currentUserUrl, loginUrl, logoutUrl;
public AuthService( HttpClient httpClient, AuthenticationStateProvider authenticationStateProvider, IConfiguration configuration,Api.AuthController authController)
{
this.authController = authController;
this.httpClient = httpClient;
this.authenticationStateProvider = authenticationStateProvider;
this.configuration = configuration;
currentUserUrl = configuration["AuthUrl:Current"] ?? "Auth/Current/";
loginUrl = configuration["AuthUrl:Login"] ?? "api/Auth/Login";
logoutUrl = configuration["AuthUrl:Logout"] ?? "/api/Auth/Logout/";
}
public async Task<UserToken> LoginAsync(UserInfo userInfo)
{
var result = authController.Login(userInfo);
var loginResponse = result.Result.Value;
if (loginResponse != null && loginResponse.IsSuccess)
{
TokenManager.Instance.TryAdd(TokenManager.Token, loginResponse);
((ImitateAuthStateProvider)authenticationStateProvider).NotifyUserAuthentication(loginResponse.Token);
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", loginResponse.Token);
return loginResponse;
}
return new UserToken() { IsSuccess = false };
}
public Task<UserToken> LogoutAsync()
{
throw new NotImplementedException();
}
}
}
7. 在Visual Studio 2022的解決方案管理器中,使用滑鼠左鍵,雙擊ImitateAuthStateProvider.cs檔案,對程式碼進行修改。具體程式碼如下:
using BlazorAppDemo.Models;
using BlazorAppDemo.Utils;
using Microsoft.AspNetCore.Components.Authorization;
using System.Net.Http;
using System.Security.Claims;
namespace BlazorAppDemo.Auth
{
public class ImitateAuthStateProvider : AuthenticationStateProvider
{
private readonly IJWTHelper jwt;
private AuthenticationState anonymous;
private readonly HttpClient httpClient;
public ImitateAuthStateProvider(IJWTHelper _jwt, HttpClient httpClient)
{
anonymous = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
jwt = _jwt;
this.httpClient = httpClient;
}
bool isLogin = false;
string token = string.Empty;
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
//確認是否已經登入
UserToken userToken;
TokenManager.Instance.TryGetValue(TokenManager.Token,out userToken);
string tokenInLocalStorage=string.Empty;
if (userToken != null)
{
tokenInLocalStorage = userToken.Token;
}
if (string.IsNullOrEmpty(tokenInLocalStorage))
{
//沒有登入,則返回匿名登入者
return Task.FromResult(anonymous);
}
//將token取出轉換為claim
var claims = jwt.ParseToken(tokenInLocalStorage);
//在每次request的header中都將加入bearer token
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer",
tokenInLocalStorage);
//回傳帶有user claim的AuthenticationState
return Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt"))));
}
public void Login(UserInfo request)
{
//1.驗證使用者賬號密碼是否正確
if (request == null)
{
isLogin=false;
}
if (request.UserName == "user" && request.Password == "111111")
{
isLogin = true;
token= jwt.CreateJwtToken<UserInfo>(request);
Console.WriteLine($"JWT Token={token}");
}
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public void NotifyUserAuthentication(string token)
{
var claims = jwt.ParseToken(token);
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt"));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}
}
}
using BlazorAppDemo.Data;
using BlazorAppDemo.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Components.Authorization;
using BlazorAppDemo.Auth;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using System.IdentityModel.Tokens.Jwt;
using BlazorAppDemo.Utils;
using BlazorAppDemo.Api;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
IConfiguration config = ConfigHelper.Configuration;
System.Console.WriteLine(config["ConnectionStrings:BookContext"]);
builder.Services.AddDbContextFactory<BookContext>(opt =>
opt.UseSqlServer(ConfigHelper.Configuration["ConnectionStrings:BookContext"]));
builder.Services.AddScoped<ImitateAuthStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(implementationFactory =>
implementationFactory.GetRequiredService<ImitateAuthStateProvider>());
builder.Services.AddScoped<JwtSecurityTokenHandler>();
//此處的url地址改成自己實際的地址
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("http://localhost:7110") });
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<AuthController>();
//JWT
//JWT認證
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
//取出私鑰
var secretByte = Encoding.UTF8.GetBytes(builder.Configuration["Authentication:SecretKey"]);
options.TokenValidationParameters = new TokenValidationParameters()
{
//驗證釋出者
ValidateIssuer = true,
ValidIssuer = builder.Configuration["Authentication:Issuer"],
//驗證接收者
ValidateAudience = true,
ValidAudience = builder.Configuration["Authentication:Audience"],
//驗證是否過期
ValidateLifetime = true,
//驗證私鑰
IssuerSigningKey = new SymmetricSecurityKey(secretByte)
};
});
;
builder.Services.AddScoped<IJWTHelper,JWTHelper>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
Console.WriteLine("資料庫開始初始化。");
var context = services.GetRequiredService<BookContext>();
// requires using Microsoft.EntityFrameworkCore;
context.Database.Migrate();
// Requires using RazorPagesMovie.Models;
SeedData.Initialize(services);
Console.WriteLine("資料庫初始化結束。");
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "資料庫資料初始化錯誤.");
}
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.UseAuthentication();
app.UseAuthorization();
app.Run();
10.我們在使用者名稱輸入框中輸入使用者名稱"admin",在密碼輸入框中輸入密碼"111111",點選「登入」按鈕,進行登入。我們進入了系統。如下圖。