# [112]天方科技 ASP.net core 教育訓練 1120418([Authorize]身分驗證、JWT、DB動態轉換設定) ## HTTP request 取得來源預設順序 1. Form fields 2. The request body (For controllers that have the [ApiController] attribute.) 3. Route data 4. Query string parameters 5. Uploaded files ### 如果是使用`[HttpGet]`只有`Route data`、`Query string`兩種 參考: [**ASP.NET Core 中的資料繫結─來源**](https://learn.microsoft.com/zh-tw/aspnet/core/mvc/models/model-binding?view=aspnetcore-7.0#sources) ### 上傳檔案使用form-data `Content-Type:multipart/form-data` 每個分隔可以放入資料,例如純文字、二進位等資料,ASP.Net為了避免攻擊,預設限制檔案大小,如果有需求需額外設定 ![](https://i.imgur.com/kBaIAxo.png) ![](https://i.imgur.com/PbNjEhM.png) ## 簡單型別 - Boolean - Byte, SByte - Char - DateOnly - DateTime - DateTimeOffset - Decimal - Double - Enum - Guid - Int16, Int32, Int64 - Single - TimeOnly - TimeSpan - UInt16, UInt32, UInt64 - Uri - Version 參考: [**簡單型別**](https://learn.microsoft.com/zh-tw/aspnet/core/mvc/models/model-binding?view=aspnetcore-7.0#simple-types) ### Guid 全球唯一碼 長度16bytes 可以在Visual Studios產生 ### 工具→建立GUID ![](https://i.imgur.com/LBn5gJm.png) ### 常用的是第4點登錄格式,可以看到結果裡的GUID`0394B6BB-0583-4B5F-B0E4-94F479887064` ![](https://i.imgur.com/sS7cxkv.png) ## 資料放在body ### 使用Get ```csharp= [HttpGet("year/{year}")] public IActionResult GetStudentByYear(int year, [FromForm] DtoStudent dtoStudent) { string name = dtoStudent.Name!; string gender = dtoStudent.Gender!; var linq = from s in _context.s30_student where s.std_year == year && (name != null && name.Length > 0 ? s.std_name.StartsWith(name) : true) && (gender != null && gender.Length == 1 ? s.std_sex == gender : true) group s by new { s.cls_id, s.std_year } into g where g.Count() > 0 select new { cls_id = g.Key.cls_id, year = g.Key.std_year, count = g.Count(), Students = g.ToList() }; return Ok(linq); } ``` ### 在Swagger被拒絕 ![](https://i.imgur.com/xE6AXvq.png) ### 使用Post 查詢動作可以使用Post方式進行 ```csharp= [HttpPost("year/{year}")] public IActionResult GetStudentByYear(int year, [FromForm] DtoStudent dtoStudent) { string name = dtoStudent.Name!; string gender = dtoStudent.Gender!; var linq = from s in _context.s30_student where s.std_year == year && (name != null && name.Length > 0 ? s.std_name.StartsWith(name) : true) && (gender != null && gender.Length == 1 ? s.std_sex == gender : true) group s by new { s.cls_id, s.std_year } into g where g.Count() > 0 select new { cls_id = g.Key.cls_id, year = g.Key.std_year, count = g.Count(), Students = g.ToList() }; return Ok(linq); } ``` ![](https://i.imgur.com/Jmvnk6m.png) ### 屬性設定 ```csharp= public class DtoStudent { public string? Gender { get; } //如果不設set,代表只能使用get,變成唯獨屬性 } ``` ## JWT Authentication(身分驗證) ### 需要先從NuGet新增套件`Microsoft.AspNetCore.Authentication.JwtBearer` ![](https://i.imgur.com/loy9JVH.png) ### 註冊`Authentication`服務,設定JWT驗證時的`token`資訊 初始版本 ```csharp= builder.Services .AddAuthentication(//JwtBearerDefaults.AuthenticationScheme net7已預設 ) .AddJwtBearer(options => options.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = true, //簽發者 ValidIssuer="Skytek.com.tw", //驗證內容可以自訂 ValidateAudience = true, //接收者 ValidAudience="http://www.Skytek.com.tw", IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my password")), // 發行數位簽章,使用對稱式金鑰,將字串轉為byte陣列,交由key檢查數位簽章 ValidateLifetime = true, //驗證有效期限 }); ``` 將設定放入`appsettings.json`,`ConnectionStrings`下方 ```csharp= "ConnectionStrings": { "edu": "Data Source=.;Initial Catalog=edu;Persist Security Info=True;TrustServerCertificate=true;User ID=sa;Password=localhost" }, "JWT": { "Key": "FF13FC14-DE54-49F0-8D51-DCD1C676377F", //工具→建立GUID→登錄格式 "Issuer": "Skytek.com.tw", "Audience": "http://Skytek.com.tw" } ``` 透過`Configuration[]`引入設定檔 ```csharp= builder.Services .AddAuthentication(//JwtBearerDefaults.AuthenticationScheme 選擇JWT驗證 net7已預設 ) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, //簽發者 ValidIssuer = builder.Configuration["JWT:Issuer"], //讀取appsettings設定檔 ValidateAudience = true, //接收者 ValidAudience = builder.Configuration["JWT:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JWT:Key"]!)), //對等式金鑰,呼叫索引器[],"!"表示不會出現null情況 //IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration.GetSection("JWT:Key").Value!)) //呼叫方法() ValidateLifetime = true, //有效期限 }; }); ``` ### 原先註冊服務後需要去使用`UseAuthentication()` ```csharp= //app.UseAuthentication(); //原先需要增加,順序要在UseAuthorization()前 net7已預設 app.UseAuthorization(); //使用授權 app.MapControllers(); app.Run(); ``` ## Controller驗證設定 ### `Authorize`放置 `Action`後 ```csharp= [HttpGet,Authorize] //身分驗證,如果沒有指定角色,只要通過驗證就能使用API //or [HttpGet] [Authorize(Roles = "Admin, Users")] //指定角色,不符合角色的話即使有驗證也不能進入,有區分大小寫 ``` ### Swagger `Authorize`功能註冊 需要先從NuGet新增套件`Swashbuckle.AspNetCore.Filters` ![](https://i.imgur.com/MYaMdoJ.png) 註冊前 ![](https://i.imgur.com/cYsjQ8x.png) 註冊後 ![](https://i.imgur.com/TtC5N1j.png) ```csharp= builder.Services.AddSwaggerGen(); //↓ builder.Services.AddSwaggerGen(options => { options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme //自訂名稱 { Description = "bearer{token}", //輸入token時的提示描述,可自訂 In = ParameterLocation.Header, //參數位址 Name = "Authorization", //參數名稱 Type = SecuritySchemeType.ApiKey //安全碼種類為ApiKey }); options.OperationFilter<SecurityRequirementsOperationFilter>(); } ); ``` ### 設定完成 ![](https://i.imgur.com/56ao4wi.png) ### 如果沒有驗證會被拒絕使用 ![](https://i.imgur.com/ApJDO2l.png) ## 創建`AuthController.cs` 新增空的API ![](https://i.imgur.com/7kV5VVI.png) ### 創建`DtoLogin.cs` ```csharp= namespace Edu.Dto { public class DtoLogin { //登入需要的資訊 public string Name { get; set; } //帳號 public string Password { get; set; } //密碼 public string DB { get; set; } //之後切換DB可以使用 public bool RememberMe { get; set; } //登入資訊記憶(有資安問題,可用可不用) } } ``` ### 因為要用`Dto`會將資料包在`body`,所以使用`Post` 先注入`IConfiguration`,此為內建服務 ```csharp= private readonly IConfiguration _configuration; //內建服務 public AuthController(IConfiguration configuration) { _configuration = configuration; } ``` 實作Login以及產生JWT驗證功能 ```csharp= [HttpPost] [AllowAnonymous] //允許匿名者,沒有加Authorize時預設 public IActionResult Login(DtoLogin login) { //尚未使用DB,先行自訂 //如果不加(),會先執行&&在執行||,所以不加()也行 if((login.Name != "clone" && login.Name != "eric") || login.Password != "pwd") { //檢查帳號 return BadRequest("Login Fail"); } var roles = new List<string> //login的角色身分 { "Admin", "clone" }; if(login.Name == "eric") roles = new List<string>{ "Users"}; var token = CreateToken(login, roles); //呼叫自訂方法回傳的token return Ok(token); } //建立token private string CreateToken(DtoLogin login, List<string> roles) { //Identity Principal List<Claim> claims = new List<Claim> { new Claim(ClaimTypes.Name, login.Name), //使用ClaimTypes.Name屬性放置登入的Name //由於是要放在token的資料,所以不要放入密碼等敏感性資料 new Claim(ClaimTypes.GroupSid, login.DB), //使用ClaimTypes沒有DB屬性,所以選擇一個不常使用的屬性來放置,GroupSid = 群組身分號碼 new Claim("Comp", "Skytek"), //也可自訂屬性 }; roles.ForEach(r => claims.Add(new Claim(ClaimTypes.Role, r))); //放置角色種類 var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Key"]!)); //在program.cs註冊也有使用,但是在這裡要用_configuration var cre = new SigningCredentials(key, SecurityAlgorithms.HmacSha512); //數位簽章要使用剛剛宣告的key,以及設定演算碼 var token = new JwtSecurityToken( issuer: _configuration["JWT:Issuer"], audience: _configuration["JWT:Audience"], claims: claims, expires: dtoLogin.RememberMe ? DateTime.Now.AddYears(1) : DateTime.Now.AddSeconds(30), //設定RememberMe和有效期限 signingCredentials:cre //數位簽章 ); var jwt = new JwtSecurityTokenHandler().WriteToken(token); //產生JWT return jwt; } ``` ## 設定完成後測試 ### 首先透過`[HttpPost]Login`取得JWT ![](https://i.imgur.com/b2bKkA2.png) ![](https://i.imgur.com/3nd3hg6.png) ### JWT驗證:`bearer JWT` ![](https://i.imgur.com/XioR9Jq.png) ### 通過驗證後,允許使用功能 ![](https://i.imgur.com/cDd57qN.png) ![](https://i.imgur.com/ThiFE6Q.png) ### 將JWT驗證碼貼到JWT.io查看驗證碼轉譯內容 ![](https://i.imgur.com/KnhB2RW.png) 參考: [**JSON Web Tokens**](https://jwt.io/) ### 其中`"exp": 1714185066`是驗證碼過期時間 不論設定的時間是多久,系統都會預設偏移+5分鐘 ![](https://i.imgur.com/7QsNOxw.png) ```csharp= var token = new JwtSecurityToken( issuer: _configuration["JWT:Issuer"], audience: _configuration["JWT:Audience"], claims: claims, expires: dtoLogin.RememberMe ? DateTime.Now.AddYears(1) : DateTime.Now.AddSeconds(30), //設定RememberMe和有效期限 signingCredentials:cre ); ``` ### 如果要修改時間偏移,可以在註冊服務時設定 ```csharp= builder.Services .AddAuthentication() .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = builder.Configuration["JWT:Issuer"], ValidateAudience = true, ValidAudience = builder.Configuration["JWT:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JWT:Key"]!)), ValidateLifetime = true, ClockSkew = TimeSpan.Zero //預設驗證有效期間除了自行設定的時間,會加上系統預設的5分鐘,這裡將系統預設時間設為0 }; }); ``` ## [Authorize]放置 ### 個別Action驗證 ```csharp= [HttpGet] [Authorize(Roles = "Admin, Users")] //角色可以不設 ``` ### 整個Controller驗證,這時可以在個別Action使用`AllowAnonymous`匿名者,不用經過驗證 ```csharp= [Route("api/[controller]")] [ApiController] [Authorize] //個別Action設定允許匿名者 [HttpGet] [AllowAnonymous] ``` ### 整個專案驗證,在`program.cs`註冊服務,如果有Login功能,需要加上`[AllowAnonymous]`標籤,避免Login功能也被鎖住 此設定在Swagger不會生效,可能是Swagger沒有包含此功能,可改用Postman測試 ```csharp= builder.Services.AddControllers(options => { options.Filters.Add(new AuthorizeFilter()); }); ``` ## Postman `Token`驗證 ### 選擇`Authorization`頁面 ![](https://i.imgur.com/8QBgIwh.png) ### Type選擇`Bearer Token` ![](https://i.imgur.com/RnRILLO.png) ### 將JWT填入Token輸入框 ![](https://i.imgur.com/bFG4hzz.png) ### 驗證成功 ![](https://i.imgur.com/TjnnI5M.png) ## DB轉換設定 ### 在`appsettings.json`裡新增`ConnectionStrings`設定 ```json= "ConnectionStrings": { "edu": "Data Source=.;Initial Catalog=edu;Persist Security Info=True;TrustServerCertificate=true;User ID=sa;Password=localhost", "db1": "Data Source=.;Initial Catalog=db1;Persist Security Info=True;TrustServerCertificate=true;User ID=sa;Password=localhost" } ``` ### 新增`Context`擴充類別`eduContext.partial.cs`,也可以`eduContext2.cs` 使用`eduContext.partial.cs`可以與擴充的`Context`變成巢狀結構,`eduContext2.cs`則會變成另外的`class` ![](https://i.imgur.com/HKhPuef.png) ![](https://i.imgur.com/lqQPQGh.png) ### 在`program.cs`註冊`HttpContext`相依性 `HttpContext` 用於封裝個別 `HTTP` 要求和回應的所有資訊,`IHttpContextAccessor`只是取用`HttpContext`實例的接口,用 Singleton 的方式就可以供其它物件使用。 `builder.Services.AddSingleton<Interface, 實作Interface的class>()`:類似global變數,整個應用程式只會有一個物件,可被共用,生命週期持續到應用程式結束 ```csharp= builder.Services.AddDbContext<eduContext>( options => options.UseSqlServer(builder.Configuration.GetConnectionString("edu"))); //↓ builder.Services.AddDbContext<eduContext>(); //options改在DBContext的OnConfiguring依HttpContext.User動態設定 builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); //為了給DBContext取得HttpContext.User ``` 參考: [**HttpContext 類別**](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.aspnetcore.http.httpcontext?view=aspnetcore-7.0) ### 設定擴充功能,讓DB可以動態修改 ```csharp= public partial class eduContext { //private readonly IConfiguration _configuration; private readonly HttpContext? _httpContext; public eduContext(DbContextOptions<eduContext> options, IHttpContextAccessor httpContext) //需使用IHttpContextAccessor作為接口 : base(options) { _httpContext = httpContext.HttpContext; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { //base.OnConfiguring(optionsBuilder); if(!optionsBuilder.IsConfigured) //判斷optionsBuilder是否設定 { var db = _httpContext?.User.Claims //取得是否有做過身分驗證的資料,沒有的話就是空的 .Where(c => c.Type == ClaimTypes.GroupSid) //取得當初AuthController.cs設定DB時的位置 .Select(c => c.Value) .SingleOrDefault() ?? "edu"; //無授權者預設的db 必須回傳單筆 optionsBuilder.UseSqlServer($"Server=.;Database={db};Trusted_Connection=True;TrustServerCertificate=true"); //複製ConnectionStrings的設定,將DB的部分改為動態 } } } ``` ### 可以再登入時決定使用的DB ![](https://i.imgur.com/7CUKpVG.png) ## 前端Token驗證 待補