C#技術棧入門到精通系列19——鑑權授權IdentityServer JWT
閱讀目錄
1、介紹
2、對比session登入
3、應用案例
3.1、啟用無狀態登入系統
3.2、使用token訪問受保護的action
3.3、給使用者新增角色和許可權
3.4、完成註冊過程
3.5、完成登入過程
3.6、自定義使用者模型,新增初始化資料
4、參考
IdentityServer JWT是一套鑑權授權開源元件,授權是允許你做什麼?即授予權力,鑑權是在程式執行過程中確認你是誰?被允許做什麼?IdentityServer JWT遵循JWT(JSON Web Token)這個開放標準。JWT它定義了一種緊湊獨立的方式,可以將各方之間的資訊作為JSON物件進行安全傳輸。該資訊可以驗證和信任,因為是經過數字簽名的。JWT由頭部Header、有效載荷Payload和簽名Signature三部分構成。各個部分見用“.”隔開,可以在網站 https://
頭部Header由typ和alg兩部分構成,typ表示token的型別,這裡固定為JWT,alg表示使用hash演算法,例如HMAC SHA256或者RSA。例如編碼前Header“{"alg":"HS256","typ":"JWT"}”,編碼後“eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9”。
有效載荷Payload作用①儲存需要傳遞的資訊,如使用者ID、使用者名稱等,②儲存元素據,如過期時間、釋出人等,③與Header不同,Payload可以加密。例如編碼前“{"userId":"BigBox777"}”,base64編碼後“eyJ1c2VySWQiOiJCaWdCb3g3NzcifQ==”,base64Url編碼後“eyJ1c2VySWQiOiJCaWdCb3g3NzcifQ”。
簽名Signature作用對Header和Payload部分進行簽名,保證Token在傳輸的過程中沒有被篡改或者損壞,Signature演算法:Signature=HMACSHA256(base64UrlEncode(header)+“.”+base64UrlEncode(payload),secret)。
最後我們來看一看JWT標準所定義常用有效載荷欄位:
- iss:該 jwt 的簽發者
- sub:該 jwt 所面向的使用者
- aud:接收該 jwt 的一方
- nbf:定義在什麼時間之前該jwt是不可用的
- iat(issued at簽發):jwt的簽發時間,是一個 unix 時間戳
- exp(expires過期):jwt的過期時長,一個時間長度單位ms
- jti:jwt的唯一標識,主要用作一次性token,避免重放攻擊
除了JWT之外還有一種鑑權授權方式session,這是一種有狀態的登入方式,而JWT是無狀態登入,常用來做單點登入系統(開源免費有OpenAM、OpenIDM、OpenDJ,企業付費有ForgeRock、Microsoft AM),下面看一下兩種方式原理對比。
Session對比JWT優勢
1:相比JWT,伺服器可以主動清除session,可以管理授權有效期。
2:session儲存在伺服器端,相對比較安全,JWT儲存在客戶端不安全,可以使用HTTPS傳輸解決。
3:結合Cookie使用,較為靈活,相容性好。
Session對比JWT劣勢
1:Cookie+session在跨域場景表現得並不太好,Cookie跨域不好。
2:如果是分散式部署,需要做多機共享session機制,額外資源投入。
3:基於Cookie的機制很容易被CSRF(跨站請求偽造攻擊)。
4:查詢session資訊可能有資料庫查詢操作,額外的資源投入。
相關概念
1:session主要存放在伺服器,相對安全。
2:Cookie主要存放在客戶端,不是很安全。
3:sessionStorage僅在當前會話下有效,關閉頁面或瀏覽器後被清除。
4:localStorage除非被清除,否則被永久儲存。
此案例使用一個NET5建立的ASP.NET Core MVC專案上做演示,先建立一個專案。
3.1、啟用無狀態登入系統
第一步:在專案資料夾 Controllers 下建立一個控制器 AuthenticateController 。
第二步:給這個控制器加上登入的Action Login ,分別加上Post請求特性 [HttpPost] 和允許匿名訪問特性 [AllowAnonymous] //允許匿名訪問 ,
第三步:在專案資料夾 Models 下建立登入資料DTO LoginDto.cs 。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Threading.Tasks; 5 6 namespace Demo19_IdentityServerJWT.Models 7 { 8 public class LoginDto 9 { 10 public string Username { get; set; } 11 public string Password { get; set; } 12 } 13 }LoginDto.cs
第四步:給專案安裝JWT框架 Microsoft.AspNetCore.Authentication.JwtBearer v5.0.15 ,這裡需要主要NET6使用的是v6.0.0以上的框架。
第五步:在 Login Action中完成建立jwt,並響應客戶端200 OK,並返回jwt(注意密碼有最小長度要求,太短會報IDX10653異常),完整的 AuthenticateController.cs 程式碼如下。
1 using Demo19_IdentityServerJWT.Models; 2 using Microsoft.AspNetCore.Authorization; 3 using Microsoft.AspNetCore.Mvc; 4 using Microsoft.IdentityModel.Tokens; 5 using System; 6 using System.Collections.Generic; 7 using System.IdentityModel.Tokens.Jwt; 8 using System.Linq; 9 using System.Security.Claims; 10 using System.Text; 11 using System.Threading.Tasks; 12 13 namespace Demo19_IdentityServerJWT.Controllers 14 { 15 public class AuthenticateController : Controller 16 { 17 private readonly string _secretKey = "jwtTestsMiMa777888999";//jwt加密用的密碼 18 [AllowAnonymous] //允許匿名訪問 19 [HttpPost] 20 public IActionResult Login([FromBody]LoginDto loginDto) 21 { 22 //1、驗證使用者名稱和密碼 23 //2、建立jwt(header、payload、signiture) 24 //2.1、建立header 25 var signingAlgorithm = SecurityAlgorithms.HmacSha256;//儲存使用演算法 26 //2.2、建立payload 27 var claims = new Claim[] 28 { 29 new Claim(JwtRegisteredClaimNames.Sub,"BigBox777") //自定義資料部分,這裡使用JWT標準定義的Sub表示該 jwt 所面向的使用者 30 }; 31 //2.3、建立signiture 32 var secretByte = Encoding.UTF8.GetBytes(_secretKey); 33 var signingKey = new SymmetricSecurityKey(secretByte);//對密碼進行加密 34 var signingCredentials = new SigningCredentials(signingKey, signingAlgorithm); //驗證加密後的私鑰 35 //2.4、建立jwt 36 var token = new JwtSecurityToken( 37 issuer: "donesoft.cn",//誰釋出的 38 audience: "donesoft.cn",//釋出給誰用 39 claims,//payload資料 40 notBefore:DateTime.UtcNow,//釋出時間 41 expires:DateTime.UtcNow.AddMinutes(10),//有效期10分鐘 42 signingCredentials //數字簽名 43 ); 44 var tokenString = new JwtSecurityTokenHandler().WriteToken(token);//轉成字串token 45 //3、返回200 OK,回傳jwt 46 return Ok(tokenString); 47 } 48 } 49 }AuthenticateController.cs
第六步:使用測試工具Postman來發起請求,測試一下是否正常執行。
第七步:複製返貨的jwt到網站 https://jwt.io/ ,並輸入密碼進行驗證。
要使用token訪問受保護的action,就需要先做系統級的jwt配置,看下面操作。
第一步:由於很多地方需要共用jwt密碼,釋出者這些資訊,我們先把這部分放到配置檔案 appsettings.json 裡。
1 { 2 "Logging": { 3 "LogLevel": { 4 "Default": "Information", 5 "Microsoft": "Warning", 6 "Microsoft.Hosting.Lifetime": "Information" 7 } 8 }, 9 "AllowedHosts": "*", 10 "Authentication": { 11 "secretKey": "jwtTestsMiMa777888999", //jwt密碼 12 "issuer": "donesoft.cn", //釋出者 13 "audience": "donesoft.cn" //釋出給誰用 14 } 15 }appsettings.json
第二步:更新專案資料夾 Controllers 下建立一個控制器 AuthenticateController.cs檔案,加入對配置檔案的依賴注入。
1 using Demo19_IdentityServerJWT.Models; 2 using Microsoft.AspNetCore.Authorization; 3 using Microsoft.AspNetCore.Mvc; 4 using Microsoft.Extensions.Configuration; 5 using Microsoft.IdentityModel.Tokens; 6 using System; 7 using System.Collections.Generic; 8 using System.IdentityModel.Tokens.Jwt; 9 using System.Linq; 10 using System.Security.Claims; 11 using System.Text; 12 using System.Threading.Tasks; 13 14 namespace Demo19_IdentityServerJWT.Controllers 15 { 16 public class AuthenticateController : Controller 17 { 18 private readonly IConfiguration _configuration;//配置檔案 19 public AuthenticateController(IConfiguration configuration) 20 { 21 _configuration = configuration; 22 } 23 24 [AllowAnonymous] //允許匿名訪問 25 [HttpPost] 26 public IActionResult Login([FromBody]LoginDto loginDto) 27 { 28 //1、驗證使用者名稱和密碼 29 //2、建立jwt(header、payload、signiture) 30 //2.1、建立header 31 var signingAlgorithm = SecurityAlgorithms.HmacSha256;//儲存使用演算法 32 //2.2、建立payload 33 var claims = new Claim[] 34 { 35 new Claim(JwtRegisteredClaimNames.Sub,"BigBox777") //自定義資料部分,這裡使用JWT標準定義的Sub表示該 jwt 所面向的使用者 36 }; 37 //2.3、建立signiture 38 var secretByte = Encoding.UTF8.GetBytes(_configuration["Authentication:secretKey"]); 39 var signingKey = new SymmetricSecurityKey(secretByte);//對密碼進行加密 40 var signingCredentials = new SigningCredentials(signingKey, signingAlgorithm); //驗證加密後的私鑰 41 //2.4、建立jwt 42 var token = new JwtSecurityToken( 43 issuer: _configuration["Authentication:issuer"],//誰釋出的 44 audience: _configuration["Authentication:audience"],//釋出給誰用 45 claims,//payload資料 46 notBefore:DateTime.UtcNow,//釋出時間 47 expires:DateTime.UtcNow.AddMinutes(10),//有效期10分鐘 48 signingCredentials //數字簽名 49 ); 50 var tokenString = new JwtSecurityTokenHandler().WriteToken(token);//轉成字串token 51 //3、返回200 OK,回傳jwt 52 return Ok(tokenString); 53 } 54 } 55 }AuthenticateController.cs
第三步:在專案Startup.cs檔案下的 public void ConfigureServices(IServiceCollection services) 方法內註冊jwt服務。
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).//使用jwtBearer註冊身份認真服務 4 AddJwtBearer(options =>//配置jwtBearer 5 { 6 var secretByte = Encoding.UTF8.GetBytes(Configuration["Authentication:secretKey"]); 7 options.TokenValidationParameters = new TokenValidationParameters() 8 { 9 //只有配置的釋出者donesoft.cn才會被接受 10 ValidateIssuer = true, 11 ValidIssuer = Configuration["Authentication:issuer"], 12 //只有配置的使用者donesoft.cn才會被接受 13 ValidateAudience = true, 14 ValidAudience = Configuration["Authentication:audience"], 15 //驗證token是否過期 16 ValidateLifetime=true, 17 //對密碼進行加密 18 IssuerSigningKey=new SymmetricSecurityKey(secretByte) 19 }; 20 }); 21 services.AddControllersWithViews(); 22 }public void ConfigureServices(IServiceCollection services)
第四步:在專案Startup.cs檔案下的 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 方法內配置授權鑑權中介軟體,需要特別注意中介軟體順序。
1 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 2 { 3 if (env.IsDevelopment()) 4 { 5 app.UseDeveloperExceptionPage(); 6 } 7 else 8 { 9 app.UseExceptionHandler("/Home/Error"); 10 } 11 app.UseStaticFiles(); 12 //使用路由,訪問去哪裡? 13 app.UseRouting(); 14 //驗證中介軟體,你有什麼許可權? 15 app.UseAuthentication(); 16 //授權中介軟體,你可以幹什麼? 17 app.UseAuthorization(); 18 19 app.UseEndpoints(endpoints => 20 { 21 endpoints.MapControllerRoute( 22 name: "default", 23 pattern: "{controller=Home}/{action=Index}/{id?}"); 24 }); 25 }public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
第五步:資料夾 Controllers 下建立一個控制器 BookController.cs 檔案,再建立一個POST的Action CreateNewBook 用來測試業務邏輯。
1 using Demo19_IdentityServerJWT.Models; 2 using Microsoft.AspNetCore.Authorization; 3 using Microsoft.AspNetCore.Mvc; 4 using System; 5 using System.Collections.Generic; 6 using System.Linq; 7 using System.Threading.Tasks; 8 9 namespace Demo19_IdentityServerJWT.Controllers 10 { 11 public class BookController : Controller 12 { 13 [HttpPost] 14 [Authorize] 15 public IActionResult CreateNewBook([FromBody]BookDto bookDto) 16 { 17 return Ok($"{bookDto.BookName}:建立成功!"); 18 } 19 } 20 }BookController.cs
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Threading.Tasks; 5 6 namespace Demo19_IdentityServerJWT.Models 7 { 8 public class BookDto 9 { 10 public string BookName { get; set; } 11 public decimal UnitPrice { get; set; }//單價 12 } 13 }Models下DTO資料BookDto.cs
第六步:使用Postman測試一下在沒有登入的時候,訪問返回401 Unauthorized(沒有授權)。
第七步:我們在Postman請求的頭Header里加入登入返回的token,注意新增的欄位為 Authorization ,對應值為 bearer +登入返回的token,這裡要嚴格注意大小寫格式和bearer和token之間有空格。
第八步:以上就完成了使用token訪問授權的action,下一節我們來介紹一下對角色和許可權的管理。
在.NET中基於Claims的身份認證體系(JWT只是Claim的其中一種認證方式),需要用一個類 Claim ①用來表示資源的所有權,例如:說明使用者的角色,表示使用者所具有的許可權。②最小不可分割單位,可以自由組合使用的靈活度相當高。
第一步:更新專案資料夾 Controllers 下控制器 AuthenticateController.cs 檔案,給jwt中加入角色資訊,關鍵程式碼 new Claim(ClaimTypes.Role,"Admin") 。
1 using Demo19_IdentityServerJWT.Models; 2 using Microsoft.AspNetCore.Authorization; 3 using Microsoft.AspNetCore.Mvc; 4 using Microsoft.Extensions.Configuration; 5 using Microsoft.IdentityModel.Tokens; 6 using System; 7 using System.Collections.Generic; 8 using System.IdentityModel.Tokens.Jwt; 9 using System.Linq; 10 using System.Security.Claims; 11 using System.Text; 12 using System.Threading.Tasks; 13 14 namespace Demo19_IdentityServerJWT.Controllers 15 { 16 public class AuthenticateController : Controller 17 { 18 private readonly IConfiguration _configuration;//配置檔案 19 public AuthenticateController(IConfiguration configuration) 20 { 21 _configuration = configuration; 22 } 23 24 [AllowAnonymous] //允許匿名訪問 25 [HttpPost] 26 public IActionResult Login([FromBody]LoginDto loginDto) 27 { 28 //1、驗證使用者名稱和密碼 29 //2、建立jwt(header、payload、signiture) 30 //2.1、建立header 31 var signingAlgorithm = SecurityAlgorithms.HmacSha256;//儲存使用演算法 32 //2.2、建立payload 33 var claims = new Claim[] 34 { 35 new Claim(JwtRegisteredClaimNames.Sub,"BigBox777"), //自定義資料部分,這裡使用JWT標準定義的Sub表示該 jwt 所面向的使用者 36 new Claim(ClaimTypes.Role,"Admin") //加入角色認證資訊 37 }; 38 //2.3、建立signiture 39 var secretByte = Encoding.UTF8.GetBytes(_configuration["Authentication:secretKey"]); 40 var signingKey = new SymmetricSecurityKey(secretByte);//對密碼進行加密 41 var signingCredentials = new SigningCredentials(signingKey, signingAlgorithm); //驗證加密後的私鑰 42 //2.4、建立jwt 43 var token = new JwtSecurityToken( 44 issuer: _configuration["Authentication:issuer"],//誰釋出的 45 audience: _configuration["Authentication:audience"],//釋出給誰用 46 claims,//payload資料 47 notBefore:DateTime.UtcNow,//釋出時間 48 expires:DateTime.UtcNow.AddMinutes(10),//有效期10分鐘 49 signingCredentials //數字簽名 50 ); 51 var tokenString = new JwtSecurityTokenHandler().WriteToken(token);//轉成字串token 52 //3、返回200 OK,回傳jwt 53 return Ok(tokenString); 54 } 55 } 56 }AuthenticateController.cs
第二步:更新資料夾 Controllers 下控制器 BookController.cs 檔案,指定訪問action的角色名稱,關鍵程式碼 [Authorize(Roles = "Admin")] 。
1 using Demo19_IdentityServerJWT.Models; 2 using Microsoft.AspNetCore.Authorization; 3 using Microsoft.AspNetCore.Mvc; 4 using System; 5 using System.Collections.Generic; 6 using System.Linq; 7 using System.Threading.Tasks; 8 9 namespace Demo19_IdentityServerJWT.Controllers 10 { 11 public class BookController : Controller 12 { 13 [HttpPost] 14 [Authorize(Roles = "Admin")] 15 public IActionResult CreateNewBook([FromBody]BookDto bookDto) 16 { 17 return Created("donesoft.cn",bookDto); 18 } 19 } 20 }BookController.cs
第三步:完成以上就完成了角色和許可權功能。
這節來完成註冊過程,註冊需要用到資料庫,這裡用到了EFCore,跟著下面一起操作吧。
第一步:nuget安裝 Microsoft.AspNetCore.Identity.EntityFrameworkCore v5.0.15 。
第二步:專案下建立Database資料夾,在建立一個數據庫上下文AppDbContext.cs來管理資料庫資料。
1 using Demo19_IdentityServerJWT.Models; 2 using Microsoft.AspNetCore.Identity; 3 using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 4 using Microsoft.EntityFrameworkCore; 5 using System; 6 using System.Collections.Generic; 7 using System.Linq; 8 using System.Threading.Tasks; 9 10 namespace Demo19_IdentityServerJWT.Database 11 { 12 public class AppDbContext : IdentityDbContext<IdentityUser> 13 { 14 public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) 15 { 16 } 17 public DbSet<BookDto> BookDtos { get; set; } 18 } 19 }AppDbContext.cs
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel.DataAnnotations; 4 using System.ComponentModel.DataAnnotations.Schema; 5 using System.Linq; 6 using System.Threading.Tasks; 7 8 namespace Demo19_IdentityServerJWT.Models 9 { 10 public class BookDto 11 { 12 [Key] 13 public int Id { get; set; } 14 [Required] 15 [MaxLength(100)] 16 public string BookName { get; set; } 17 [Required] 18 [Column(TypeName = "decimal(18, 2)")] 19 public decimal UnitPrice { get; set; }//單價 20 [Range(0.0, 1.0)] 21 public double? DiscountPresent { get; set; }//折扣 22 public DateTime CreateTime { get; set; } 23 public DateTime? UpdateTime { get; set; } 24 } 25 }BookDto.cs
第三步:更新專案下檔案Startup.cs裡 public void ConfigureServices(IServiceCollection services) 方法裡程式碼,註冊身份認證服務框架依賴,關鍵程式碼 services.AddIdentity<IdentityUser, IdentityRole>().AddEntityFrameworkStores<AppDbContext>();//註冊身份認證服務框架依賴 。
1 using Demo19_IdentityServerJWT.Database; 2 using Microsoft.AspNetCore.Authentication.JwtBearer; 3 using Microsoft.AspNetCore.Builder; 4 using Microsoft.AspNetCore.Hosting; 5 using Microsoft.AspNetCore.Identity; 6 using Microsoft.Extensions.Configuration; 7 using Microsoft.Extensions.DependencyInjection; 8 using Microsoft.Extensions.Hosting; 9 using Microsoft.IdentityModel.Tokens; 10 using System; 11 using System.Collections.Generic; 12 using System.Linq; 13 using System.Text; 14 using System.Threading.Tasks; 15 16 namespace Demo19_IdentityServerJWT 17 { 18 public class Startup 19 { 20 public Startup(IConfiguration configuration) 21 { 22 Configuration = configuration; 23 } 24 25 public IConfiguration Configuration { get; } 26 27 // This method gets called by the runtime. Use this method to add services to the container. 28 public void ConfigureServices(IServiceCollection services) 29 { 30 services.AddIdentity<IdentityUser, IdentityRole>().AddEntityFrameworkStores<AppDbContext>();//註冊身份認證服務框架依賴 31 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).//使用jwtBearer註冊身份認真服務 32 AddJwtBearer(options =>//配置jwtBearer 33 { 34 var secretByte = Encoding.UTF8.GetBytes(Configuration["Authentication:secretKey"]); 35 options.TokenValidationParameters = new TokenValidationParameters() 36 { 37 //只有配置的釋出者donesoft.cn才會被接受 38 ValidateIssuer = true, 39 ValidIssuer = Configuration["Authentication:issuer"], 40 //只有配置的使用者donesoft.cn才會被接受 41 ValidateAudience = true, 42 ValidAudience = Configuration["Authentication:audience"], 43 //驗證token是否過期 44 ValidateLifetime=true, 45 //對密碼進行加密 46 IssuerSigningKey=new SymmetricSecurityKey(secretByte) 47 }; 48 }); 49 services.AddControllersWithViews(); 50 } 51 52 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 53 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 54 { 55 if (env.IsDevelopment()) 56 { 57 app.UseDeveloperExceptionPage(); 58 } 59 else 60 { 61 app.UseExceptionHandler("/Home/Error"); 62 } 63 app.UseStaticFiles(); 64 //使用路由,訪問去哪裡? 65 app.UseRouting(); 66 //驗證中介軟體,你有什麼許可權? 67 app.UseAuthentication(); 68 //授權中介軟體,你可以幹什麼? 69 app.UseAuthorization(); 70 71 app.UseEndpoints(endpoints => 72 { 73 endpoints.MapControllerRoute( 74 name: "default", 75 pattern: "{controller=Home}/{action=Index}/{id?}"); 76 }); 77 } 78 } 79 }Startup.cs
第四步:開啟Visual Studio選單欄檢視=》SQL Server物件資源管理器,建立一個測試資料庫TestDB,開啟TestDB的屬性,找到連線字串儲存到配置檔案 appsettings.json 中。
1 { 2 "Logging": { 3 "LogLevel": { 4 "Default": "Information", 5 "Microsoft": "Warning", 6 "Microsoft.Hosting.Lifetime": "Information" 7 } 8 }, 9 "AllowedHosts": "*", 10 "Authentication": { 11 "secretKey": "jwtTestsMiMa777888999", //jwt密碼 12 "issuer": "donesoft.cn", //釋出者 13 "audience": "donesoft.cn" //釋出給誰用 14 }, 15 "DbContext": { 16 "ConnectionString": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=TestDB;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False" 17 } 18 }appsettings.json
第五步:nuget安裝EFCore需要的遷移工具 Microsoft.EntityFrameworkCore.Tools v5.0.15 、sqlServer包 Microsoft.EntityFrameworkCore v5.0.15 和 Microsoft.EntityFrameworkCore.SqlServer v5.0.15 。在更新Startup.cs裡 public void ConfigureServices(IServiceCollection services) 方法裡程式碼,新增EF Core的使用 services.AddDbContext<AppDbContext>(options => { options.UseSqlServer(Configuration["DbContext:ConnectionString"]); }); 。
1 using Demo19_IdentityServerJWT.Database; 2 using Microsoft.AspNetCore.Authentication.JwtBearer; 3 using Microsoft.AspNetCore.Builder; 4 using Microsoft.AspNetCore.Hosting; 5 using Microsoft.AspNetCore.Identity; 6 using Microsoft.EntityFrameworkCore; 7 using Microsoft.Extensions.Configuration; 8 using Microsoft.Extensions.DependencyInjection; 9 using Microsoft.Extensions.Hosting; 10 using Microsoft.IdentityModel.Tokens; 11 using System; 12 using System.Collections.Generic; 13 using System.Linq; 14 using System.Text; 15 using System.Threading.Tasks; 16 17 namespace Demo19_IdentityServerJWT 18 { 19 public class Startup 20 { 21 public Startup(IConfiguration configuration) 22 { 23 Configuration = configuration; 24 } 25 26 public IConfiguration Configuration { get; } 27 28 // This method gets called by the runtime. Use this method to add services to the container. 29 public void ConfigureServices(IServiceCollection services) 30 { 31 services.AddDbContext<AppDbContext>(options => { 32 options.UseSqlServer(Configuration["DbContext:ConnectionString"]); 33 }); 34 services.AddIdentity<IdentityUser, IdentityRole>().AddEntityFrameworkStores<AppDbContext>();//註冊身份認證服務框架依賴 35 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).//使用jwtBearer註冊身份認真服務 36 AddJwtBearer(options =>//配置jwtBearer 37 { 38 var secretByte = Encoding.UTF8.GetBytes(Configuration["Authentication:secretKey"]); 39 options.TokenValidationParameters = new TokenValidationParameters() 40 { 41 //只有配置的釋出者donesoft.cn才會被接受 42 ValidateIssuer = true, 43 ValidIssuer = Configuration["Authentication:issuer"], 44 //只有配置的使用者donesoft.cn才會被接受 45 ValidateAudience = true, 46 ValidAudience = Configuration["Authentication:audience"], 47 //驗證token是否過期 48 ValidateLifetime=true, 49 //對密碼進行加密 50 IssuerSigningKey=new SymmetricSecurityKey(secretByte) 51 }; 52 }); 53 services.AddControllersWithViews(); 54 } 55 56 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 57 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 58 { 59 if (env.IsDevelopment()) 60 { 61 app.UseDeveloperExceptionPage(); 62 } 63 else 64 { 65 app.UseExceptionHandler("/Home/Error"); 66 } 67 app.UseStaticFiles(); 68 //使用路由,訪問去哪裡? 69 app.UseRouting(); 70 //驗證中介軟體,你有什麼許可權? 71 app.UseAuthentication(); 72 //授權中介軟體,你可以幹什麼? 73 app.UseAuthorization(); 74 75 app.UseEndpoints(endpoints => 76 { 77 endpoints.MapControllerRoute( 78 name: "default", 79 pattern: "{controller=Home}/{action=Index}/{id?}"); 80 }); 81 } 82 } 83 }Startup.cs
第六步:開啟Visual Studio選單工具=》Nuget包管理器=》程式包管理器控制檯 中輸入命令 EntityFrameworkCore\Enable-Migrations 開啟遷移,再用命令 EntityFrameworkCore\Add-Migration InitDB 和 EntityFrameworkCore\update-database 更新Identity EFCore相關的角色許可權資料表到資料庫中(開啟資料庫就可以看見剛剛遷移的資料表)。不少很清楚EF的遷移可以看我的文章https://www.cnblogs.com/bigbox777/p/13907142.html。
第七步:更新專案資料夾 Controllers 下控制器 AuthenticateController.cs 檔案,添加註冊使用者action函式,這裡需要依賴注入使用者工具類 private readonly UserManager<IdentityUser> _userManager; 。
1 using Demo19_IdentityServerJWT.Models; 2 using Microsoft.AspNetCore.Authorization; 3 using Microsoft.AspNetCore.Identity; 4 using Microsoft.AspNetCore.Mvc; 5 using Microsoft.Extensions.Configuration; 6 using Microsoft.IdentityModel.Tokens; 7 using System; 8 using System.Collections.Generic; 9 using System.IdentityModel.Tokens.Jwt; 10 using System.Linq; 11 using System.Security.Claims; 12 using System.Text; 13 using System.Threading.Tasks; 14 15 namespace Demo19_IdentityServerJWT.Controllers 16 { 17 public class AuthenticateController : ControllerBase 18 { 19 private readonly IConfiguration _configuration;//配置檔案 20 private readonly UserManager<IdentityUser> _userManager; //依賴注入的使用者工具 21 public AuthenticateController(IConfiguration configuration, UserManager<IdentityUser> userManager) 22 { 23 _configuration = configuration; 24 _userManager = userManager; 25 } 26 27 [AllowAnonymous] //允許匿名訪問 28 [HttpPost] 29 public IActionResult Login([FromBody] LoginDto loginDto) 30 { 31 //1、驗證使用者名稱和密碼 32 //2、建立jwt(header、payload、signiture) 33 //2.1、建立header 34 var signingAlgorithm = SecurityAlgorithms.HmacSha256;//儲存使用演算法 35 //2.2、建立payload 36 var claims = new Claim[] 37 { 38 new Claim(JwtRegisteredClaimNames.Sub,"BigBox777"), //自定義資料部分,這裡使用JWT標準定義的Sub表示該 jwt 所面向的使用者 39 new Claim(ClaimTypes.Role,"Admin") //加入角色認證資訊 40 }; 41 //2.3、建立signiture 42 var secretByte = Encoding.UTF8.GetBytes(_configuration["Authentication:secretKey"]); 43 var signingKey = new SymmetricSecurityKey(secretByte);//對密碼進行加密 44 var signingCredentials = new SigningCredentials(signingKey, signingAlgorithm); //驗證加密後的私鑰 45 //2.4、建立jwt 46 var token = new JwtSecurityToken( 47 issuer: _configuration["Authentication:issuer"],//誰釋出的 48 audience: _configuration["Authentication:audience"],//釋出給誰用 49 claims,//payload資料 50 notBefore:DateTime.UtcNow,//釋出時間 51 expires:DateTime.UtcNow.AddMinutes(10),//有效期10分鐘 52 signingCredentials //數字簽名 53 ); 54 var tokenString = new JwtSecurityTokenHandler().WriteToken(token);//轉成字串token 55 //3、返回200 OK,回傳jwt 56 return Ok(tokenString); 57 } 58 59 [AllowAnonymous] //允許匿名訪問 60 [HttpPost] 61 public async Task<IActionResult> Register([FromBody] RegisterDto registerDto) //需要訪問資料庫用非同步操作 62 { 63 //1、使用registerDto物件建立IdentityUser使用者物件 64 var user = new IdentityUser() 65 { 66 UserName = registerDto.Username 67 }; 68 69 //2、使用UserManager來把密碼hash加密,儲存到資料庫 70 var result = await _userManager.CreateAsync(user, registerDto.Password); 71 if (!result.Succeeded) 72 { 73 return BadRequest(); 74 } 75 else 76 { 77 //3、返回200 78 return Ok(); 79 } 80 } 81 } 82 }AuthenticateController.cs
第八步:注入還需要用到 RegisterDto.cs ,這用到了特性來比較密碼是否一致 [Compare(nameof(Password), ErrorMessage ="密碼輸入不一致")] //比較兩個欄位是否一致 。
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel.DataAnnotations; 4 using System.Linq; 5 using System.Threading.Tasks; 6 7 namespace Demo19_IdentityServerJWT.Models 8 { 9 public class RegisterDto 10 { 11 12 [Required] 13 public string Username { get; set; } 14 [Required] 15 public string Password { get; set; } 16 [Required] 17 [Compare(nameof(Password), ErrorMessage ="密碼輸入不一致")] //比較兩個欄位是否一致 18 public string ConfirmPassword { get; set; } 19 } 20 }RegisterDto.cs
第九步:以上就完成了註冊功能,看下面測試結果。
第一步:登入的過程還需要用到SignInManager這個簽名工具類,更新專案資料夾 Controllers 下控制器 AuthenticateController.cs 檔案,新增SignInManager的依賴注入和更新Action函式Login,新增登入驗證的邏輯。
1 using Demo19_IdentityServerJWT.Models; 2 using Microsoft.AspNetCore.Authorization; 3 using Microsoft.AspNetCore.Identity; 4 using Microsoft.AspNetCore.Mvc; 5 using Microsoft.Extensions.Configuration; 6 using Microsoft.IdentityModel.Tokens; 7 using System; 8 using System.Collections.Generic; 9 using System.IdentityModel.Tokens.Jwt; 10 using System.Linq; 11 using System.Security.Claims; 12 using System.Text; 13 using System.Threading.Tasks; 14 15 namespace Demo19_IdentityServerJWT.Controllers 16 { 17 public class AuthenticateController : ControllerBase 18 { 19 private readonly IConfiguration _configuration;//配置檔案 20 private readonly UserManager<IdentityUser> _userManager; //依賴注入的使用者工具 21 private readonly SignInManager<IdentityUser> _signInManager;//依賴注入簽名使用者工具 22 public AuthenticateController(IConfiguration configuration, 23 UserManager<IdentityUser> userManager, 24 SignInManager<IdentityUser> signInManager) 25 { 26 _configuration = configuration; 27 _userManager = userManager; 28 _signInManager = signInManager; 29 } 30 31 [AllowAnonymous] //允許匿名訪問 32 [HttpPost] 33 public async Task<IActionResult> Login([FromBody] LoginDto loginDto) 34 { 35 //1、驗證使用者名稱和密碼 36 var loginResult = await _signInManager.PasswordSignInAsync( 37 loginDto.Username,//使用者名稱 38 loginDto.Password,//密碼 39 false,//關閉瀏覽器後登入 cookie 是否應保持不變 40 false//登入失敗時是否鎖定使用者帳戶 41 ); 42 if (!loginResult.Succeeded) 43 { 44 return BadRequest(); 45 } 46 //從資料庫中取得使用者資料 47 var user = await _userManager.FindByNameAsync(loginDto.Username); 48 //2、建立jwt的header 49 var signingAlgorithm = SecurityAlgorithms.HmacSha256;//儲存使用演算法 50 //3、建立jwt的payload 51 var claims = new List<Claim>() 52 { 53 new Claim(JwtRegisteredClaimNames.Sub,user.Id) 54 }; 55 //獲取使用者所有角色,把角色加入payload資料中 56 var roleNames = await _userManager.GetRolesAsync(user); 57 foreach (var roleName in roleNames) 58 { 59 var roleClaim = new Claim(ClaimTypes.Role, roleName); 60 claims.Add(roleClaim); 61 } 62 //4、建立jwt的signiture 63 var secretByte = Encoding.UTF8.GetBytes(_configuration["Authentication:secretKey"]); 64 var signingKey = new SymmetricSecurityKey(secretByte);//對密碼進行加密 65 var signingCredentials = new SigningCredentials(signingKey, signingAlgorithm); //驗證加密後的私鑰 66 //5、建立jwt 67 var token = new JwtSecurityToken( 68 issuer: _configuration["Authentication:issuer"],//誰釋出的 69 audience: _configuration["Authentication:audience"],//釋出給誰用 70 claims,//payload資料 71 notBefore: DateTime.UtcNow,//釋出時間 72 expires: DateTime.UtcNow.AddMinutes(10),//有效期10分鐘 73 signingCredentials //數字簽名 74 ); 75 var tokenString = new JwtSecurityTokenHandler().WriteToken(token);//轉成字串token 76 //6、返回200 OK,回傳jwt 77 return Ok(tokenString); 78 79 #region Login之前測試程式碼 80 ////2、建立jwt(header、payload、signiture) 81 ////2.1、建立header 82 //var signingAlgorithm = SecurityAlgorithms.HmacSha256;//儲存使用演算法 83 ////2.2、建立payload 84 //var claims = new Claim[] 85 //{ 86 // new Claim(JwtRegisteredClaimNames.Sub,"BigBox777"), //自定義資料部分,這裡使用JWT標準定義的Sub表示該 jwt 所面向的使用者 87 // new Claim(ClaimTypes.Role,"Admin") //加入角色認證資訊 88 //}; 89 ////2.3、建立signiture 90 //var secretByte = Encoding.UTF8.GetBytes(_configuration["Authentication:secretKey"]); 91 //var signingKey = new SymmetricSecurityKey(secretByte);//對密碼進行加密 92 //var signingCredentials = new SigningCredentials(signingKey, signingAlgorithm); //驗證加密後的私鑰 93 ////2.4、建立jwt 94 //var token = new JwtSecurityToken( 95 // issuer: _configuration["Authentication:issuer"],//誰釋出的 96 // audience: _configuration["Authentication:audience"],//釋出給誰用 97 // claims,//payload資料 98 // notBefore: DateTime.UtcNow,//釋出時間 99 // expires: DateTime.UtcNow.AddMinutes(10),//有效期10分鐘 100 // signingCredentials //數字簽名 101 // ); 102 //var tokenString = new JwtSecurityTokenHandler().WriteToken(token);//轉成字串token 103 ////3、返回200 OK,回傳jwt 104 //return Ok(tokenString); 105 #endregion 106 } 107 108 [AllowAnonymous] //允許匿名訪問 109 [HttpPost] 110 public async Task<IActionResult> Register([FromBody] RegisterDto registerDto) //需要訪問資料庫用非同步操作 111 { 112 //1、使用registerDto物件建立IdentityUser使用者物件 113 var user = new IdentityUser() 114 { 115 UserName = registerDto.Username 116 }; 117 118 //2、使用UserManager來把密碼hash加密,儲存到資料庫 119 var result = await _userManager.CreateAsync(user, registerDto.Password); 120 if (!result.Succeeded) 121 { 122 return BadRequest(); 123 } 124 else 125 { 126 //3、返回200 127 return Ok(); 128 } 129 } 130 } 131 }AuthenticateController.cs
第二步:給需要驗證的Action都加上IdentityServer的Bearer驗證,關鍵程式碼 [Authorize(AuthenticationSchemes = "Bearer")] ,否則就會返回404錯誤。
第三步:驗證一下。
修改使用者模型之前,先來介紹一下IdentityServer框架預設新增的使用者資料表功能,看下圖。
第一步:在專案資料夾 Models 下建立自定義使用者模型ApplicationUser.cs,繼承自IdentityServer框架的IdentityUser。
1 using Microsoft.AspNetCore.Identity; 2 using System; 3 using System.Collections.Generic; 4 using System.Linq; 5 using System.Threading.Tasks; 6 7 namespace Demo19_IdentityServerJWT.Models 8 { 9 public class ApplicationUser:IdentityUser 10 { 11 public string Address { get; set; } //新增拓展的欄位 12 //建立程式碼和模型的關係 13 public virtual ICollection<IdentityUserRole<string>> UserRoles { get; set; } //使用者角色 14 public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; } //使用者許可權宣告 15 public virtual ICollection<IdentityUserLogin<string>> Logins { get; set; } //使用者第三方登入資訊 16 public virtual ICollection<IdentityUserToken<string>> Tokens { get; set; } //使用者登入的session 17 } 18 }ApplicationUser.cs
第二步:把專案裡使用的全部 IdentityUser類 替換成我們自定義的 ApplicationUser 類, Startup.cs 、 AppDbContext.cs 和 AuthenticateController.cs 。
第三步:更改專案資料夾 Database 下AppDbContext.cs,重寫 protected override void OnModelCreating(ModelBuilder modelBuilder) 方法,加入初始化資料庫程式碼。
1 using Demo19_IdentityServerJWT.Models; 2 using Microsoft.AspNetCore.Identity; 3 using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 4 using Microsoft.EntityFrameworkCore; 5 using System; 6 using System.Collections.Generic; 7 using System.Linq; 8 using System.Threading.Tasks; 9 10 namespace Demo19_IdentityServerJWT.Database 11 { 12 public class AppDbContext : IdentityDbContext<ApplicationUser> 13 { 14 public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) 15 { 16 } 17 public DbSet<BookDto> BookDtos { get; set; } 18 protected override void OnModelCreating(ModelBuilder builder) 19 { 20 // 初始化使用者與角色的種子資料 21 // 1. 更新使用者ApplicationUser與角色IdentityUserRole的外來鍵關係 22 builder.Entity<ApplicationUser>(b => { 23 b.HasMany(x => x.UserRoles) 24 .WithOne() //HasMany表示和一對多的關係 25 .HasForeignKey(ur => ur.UserId) //使用IdentityUserRole的UserId作為外來鍵 26 .IsRequired(); //不能為空必填 27 }); 28 29 // 2. 新增管理員角色 30 var adminRoleId = "308660dc-ae51-480f-824d-7dca6714c3e2"; // guid 31 builder.Entity<IdentityRole>().HasData( 32 new IdentityRole 33 { 34 Id = adminRoleId, 35 Name = "Admin", 36 NormalizedName = "Admin".ToUpper() 37 } 38 ); 39 40 // 3. 新增使用者 41 var adminUserId = "90184155-dee0-40c9-bb1e-b5ed07afc04e"; 42 ApplicationUser adminUser = new ApplicationUser 43 { 44 Id = adminUserId, 45 UserName = "BigBox777", 46 NormalizedUserName = "BigBox777".ToUpper(), 47 Email = "[email protected]", 48 NormalizedEmail = "[email protected]".ToUpper(), 49 TwoFactorEnabled = false, 50 EmailConfirmed = true, 51 PhoneNumber = "13888888888", 52 PhoneNumberConfirmed = false 53 }; 54 PasswordHasher<ApplicationUser> ph = new PasswordHasher<ApplicationUser>(); 55 adminUser.PasswordHash = ph.HashPassword(adminUser, "Abcdef123$"); 56 builder.Entity<ApplicationUser>().HasData(adminUser); 57 58 // 4. 給使用者加入管理員許可權 59 // 通過使用 linking table:IdentityUserRole 60 builder.Entity<IdentityUserRole<string>>().HasData( 61 new IdentityUserRole<string>() 62 { 63 RoleId = adminRoleId, 64 UserId = adminUserId 65 }); 66 base.OnModelCreating(builder); 67 } 68 } 69 }AppDbContext.cs
第四步:開啟Visual Studio選單工具=》Nuget包管理器=》程式包管理器控制檯 中輸入命令 EntityFrameworkCore\Enable-Migrations 開啟遷移。
第五步:以上就完成了使用者模型的擴充套件功能,可以登入測試一下。
EF資料遷移完整步驟 https://www.cnblogs.com/bigbox777/p/13907142.html