在本文中,我将演示如何使用ASP.NET Core 3.1构建RESTful Web API,使用Entity Framework Core与现有数据库连接,创建JWT令牌提供对API访问的保护权限。如果您不熟悉.NET Core,可以先阅读我的.NET Core 3.1简介。
这篇文章的各节如下:
- 什么是RESTful API?
- 什么是JWT令牌?
- 添加控制器和脚手架构建RESTful API
- 使用Postman测试API接口
- 使用Cors解决前端调用API跨域的问题
- 创建一个JWT令牌保护我们的API接口
什么是REST API?
由于客户端种类的增加(移动端,基于浏览器的SPA,桌面应用程序,IOT应用程序等),我们需要更好的方法将数据从服务器传输到客户端,而与所用技术和服务器无关。
REST API解决了这个问题。REST代表状态转移。REST API基于HTTP,并为应用程序提供了使用轻量级JSON格式进行通信的功能。它们在Web服务器上运行。本文不着重讲REST API,本博客的其他章节会详细解答。
REST由以下实体组成:
资源:资源是唯一可识别的实体(例如:数据库中的数据,图像或任何数据)。
端点:可以通过URL标识符访问资源。
HTTP方法:HTTP方法是客户端发送到服务器的请求的类型。我们对资源执行的操作应遵循此操作。
HTTP标头:HTTP标头是一个键值对,用于在客户端和服务器之间共享其他信息,例如:
- 发送到服务器的数据类型(JSON,XML)。
- 客户端支持的加密类型。
- 与身份验证相关的令牌。
- 应用程序需要的客户数据。
数据格式:JSON是通过REST API发送和接收数据的通用格式。
什么是JWT令牌?
我们了解了REST API是什么,在这里将了解JWT承载令牌是什么,它是为了保护REST API的。
JWT代表JSON Web Token。它是一种开放标准,为两个实体(客户端和服务器)之间安全地传输数据定义了更好的方法,是目前流行的跨域身份验证解决方案。
JWT由令牌提供者或身份验证服务器者使用密钥进行数字签名。JWT帮助资源服务器使用相同的密钥来验证令牌数据,以便您可以信任数据。
JWT由以下三个部分组成:
Header(标头文件):令牌类型的编码数据和用于对数据进行签名的算法。
{
"alg": "HS256", //签名使用的算法HS256
"typ": "JWT" //typ属性表示令牌的类型
}
Payload(有效荷载):打算共享申明的编码数据,JWT默认提供了以下7个属性设置,也可以自定义属性字段(类似Map中设置)。
默认参数:
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
Signature(签名文件):通过使用秘钥签名(编码的标头+编码的有效荷载)创建。签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
最终的JWT令牌将如下所示:Header.Payload.Signature。Token分为3部分,以. 号分割。下面是令牌的工作流程。
步骤1:客户要求凭证
客户端将请求与必要的信息一起发送到身份验证服务器,以证明其身份。
步骤2:建立凭证
认证服务器接收令牌请求并验证身份。如果发现有效,将创建带有必要声明的令牌(如前所述),并将JWT令牌发送回客户端。
步骤3:客户端将令牌发送到资源服务器
对于对资源或API服务器的每个请求,客户端都需要在标头中包含一个令牌,并使用其URI请求资源。
步骤4:资源服务器验证令牌
请按照以下步骤验证令牌:
- 从身份验证标头中读取令牌。
- 从令牌中拆分标头,有效荷载和签名。
- 用创建令牌时使用的相同密钥创建接收的标头和有效荷载的签名。
- 检查新创建的签名和从令牌接收的签名是否均有效。
- 如果签名相同,则令牌有效(中间未被更改),并且它们提供对所请求资源的访问。
- 如果签名不同,则会将未经授权的响应发送回客户端。(在中间,如果声明受到警告,它们将生成不同的签名,因此将限制资源访问。)
不要使用JWT共享机密信息,因为JWT可以解码,并且可以查看其拥有的声明或数据。
以下部分说明了如何创建REST API并使用令牌对其进行保护。
添加控制器和脚手架构建RESTful API
之前的博客已经写过如何使用创建ASP.NET Core Web API项目,以及如何使用Entity Framework Core 的Code First的迁移机制去同步数据库表字段等,这里就不在进行详述,不清楚的码友转到“NET Core 项目中通过EF Core的Code First方式进行数据库的迁移”进行查看。
右键单击Controllers文件夹,然后选择添加->控制器,在弹出的添加基架的对话框中,选择“使用Entity Framework的API控制器”,然后单击“添加”。
在下一个对话框中,从”模型类"的下拉列表中选择我们定义的"Employee"这个Model类,然后单击+号以添加我们定义的数据库的上下文"EmployeeDbContext"。您可以保留默认控制器的名称,然后点击"添加"。
至此,脚手架工具开始在工作。
自动创建数据库上下文和CRUD(创建,读取,更新和删除)操作的方法被称为scaffolding(脚手架)。
脚手架生成的EmployeesController.cs文件里的内容如下:
namespace EFCoreMigration.Controllers
{[Route("api/[controller]")][ApiController]public class EmployeesController : ControllerBase{private readonly EmployeeDbContext _context;public EmployeesController(EmployeeDbContext context){_context = context;}// GET: api/Employees[HttpGet]public async Task<ActionResult<IEnumerable<Employee>>> GetEmployees(){return await _context.Employees.ToListAsync();}// GET: api/Employees/5[HttpGet("{id}")]public async Task<ActionResult<Employee>> GetEmployee(int id){var employee = await _context.Employees.FindAsync(id);if (employee == null){return NotFound();}return employee;}// PUT: api/Employees/5// To protect from overposting attacks, enable the specific properties you want to bind to, for// more details, see https://go.microsoft.com/fwlink/?linkid=2123754.[HttpPut("{id}")]public async Task<IActionResult> PutEmployee(int id, Employee employee){if (id != employee.EmployeeId){return BadRequest();}_context.Entry(employee).State = EntityState.Modified;try{await _context.SaveChangesAsync();}catch (DbUpdateConcurrencyException){if (!EmployeeExists(id)){return NotFound();}else{throw;}}return NoContent();}// POST: api/Employees// To protect from overposting attacks, enable the specific properties you want to bind to, for// more details, see https://go.microsoft.com/fwlink/?linkid=2123754.[HttpPost]public async Task<ActionResult<Employee>> PostEmployee(Employee employee){_context.Employees.Add(employee);await _context.SaveChangesAsync();return CreatedAtAction("GetEmployee", new { id = employee.EmployeeId }, employee);}// DELETE: api/Employees/5[HttpDelete("{id}")]public async Task<ActionResult<Employee>> DeleteEmployee(int id){var employee = await _context.Employees.FindAsync(id);if (employee == null){return NotFound();}_context.Employees.Remove(employee);await _context.SaveChangesAsync();return employee;}private bool EmployeeExists(int id){return _context.Employees.Any(e => e.EmployeeId == id);}}
}
如上所见,该类由[ApiController]属性装饰。此属性指示控制器响应Web API请求。EmployeesController类从 ControllerBase继承。
使用Postman测试API接口?
选择调试项目为当前项目,并在launchSettings.json文件中配置启动项目的域名applicationUrl默认访问的控制器路由launchUrl。
单击运行将打开一个新的浏览器选项卡,可查看到所有Employee的信息列表。
使用Postman来访问我们的API服务(Postman是一种API测试工具,可以帮助开发人员使用和检查API的工作原理)。
1)GET方法
// GET: api/Employees
[HttpGet]
public async Task<ActionResult<IEnumerable<Employee>>> GetEmployees()
{return await _context.Employees.ToListAsync();
}// GET: api/Employees/5
[HttpGet("{id}")]
public async Task<ActionResult<Employee>> GetEmployee(int id)
{var employee = await _context.Employees.FindAsync(id);if (employee == null){return NotFound();}return employee;
}
GetEmployees
方法返回数据库中的所有员工,而GetEmployees(int id)
方法返回以主键ID作为输入参数的员工。它们用[HttpGet]属性修饰,该属性表示方法响应HTTP GET请求。
这些方法实现了两个GET端点:
- GET api/Employees
- GET api/Employees/{id}
我们可以通过从浏览器调用两个端点来测试接口程序,如下所示:
http://localhost:{port}/api/
Employees
http://localhost:{port}/api/
Employees
/{id}
方法的返回类型GetMovie
为ActionResult <T> type。ASP.NET Core会自动将对象序列化为JSON ,并将JSON写入响应消息的正文中。假定没有未处理的异常,则此返回类型的响应代码为200。未处理的异常会转换为5xx错误。
【查询所有的Employees】步骤1:打开Postman并输入以下端点:
http://localhost:5001/api/Employees
【查询所有的Employees】步骤2:选择GET方法,然后单击Send。现在,所有Employee都将列出,如下所示。
【查询主键ID为1的Employees信息】步骤1:打开Postman并输入以下端点:
http://localhost:5001/api/Employees/1
【查询主键ID为1的Employees信息】步骤2:选择GET方法,然后单击Send。现在,主键ID为1的Employee的详细信息将列出,如下所示。
如果没有项目与请求的ID匹配,则该方法返回404 Not Found错误代码,比如下面ID为10000的员工信息数据库并没有。
2)POST方法
[HttpPost]
public async Task<ActionResult<Employee>> PostEmployee(Employee employee)
{_context.Employees.Add(employee);await _context.SaveChangesAsync();return CreatedAtAction("GetEmployee", new { id = employee.EmployeeId }, employee);
}
PostEmployee
方法在数据库中创建员工记录。上面的代码是HTTP POST方法,由[HttpPost]属性所指示。该方法从HTTP请求的主体获取员工记录的值。
该CreatedAtAction
方法:
- 如果成功,则返回HTTP 201状态代码。HTTP 201是HTTP POST方法的标准响应,该方法在服务器上创建新资源。
- 将
Location
标头添加到响应中。该Location
头指定新创建的员工记录的URI。
【新增一个Employees对象】步骤1:打开Postman并输入以下端点:
3)Put方法
// PUT: api/Employees/5
// To protect from overposting attacks, enable the specific properties you want to bind to, for
// more details, see https://go.microsoft.com/fwlink/?linkid=2123754.
[HttpPut("{id}")]
public async Task<IActionResult> PutEmployee(int id, Employee employee)
{if (id != employee.EmployeeId){return BadRequest();}_context.Entry(employee).State = EntityState.Modified;try{await _context.SaveChangesAsync();}catch (DbUpdateConcurrencyException){if (!EmployeeExists(id)){return NotFound();}else{throw;}}return NoContent();
}
PutEmployee
方法使用数据库中的给定的主键ID更新员工记录。 前面的代码是HTTP PUT方法,由[HttpPut]属性所指示。该方法从HTTP请求的主体获取员工记录的值。您需要在请求网址和正文中提供ID,并且它们必须匹配。根据HTTP规范,PUT请求要求客户端发送整个更新的实体,而不仅仅是更改。
如果操作成功,则响应为204(无内容)
【修改一个Employees对象】步骤1:打开Postman并输入以下端点:
上面修改执行完后,执行ID为7的查询,以下显示的结果可验证是否修改成功。
4)Delete方法
// DELETE: api/Employees/5
[HttpDelete("{id}")]
public async Task<ActionResult<Employee>> DeleteEmployee(int id)
{var employee = await _context.Employees.FindAsync(id);if (employee == null){return NotFound();}_context.Employees.Remove(employee);await _context.SaveChangesAsync();return employee;
}
DeleteEmployee
方法删除数据库中具有给定ID的员工记录。 上面的代码是HTTP DELETE方法,由[HttpDelete]属性所指示。此方法期望URL中的ID标识我们要删除的员工记录。
【删除一个Employees对象】步骤1:打开Postman并输入以下端点:
以下验证是否删除成功:
使用Cors解决前端调用API跨域的问题
首先在Startup.cs的ConfigureServices方法中注册Cors服务
//注册CORS服务
services.AddCors();
然后在Configure方法中启用Cors
//使用中间件启用CORS
//AllowAnyMethod() 允许所有的HTTP方法;AllowAnyHeader() 允许所有的请求标头;AllowAnyOrigin()运行所有的域名
//app.UseCors(builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());//允许单个域名访问
app.UseCors(builder => builder.WithOrigins(new string[] { "http://localhost:4200", "http://localhost:4202" }).AllowAnyMethod().AllowAnyHeader().AllowCredentials());
创建一个JWT令牌保护我们的API接口
我们可以使用Postman来使用和测试我们的API,但是这里的问题是任何知道端点可以使用它的人。因此并非如此,我们需要一个选项来控制谁可以使用我们的服务。这是通过JWT承载令牌实现的。在这里,我们将看到如何创建令牌:
步骤1:将以下JWT的配置粘贴到appsetting.json文件中。
"Jwt": {"key": "abcdefg78hijklmnrobinfiona98mmnnxyz","Issuer": "InventoryAuthenticationServer","Audience": "InvetoryServicePostmanClient","Subject":"InventoryServiceAccessToken"
}
步骤2:在EmployeesController下添加action方法以执行以下操作:
- 接受用户名和密码作为输入。
- 使用数据库检查用户的凭据,以确保用户的身份,这里直接做的判断校验,实际情况要去数据中查询:
- 如果有效,将返回访问令牌。
- 如果无效,将返回错误的请求错误。
以下代码示例演示了如何创建令牌。实际情况下也不应该把生成令牌的代码放到EmployeesController下,应该单独放到登录的地方。这里为了方便操作,先就这样做了,大家根据实际情况来。
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 EFCoreMigration.dbContext;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using Microsoft.AspNetCore.Authorization;namespace EFCoreMigration.Controllers
{[Route("api/[controller]")][ApiController]public class EmployeesController : ControllerBase{private readonly EmployeeDbContext _context;public IConfiguration _configuration;public EmployeesController(IConfiguration config, EmployeeDbContext context){_context = context;_configuration = config;}[HttpPost("Loginon")]public async Task<IActionResult> Loginon(User uModel){if (uModel != null && !string.IsNullOrEmpty(uModel.Name) && !string.IsNullOrEmpty(uModel.Password)){if (uModel.Name == "robin" && uModel.Password == "123"){uModel.UserId = 1001;uModel.Email = "robin@163.com";var claims = new[] {new Claim(JwtRegisteredClaimNames.Sub, _configuration["Jwt:Subject"]),new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString()),new Claim("Id", uModel.UserId.ToString()),new Claim("Name", uModel.Name),new Claim("Email", uModel.Email)};var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));var signIn = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);var token = new JwtSecurityToken(_configuration["Jwt:Issuer"], _configuration["Jwt:Audience"], claims, expires: DateTime.UtcNow.AddDays(1), signingCredentials: signIn);return Ok(new JwtSecurityTokenHandler().WriteToken(token));}else{return BadRequest("Invalid credentials");}}else {return BadRequest();}}}
}
打开Postman调用上面这个接口,可以看到返回了一个令牌。
现在,我们有了一个JWT令牌,接下来操作它是如何保护我们的API的。
步骤1:将以下名称空间添加到启动文件:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
步骤2:在启动文件的configureService方法中配置授权中间件。
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{options.RequireHttpsMetadata = false;options.SaveToken = true;options.TokenValidationParameters = new TokenValidationParameters(){ValidateIssuer = true,ValidateAudience = true,ValidAudience = Configuration["Jwt:Audience"],ValidIssuer = Configuration["Jwt:Issuer"],IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))};
});
我们已经传递了创建令牌时使用的安全密钥,并且还启用了对Issuer和Audience的验证。
另外,我们将SaveToken设置为true,将承载令牌存储在HTTP Context中。因此,我们可以在需要时访问控制器中的令牌。
步骤3:将授权中间件注入Request管道。
步骤4:将授权属性添加到控制器。
在这里,是向整个控制器添加了授权,因此将使用令牌保护此控制器下的所有API。你还可以将授权添加到特定的API方法。
请按照以下步骤测试JWT是否保护API:
步骤1:在Postman中,输入以下端点:http://localhost:5001/api/Orders。选择GET方法,然后单击Send。现在,您可以看到状态代码为401未经授权。
匿名访问已被阻止,并且API已得到保护。现在,我们将看到如何使用令牌访问API。
步骤2:在Headers中添加Authorization,value值是Bearer加上上面访问得到的令牌值,如下所示。再次请求接口。
如上所示,接口访问正常。
- 在后台代码中,当我们将授权标头传递给API时,身份验证中间件将解析并验证令牌。如果发现有效,则将U Identity.IsAuthenticated设置为true。
- 控制器中添加的Authorize属性将检查请求是否已通过身份验证。如果为true,则可以访问API。
- 如果U Identity.IsAuthenticated返回false,则将返回401未经授权的错误。