本文將記錄如何一步步從無到有使用 Dotnet Core 6.0 建立 ASP.NET Core Web API,其中將會使用到下列技術:
- Dotnet CLI
- Entity Framework
- Json Web Token
- SQL Server (Docker Version)
- ASP.NET Core Generator
專案完成後的檔案結構
./專案目錄
├── .config/
│ └── dotnet-tools.json
├── .vscode/
│ ├── launch.js
│ └── tasks.json
├── Controller/
│ ├── AuthenticateController.cs
│ ├── TodoController.cs
│ └── WeatherForecast.cs
├── Data/
│ └── ApiDbContext.cs
├── Migrations/
├── Models/
│ ├── AuthenticateData.cs
│ └── ItemData.cs
├── obj/
├── Properties/
│ └── launchSettings.json
├── .gitignore
├── appsettings.Development.json
├── appsettings.json
├── dotnet6-webapi-jwt.csproj
├── global.json
├── Program.cs
├── README.md
└── WeatherForecast.cs
專案完成後所提供的 API 端點
Methods | Urls | Actions |
---|---|---|
POST | /api/Authenticate/login | 註冊新使用者帳號 |
POST | /api/Authenticate/register | 使用者帳號登入 |
POST | /api/Authenticate/register-admin | 管理者帳號登入 |
GET | /api/Todos | get all Todos |
POST | /api/Todos | add New Todo |
GET | /api/Todos/:id | get Todo by id |
PUT | /api/Todos/:id | update Todo by id |
DELETE | /api/Todos/:id | remove Todo by id |
建置新專案
$ dotnet new webapi -o dotnet6-webapi-jwt
範本「ASP.NET Core Web API」已成功建立。
正在處理建立後的動作...
正在 /home/egs/cal-data/tech-test/webapi/dotnet6-webapi-jwt/dotnet6-webapi-jwt.csproj 上執行 'dotnet restore'...
正在判斷要還原的專案...
已還原 /home/egs/cal-data/tech-test/webapi/dotnet6-webapi-jwt/dotnet6-webapi-jwt.csproj (238 ms 內)。
還原成功。
$ cd dotnet6-webapi-jwt
$ ls -al
總用量 40
drwxrwxr-x 5 egs egs 4096 五 20 09:38 .
drwxrwxr-x 12 egs egs 4096 五 20 09:38 ..
-rw-rw-r-- 1 egs egs 127 五 20 09:38 appsettings.Development.json
-rw-rw-r-- 1 egs egs 151 五 20 09:38 appsettings.json
drwxrwxr-x 2 egs egs 4096 五 20 09:38 Controllers
-rw-rw-r-- 1 egs egs 382 五 20 09:38 dotnet6-webapi-jwt.csproj
drwxrwxr-x 2 egs egs 4096 五 20 09:38 obj
-rw-rw-r-- 1 egs egs 557 五 20 09:38 Program.cs
drwxrwxr-x 2 egs egs 4096 五 20 09:38 Properties
-rw-rw-r-- 1 egs egs 267 五 20 09:38 WeatherForecast.cs
dotnet 版本管理
$ dotnet --list-sdks # 顯示已安裝的 sdk 版本資訊
5.0.408 [/usr/share/dotnet/sdk]
6.0.300 [/usr/share/dotnet/sdk]
$ dotnet --version # 顯示目前所使用的版本
6.0.300 # 預設是最新的版本
由於 dotnet 版本演化滿快的,所以會建議在專案目錄中要指定使用 SDK 的版本,以免當你又安裝了更新版本(如7.0)後程式執行出問題。
$ dotnet new globaljson --sdk-version 6.0.300
範本「global.json 檔案」已成功建立。
$ cat global.json
{
"sdk": {
"version": "6.0.300"
}
}
使用 dotnet cli 來產生預設的 git ignore 檔案
$ dotnet new gitignore
建立 git 初始版本
$ git init && git add . && git commit -m "Initial commit"
安裝本機工具
此方式安裝的工具,僅限本機存取(只針對目前的目錄和子目錄), 首先透過 dotnet new tool-manifest 命令來產生工具資訊清單檔,再使用 dotnet tool install 來安裝各式工具程式。這樣的方式好處是在專案若多人協助方式時,則可利用 dotnet tool restore 命令將紀錄在 .config/dotnet-tools.json 的工具資訊清單檔重建在不同協助人員的電腦中。
$ dotnet new tool-manifest #會產生 .config/dotnet-tools.json 檔案
$ dotnet tool install --local dotnet-ef #使用 local 安裝方式來安裝 Entity Framework 工具
您可使用下列命令,從此目錄叫用工具: 'dotnet tool run dotnet-ef' 或 'dotnet dotnet-ef'。
已成功安裝工具 'dotnet-ef' (版本 '6.0.5')。項目已新增至資訊清單檔 /home/egs/cal-data/tech-test/webapi/dotnet6-webapi-jwt/.config/dotnet-tools.json。
$ dotnet tool install --local dotnet-aspnet-codegenerator #使用 local 安裝方式來安裝 Code Generator 工具
您可使用下列命令,從此目錄叫用工具: 'dotnet tool run dotnet-aspnet-codegenerator' 或 'dotnet dotnet-aspnet-codegenerator'。
已成功安裝工具 'dotnet-aspnet-codegenerator' (版本 '6.0.5')。項目已新增至資訊清單檔 /home/egs/cal-data/tech-test/webapi/dotnet6-webapi-jwt/.config/dotnet-tools.json。
$ cat .\.config\dotnet-tools.json # 查看安裝上述二項工具後的設定資訊
|
|
安裝程式使用的相關套件
$ dotnet add package Microsoft.EntityFrameworkCore.Tools #使用 dotnet Entity Framework時必須安裝此套件
$ dotnet add package Microsoft.EntityFrameworkCore.Design #使用 dotnet Entity Framework時必須安裝此套件
$ dotnet add package Microsoft.EntityFrameworkCore.SqlServer #使用 dotnet Entity Framework時必須安裝此套件
$ dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore #使用 Identity Framework時必須安裝此套件
$ dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer #
$ dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design #搭配 dotnet-aspnet-codegenerator 使用
$ dotnet add package System.Configuration.ConfigurationManager #搭配 dotnet-aspnet-codegenerator 使用
安裝的程式套件資訊紀錄在 “專案”.csproj 檔案中
$ cat dotnet6-webapi-jwt.csproj #查看 安裝套件的相關設定值
|
|
建立 git 新版本
$ git add . && git commit -m "Add EFCore NuGet packages"
執行程式
$ dotnet watch
打開 VS Code
$ code .
目前産生的程式架構
設置使用 Entity Framework相關設定
新增 database context (自動產生)
使用 dotnet ef 工具在專案目錄 ./Data 子目錄下新建立一個 ApiDbContext.cs 的 DB Context file
註:在使用前先把 SQL Server 環境傋妥,安裝 SQL Server 可參考此篇筆紀 Run SQL Server container images with Docker
$ dotnet ef dbcontext scaffold "Data Source=localhost;Initial Catalog=TestDB;User ID=SA;Password=Sql@12345" Microsoft.EntityFrameworkCore.SqlServer -c ApiDbContext -o Data
Build started...
Build succeeded.
To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace dotnet6_webapi_jwt.Data
{
public partial class ApiDbContext : DbContext
{
public ApiDbContext()
{
}
public ApiDbContext(DbContextOptions<ApiDbContext> options)
: base(options)
{
}
public virtual DbSet<Inventory> Inventories { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
// optionsBuilder.UseSqlServer("Data Source=localhost;Initial Catalog=TestDB;User ID=SA;Password=Sql@12345");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Inventory>(entity =>
{
entity.HasNoKey();
entity.ToTable("Inventory");
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.Name)
.HasMaxLength(50)
.HasColumnName("name");
entity.Property(e => e.Quantity).HasColumnName("quantity");
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
}
在 appsettings.json 檔案中加入 Connection String
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"ConnStr": "Data Source=localhost;Initial Catalog=TestDB;User ID=SA;Password=Sql@12345"
}
}
使用 Asp.Net Core Identity framework 來管理使用者使用權限
ASP.NET Core Identity:
- 支援使用者介面 (UI) 登入功能的 API。
- 管理使用者、密碼、設定檔資料、角色、宣告、權杖、電子郵件確認等。
ASP.Net Core Identity Framework 是一個方便且還完善的使用權限管理架構。
將相關 Asp.Net Core Identity framework 功能注入到 container 中
除了安裝相關套件外,還要調整相關程式:
- 在 Program.cs 檔案中將加入以下程式碼 (before services.AddControllers())
ConfigurationManager _configuration = builder.Configuration;
// Add services to the container.
builder.Services.AddDbContext<ApiDbContext>(
options => options.UseSqlServer(
_configuration.GetConnectionString("ConnStr")
)
);
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApiDbContext>()
.AddDefaultTokenProviders();
- 使用 AspNetCore Identity,則 DataContext (ApiDbContext.cs 中) 必須要繼承 IdentityDbContext, 同時 Model creationg 時要改成呼叫 base.OnModelCreation
|
|
新增一個 entity framework 遷移 並 更新資料庫
完成上述程式調整後,來執行資料庫遷移(migrations)
$ dotnet build
$ dotnet ef migrations add "Add Identity Framework"
Build started...
Build succeeded.
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 6.0.5 initialized 'ApiDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.5' with options: None
Done. To undo this action, use 'ef migrations remove'
$ dotnet ef database update
在 dotnet ef migrations add “Add Identity Framework” 指令完成後,可以在專案目錄下發生産生新的子目錄 Migrations,並有三個新檔案
在 dotnet ef database update 指令完成後,SQL Server TestDB 資料庫中產生 Identity Framework 會使用到的資料表
建立 git 新版本
$ git add . && git commit -m "新增一個 entity framework 遷移 並 更新資料庫"
使用 Jason Web Token
在 appsettings.json 中自定JWT實作會使用到的設定值
{
// ...
"ConnectionStrings": {
"ConnStr": "Data Source=localhost;Initial Catalog=TestDB;User ID=SA;Password=Sql@12345"
},
"JwtSettings": {
"ValidIssuer": "Dotnet6WebApiDemo",
"ValidAudience": "Dotnet6WebApiDemo",
"Secret": "Dotnet6 WebApi Demo. Using Json Web Token Technology to keep user info."
}
}
新增 使用者註冊和登入時使用的 Data model class (Models/AuthenticateData.cs)
using System.ComponentModel.DataAnnotations;
namespace dotnet6_webapi_jwt.Models;
public class Response
{
public string? Status { get; set; }
public string? Message { get; set; }
}
public class LoginModel
{
[EmailAddress]
[Required(ErrorMessage = "Eamil Address is required")]
public string? Email { get; set; }
[Required(ErrorMessage = "Password is required")]
public string? Password { get; set; }
}
public class RegisterModel
{
[Required(ErrorMessage = "User Name is required")]
public string? Username { get; set; }
[EmailAddress]
[Required(ErrorMessage = "Email Address is required")]
public string? Email { get; set; }
[Required(ErrorMessage = "Password is required")]
public string? Password { get; set; }
}
public static class UserRoles
{
public const string Admin = "Admin";
public const string User = "User";
}
新增 註冊和登入邏輯 (Controllers/AuthenticateControll.cs)
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using dotnet6_webapi_jwt.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
namespace dotnet6_webapi_jwt.Controllers;
[Route("api/[controller]")]
[ApiController]
public class AuthenticateController : ControllerBase
{
private readonly UserManager<IdentityUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
private readonly IConfiguration _configuration;
public AuthenticateController(
UserManager<IdentityUser> userManager,
RoleManager<IdentityRole> roleManager,
IConfiguration configuration)
{
_userManager = userManager;
_roleManager = roleManager;
_configuration = configuration;
}
[HttpPost]
[Route("login")]
public async Task<IActionResult> Login([FromBody] LoginModel userModel)
{
var user = await _userManager.FindByEmailAsync(userModel.Email);
if (user != null && await _userManager.CheckPasswordAsync(user, userModel.Password))
{
var userRoles = await _userManager.GetRolesAsync(user);
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
foreach (var userRole in userRoles)
{
claims.Add(new Claim(ClaimTypes.Role, userRole));
}
var token = CreateToken(claims);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
});
}
return Unauthorized();
}
[HttpPost]
[Route("register")]
public async Task<IActionResult> Register([FromBody] RegisterModel model)
{
var userExists = await _userManager.FindByEmailAsync(model.Email);
if (userExists != null)
return StatusCode(StatusCodes.Status500InternalServerError, new Response {
Status = "Error", Message = "User already exists!"
});
IdentityUser user = new()
{
Email = model.Email,
SecurityStamp = Guid.NewGuid().ToString(),
UserName = model.Username
};
var result = await _userManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
return StatusCode(StatusCodes.Status500InternalServerError, new Response {
Status = "Error", Message = "User creation failed! Please check user details and try again."
});
return Ok(new Response { Status = "Success", Message = "User created successfully!" });
}
[HttpPost]
[Route("register-admin")]
public async Task<IActionResult> RegisterAdmin([FromBody] RegisterModel model)
{
var userExists = await _userManager.FindByEmailAsync(model.Email);
if (userExists != null)
return StatusCode(StatusCodes.Status500InternalServerError, new Response {
Status = "Error", Message = "User already exists!"
});
IdentityUser user = new()
{
Email = model.Email,
SecurityStamp = Guid.NewGuid().ToString(),
UserName = model.Username
};
var result = await _userManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
return StatusCode(StatusCodes.Status500InternalServerError, new Response {
Status = "Error", Message = "User creation failed! Please check user details and try again."
});
if (!await _roleManager.RoleExistsAsync(UserRoles.Admin))
await _roleManager.CreateAsync(new IdentityRole(UserRoles.Admin));
if (!await _roleManager.RoleExistsAsync(UserRoles.User))
await _roleManager.CreateAsync(new IdentityRole(UserRoles.User));
if (await _roleManager.RoleExistsAsync(UserRoles.Admin))
{
await _userManager.AddToRoleAsync(user, UserRoles.Admin);
}
if (await _roleManager.RoleExistsAsync(UserRoles.Admin))
{
await _userManager.AddToRoleAsync(user, UserRoles.User);
}
return Ok(new Response { Status = "Success", Message = "User created successfully!" });
}
private JwtSecurityToken CreateToken(List<Claim> claims)
{
var secretkey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
_configuration.GetValue<string>("JwtSettings:Secret"))); // _configuration.GetSection("JwtSettings:Secret").Value)
var credentials = new SigningCredentials(secretkey, SecurityAlgorithms.HmacSha512Signature);
var token = new JwtSecurityToken( // 亦可使用 SecurityTokenDescriptor 來産生 Token
issuer: _configuration.GetValue<string>("JwtSettings:ValidIssuer"),
audience: _configuration.GetValue<string>("JwtSettings:ValidAudience"),
expires: DateTime.Now.AddDays(1),
claims: claims,
signingCredentials: credentials);
return token;
}
}
有關實作 JWT 的流程
透過 JWT 的實作可以讓你的專案實現 Token-base 的身份驗證與授權。 (Json Web Token) 實作的過程大致可以分成三個部分:
- 在登入成功後産生合法的 JWT Token
- 每次收到 request 時驗證是否為合法有效的 JWT Token
- 在特定 API Endpoint 上驗證是否帶有 “合法有效的 JWT Token”,以達到權限管理的需求
産生合法的 Jason Web Token
在上述 AuthenticateController.cs 程式中,我們建立一個 CreateToken() 的 function,並在登入檢核成功時産生一個 token 回傳。
設置驗證是否為合法有效的 JWT Token
第一步,透過 DI 將 JWT 相關設定設置好
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
// 當驗證失敗時,回應標頭會包含 WWW-Authenticate 標頭,這裡會顯示失敗的詳細錯誤原因
options.IncludeErrorDetails = true; // 預設值為 true,有時會特別關閉
options.TokenValidationParameters = new TokenValidationParameters
{
// 透過這項宣告,就可以從 "NAME" 取值
NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
// 透過這項宣告,就可以從 "Role" 取值,並可讓 [Authorize] 判斷角色
RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
// 驗證 Issuer (一般都會)
ValidateIssuer = true,
ValidIssuer = _configuration.GetValue<string>("JwtSettings:ValidIssuer"),
// 驗證 Audience (通常不太需要)
ValidateAudience = false,
//ValidAudience = = _configuration.GetValue<string>("JwtSettings:ValidAudience"),
// 驗證 Token 的有效期間 (一般都會)
ValidateLifetime = true,
// 如果 Token 中包含 key 才需要驗證,一般都只有簽章而已
ValidateIssuerSigningKey = false,
// 應該從 IConfiguration 取得
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret))
};
});
第二步,要啟動 request pipeline 中的 Middleware (UseAuthentication & UseAuthorization 都需要)
app.UseAuthentication();
app.UseAuthorization();
在特定 API EndPoint 上驗證是否帶有合法有效的 JWT Token
在 WeatherForecastController.cs Get() function 上加入[Authorize]
即可
|
|
如上程式加入[Authorize]
後,再重新瀏覽 weatherforecast endpoint,會回傳 Status: 401 Unauthorized 的錯誤訊息。
先登入取得 Token
先加入 Authorization Header,並將登入成功後回傳的 Token 加到 Authorization Header 中。再次送出就可正常的取得所有天氣預測資料了。
使用 OpenApi Swagger 來測試 API
OpenApi Swagger 來測試 API時, 因為 Swagger 測試網頁預設是沒有設定 Token 的功能,必須將程式碼中的 builder.Services.AddSwaggerGen();
改成以下內容
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JwtDemo", Version = "v1" });
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please enter JWT with Bearer into field",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer"}
},
new string[] {}
}
});
});
有了上述的程式設定,當再次 dotnet run 啟動程式後,瀏覽器呈現的 Swagger 畫面右上角會多出了`Authorize` 的按鈕。按下按鈕就是讓你填入登入成功後回傳的 Token
在 Value: 文字框內填入 “Bearer yJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNobWFjLXNoYTUxMiIsInR5cCI6IkpXVCJ9…..",再按下 Authorize 按鈕即表下在接下來的 Request 中都會自動帶入 Token 傳給 WebApi Server。 (請注意 Bearer後再先接著一個空白字元再加上 Token值)
再次執行 “WeatherForecast” 的測試(Execute) 就可正常的取得回傳值了
若要使用 “角色” 的授權檢核,只須將`[Authorize]`改成`[Authorize(Roles = UserRoles.Admin)]`即可
|
|
加入 git 版本控制
$ git commit -m "finished JWT function" -a
使用 aspnet-codegenerator 工具來自動産生程式碼
最後我們來看看如何使用工具來自動産生程式去維護一個新的資料表
在 Models 目錄下新增一個 model(模型) class - ItemData
Models/ItemData.cs
namespace dotnet6_webapi_jwt.Models;
public class ItemData
{
public int Id { get; set; }
public string? Title { get; set; }
public string? Details { get; set; }
public bool Done { get; set; }
}
在 ApiDbContext.cs 中宣告一個 ItemData table
public DbSet<ItemData>? ItemData { get; set; }
新增一個遷移與更新資料庫
$ dotnet build
$ dotnet ef migrations add "Add New Table - ItemData"
$ dotnet ef database update
使用 ASPNET Codegenerator 自動產生 Todo Controller
$ dotnet aspnet-codegenerator controller -name TodoController -async -api -m ItemData -dc ApiDbContext -outDir Controllers
Building project ...
Finding the generator 'controller'...
Running the generator 'controller'...
Minimal hosting scenario!
Attempting to compile the application in memory with the modified DbContext.
Attempting to figure out the EntityFramework metadata for the model and DbContext: 'ItemData'
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 6.0.5 initialized 'ApiDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.5' with options: None
Added Controller : '/Controllers/TodoController.cs'.
RunTime 00:00:10.90
TodoController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using dotnet6_webapi_jwt.Data;
using dotnet6_webapi_jwt.Models;
namespace dotnet6_webapi_jwt.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly ApiDbContext _context;
public TodoController(ApiDbContext context)
{
_context = context;
}
// GET: api/Todo
[HttpGet]
public async Task<ActionResult<IEnumerable<ItemData>>> GetItemData()
{
if (_context.ItemData == null)
{
return NotFound();
}
return await _context.ItemData.ToListAsync();
}
// GET: api/Todo/5
[HttpGet("{id}")]
public async Task<ActionResult<ItemData>> GetItemData(int id)
{
if (_context.ItemData == null)
{
return NotFound();
}
var itemData = await _context.ItemData.FindAsync(id);
if (itemData == null)
{
return NotFound();
}
return itemData;
}
// PUT: api/Todo/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")]
public async Task<IActionResult> PutItemData(int id, ItemData itemData)
{
if (id != itemData.Id)
{
return BadRequest();
}
_context.Entry(itemData).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ItemDataExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/Todo
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost]
public async Task<ActionResult<ItemData>> PostItemData(ItemData itemData)
{
if (_context.ItemData == null)
{
return Problem("Entity set 'ApiDbContext.ItemData' is null.");
}
_context.ItemData.Add(itemData);
await _context.SaveChangesAsync();
return CreatedAtAction("GetItemData", new { id = itemData.Id }, itemData);
}
// DELETE: api/Todo/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteItemData(int id)
{
if (_context.ItemData == null)
{
return NotFound();
}
var itemData = await _context.ItemData.FindAsync(id);
if (itemData == null)
{
return NotFound();
}
_context.ItemData.Remove(itemData);
await _context.SaveChangesAsync();
return NoContent();
}
private bool ItemDataExists(int id)
{
return (_context.ItemData?.Any(e => e.Id == id)).GetValueOrDefault();
}
}
}
測試新功能
在 open api - swagger 網頁上透過 POST 的 EndPoint 新增一筆 Toto list
送出新增的資料,回覆新增成功
由資料庫中可以查詢到新建立的 record
透過 GET 的 EndPoint 也可以查詢到新增 Toto list
以上可以發現使用 ASPNET Codegenerator 自動産生的程式就可簡單的完成資料表格的新增、查詢、修改、刪除等日常功能,真是方便呢!
CORS 議題
Web App 與 Web Api Server 若處於“不同源”時,當 App 使用 http request 呼叫 Web Api Server 上端點時就會有“同源策略“的問題,這個狀況在測試環境中尤為明顯。 要解決這個問題就必須透過 CORS (Cross-Origin Resource Sharing) 相關設定來應對。在 DotNet Core 中設定相關簡易,僅須在 Program.cs 中加入以下二段程式碼即可:
var MyAllOrigins = "allowAll";
builder.Services.AddCors(option =>
option.AddPolicy(name: MyAllOrigins,
policy =>
{
policy.WithOrigins("http://localhost:4200").AllowAnyMethod().AllowAnyHeader();
}
)
);
以及
app.UseCors(MyAllOrigins);
完成後,當前端 Web App (如: angular 在測試環境下預設是使用 localhost:4200) 使用 http request 呼叫後端 Web Api (本例中 Web Api 使用的是 localhost:7087) 時就可避到同源策略的要求。
Program.cs 完整程式碼
using System.Text;
using dotnet6_webapi_jwt.Data;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
ConfigurationManager _configuration = builder.Configuration;
var secret = _configuration.GetValue<string>("JwtSettings:Secret");
// Add services to the container.
builder.Services.AddDbContext<ApiDbContext>(
options => options.UseSqlServer(
_configuration.GetConnectionString("ConnStr")
)
);
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApiDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
// 當驗證失敗時,回應標頭會包含 WWW-Authenticate 標頭,這裡會顯示失敗的詳細錯誤原因
options.IncludeErrorDetails = true; // 預設值為 true,有時會特別關閉
options.TokenValidationParameters = new TokenValidationParameters
{
// 透過這項宣告,就可以從 "NAME" 取值
NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
// 透過這項宣告,就可以從 "Role" 取值,並可讓 [Authorize] 判斷角色
RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
// 驗證 Issuer (一般都會)
ValidateIssuer = true,
ValidIssuer = _configuration.GetValue<string>("JwtSettings:ValidIssuer"),
// 驗證 Audience (通常不太需要)
ValidateAudience = false,
//ValidAudience = = _configuration.GetValue<string>("JwtSettings:ValidAudience"),
// 驗證 Token 的有效期間 (一般都會)
ValidateLifetime = true,
// 如果 Token 中包含 key 才需要驗證,一般都只有簽章而已
ValidateIssuerSigningKey = false,
// 應該從 IConfiguration 取得
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret))
};
});
var MyAllOrigins = "allowAll";
builder.Services.AddCors(option =>
option.AddPolicy(name: MyAllOrigins,
policy =>
{
policy.WithOrigins("http://localhost:4200").AllowAnyMethod().AllowAnyHeader();
}
)
);
// builder.Services.AddAuthorization();
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JwtDemo", Version = "v1" });
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please enter JWT with Bearer into field",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer"}
},
new string[] {}
}
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseCors(MyAllOrigins);
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
``