当前位置: 代码迷 >> 综合 >> (三)使用.NET Core3.1和EF Core构建RESTful API
  详细解决方案

(三)使用.NET Core3.1和EF Core构建RESTful API

热度:89   发布时间:2024-01-30 18:51:25.0

在本文中,我将演示如何使用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}

方法的返回类型GetMovieActionResult <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时,身份验证中间件将解析并验证令牌。如果发现有效,则将Identity.IsAuthenticated设置为true
  • 控制器中添加的Authorize属性将检查请求是否已通过身份验证。如果为true,则可以访问API。
  • 如果Identity.IsAuthenticated返回false,则将返回401未经授权的错误。
  相关解决方案