使用 .NET 5.0 Web API 和 Microsoft SQL Server 构建待办事项列表应用程序
介绍
本文展示了如何使用 .Net 5 Web API、JWT 身份验证和 AspNetCore Identity 构建待办事项列表应用程序。Microsoft SQL Server 用于查看数据库和表。这是 2 部分教程的第 1 部分。第 2 部分将使用 Angular 创建 ToDo 列表应用程序的前端。本文首先说明用户情景,展示用户希望如何使用该应用程序。
本文将展示构建完整的 ToDo 应用程序后端所需的所有必要步骤。最后,本文演示了只有登录用户才能访问 ToDo 列表端点。
工具
- Visual Studio 2019
- Microsoft SQL Server
使用情景
作为用户,我想注册使用待办事项应用程序 |
用户应该能够注册她/他的凭据才能使用待办事项应用程序。 |
作为用户,我想登录以使用待办事项应用程序 |
用户应该能够创建一个新的待办事项,该项目应该有一个名称和描述。 |
作为用户,我想创建一个新的待办事项 |
用户应该能够创建一个新的待办事项,该项目应该有一个名称和描述。 |
作为用户,我想编辑现有项目 |
用户应该能够编辑现有的待办事项,用户应该能够编辑名称和描述。 |
作为用户,我想查看我所有的待办事项 |
用户应该能够查看他/她的所有待办事项。 |
作为用户,我想更新待办事项的状态 |
作为用户,我想将待办事项标记为已完成。 |
使用 Visual Studio 2019 community 创建一个新项目。
选择 ASP.NET Core Web API 模板。下一步将是提供项目名称并为项目选择位置的选项。
选择 .NET 5.0 作为目标框架,nose 作为身份验证类型字段,并取消选择 HTTPS 配置。然后单击创建以创建项目。
项目模板将具有这种结构。
使用 NuGet 包管理器安装以下包的最新版本。NuGet 包管理器可以通过右键单击项目找到,在此示例中为 ToDoAPI,然后选择管理 NuGet 包。
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
- Microsoft.AspNetCore.Identity.EntityFrameworkCore
- Microsoft.AspNetCore.Identity
- Microsoft.AspNetCore.Authentication.JwtBearer
- Microsoft.VisualStudio.Web.CodeGeneration.Design
创建一个 Authentication 文件夹,其中将包含一个 ApplicationUser.cs 类,该类将继承 IdentityUser 类和 Response.cs 类,当用户注册或登录到应用程序时,该类将返回一条消息和一个状态代码。IdentityUser 类是 AspNetCore Identity 的一部分。
ApplicationUser.cs
using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ToDoAPI.Authentication
{
public class ApplicationUser: IdentityUser
{
}
}
Response.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ToDoAPI.Authentication
{
public class Response
{
public string Status { get; set; }
public string Message { get; set; }
}
}
创建一个模型文件夹 Models,其中将包含用于用户注册的 RegisterModel.cs 类、用于用户登录的 LoginModel.cs 类、用于用户角色的 UserRoles.cs 和用于待办事项的 ToDoItemModel.cs。它还将包含 ApplicationDbContext.cs 文件,该文件将模型映射到将通过迁移创建的表。
RegisterModel.cs、LoginModel.cs 和 UserRoles.cs 将绑定到身份表。这意味着当用户注册和登录应用程序时,仅需要模型中描述的字段。角色将显示用户可以拥有的角色,例如“admin”。
RegisterModel.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace ToDoAPI.Models
{
public class RegisterModel
{
[Required(ErrorMessage = "Username is required")]
public string Username { get; set; }
[RegularExpression(@"^[\w!#$%&'*+\-/=?\^_`{|}~]+(\.[\w!#$%&'*+\-/=?\^_`{|}~]+)*"
+ "@"
+ @"((([\-\w]+\.)+[a-zA-Z]{2,4})|(([0-9]{1,3}\.){3}[0-9]{1,3}))$",
ErrorMessage = "You have entered an invalid email address")]
[Required(ErrorMessage = "Email is required")]
public string Email { get; set; }
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$",
ErrorMessage = "Minimum eight characters, at least one uppercase letter, one lowercase letter, one number and one special character")]
[Required(ErrorMessage = "Password is required")]
public string Password { get; set; }
}
}
LoginModel.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace ToDoAPI.Models
{
public class LoginModel
{
[Required(ErrorMessage = "Username is required")]
public string Username { get; set; }
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$",
ErrorMessage = "Minimum eight characters, at least one uppercase letter, one lowercase letter, " +
"one number and one special character")]
[Required(ErrorMessage = "Password is required")]
public string Password { get; set; }
}
}
UserRoles.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ToDoAPI.Models
{
public class UserRoles
{
public const string Admin = "Admin";
public const string User = "User";
}
}
ToDoItemModel.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;
namespace ToDoAPI.Models
{
public class ToDoItemModel
{
[Key]
public int ItemId { get; set; }
[Required(ErrorMessage = "ItemName is required")]
[Column(TypeName = "nvarchar(100)")]
public string ItemName { get; set; }
[Required(ErrorMessage = "ItemDescription is required")]
[Column(TypeName = "nvarchar(100)")]
public string ItemDescription { get; set; }
[Required(ErrorMessage = "ItemStatus is required")]
[Column(TypeName = "bit")]
public bool ItemStatus { get; set; }
}
}
ApplicationDbContext.cs
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ToDoAPI.Authentication;
namespace ToDoAPI.Models
{
public class ApplicationDbContext: IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
public DbSet<ToDoItemModel> ToDoItems { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<ToDoItemModel>(entity =>
{
entity.Property(e => e.ItemName)
.IsRequired()
.HasMaxLength(100);
entity.Property(e => e.ItemDescription)
.IsRequired()
.HasMaxLength(100);
entity.Property(e => e.ItemStatus)
.IsRequired()
.HasMaxLength(1);
});
base.OnModelCreating(builder);
}
}
}
项目的文件夹结构现在如下所示。
修改 appsettings.json 文件,添加连接字符串和 jwt token 秘密字符串,valid issuer,后端服务器的端口,valid Audience,前端服务器的端口。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"SQLConnection": "Server=.;Database=ToDoDB;Trusted_Connection=True;Integrated Security=true"
},
"JWT": {
"ValidAudience": "http: //localhost:4200",
"ValidIssuer": "http://localhost:24288",
"Secret": "MySecretStringMuuustBeVeeeeeeeeeeryLooooooooOng"
}
}
在 Startup.cs 文件的 ConfigureServices 方法中添加 DbContext 并显示应用程序使用 SQL Server(它也可以使用 MySQL、Postgres 等)并添加 appsettings.json 文件中描述的连接字符串,添加JwtBearer 身份验证,并添加 AspNetCore Identity,如下所示。在 Startup.cs 文件的 Configure 方法中添加应用程序使用身份验证。
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ToDoAPI.Authentication;
using ToDoAPI.Models;
namespace ToDoAPI
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "ToDoAPI", Version = "v1" });
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("SQLConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = Configuration["JWT:ValidAudience"],
ValidIssuer = Configuration["JWT:ValidIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"]))
};
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ToDoAPI v1"));
}
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
在控制器内部,该文件夹创建了一个 Web API 控制器身份验证。这是通过右键单击 Controllers 文件夹来完成的,选择添加,从下拉列表中选择控制器,然后选择 API Controller – Empty。
AuthenticationController.cs
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using ToDoAPI.Authentication;
using ToDoAPI.Models;
namespace ToDoAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthenticationController : ControllerBase
{
private readonly UserManager<ApplicationUser> userManager;
private readonly RoleManager<IdentityRole> roleManager;
private readonly IConfiguration _configuration;
public AuthenticationController(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IConfiguration configuration)
{
this.userManager = userManager;
this.roleManager = roleManager;
_configuration = configuration;
}
[HttpPost]
[Route("login")]
public async Task<IActionResult> Login([FromBody] LoginModel model)
{
var user = await userManager.FindByNameAsync(model.Username);
if (user != null && await userManager.CheckPasswordAsync(user, model.Password))
{
var userRoles = await userManager.GetRolesAsync(user);
var authClaims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
foreach (var userRole in userRoles)
{
authClaims.Add(new Claim(ClaimTypes.Role, userRole));
}
var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));
var token = new JwtSecurityToken(
issuer: _configuration["JWT:ValidIssuer"],
audience: _configuration["JWT:ValidAudience"],
expires: DateTime.Now.AddHours(3),
claims: authClaims,
signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
);
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.FindByNameAsync(model.Username);
if (userExists != null)
{
return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User already exists!" });
};
ApplicationUser user = new ApplicationUser()
{
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.FindByNameAsync(model.Username);
if (userExists != null)
{
return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User already exists!" });
};
ApplicationUser user = new ApplicationUser()
{
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);
}
return Ok(new Response { Status = "Success", Message = "User created successfully" });
}
}
}
创建一个 Web API 控制器 ToDoItem。这是通过右键单击 Controllers 文件夹来完成的,选择添加,从下拉列表中选择“新建脚手架项”,然后使用实体框架选择带有操作的 API 控制器。
为模型类选择 ToDoItemModel,为数据上下文类选择 ApplicationDbContext,并选择控制器名称。
ToDoItemController.cs
添加 [Authorize] 以允许唯一已登录并具有有效 jwt 令牌的用户访问 ToDoItem API。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ToDoAPI.Models;
namespace ToDoAPI.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class ToDoItemController : ControllerBase
{
private readonly ApplicationDbContext _context;
public ToDoItemController(ApplicationDbContext context)
{
_context = context;
}
// GET: api/ToDoItem
[HttpGet]
public async Task<ActionResult<IEnumerable<ToDoItemModel>>> GetToDoItems()
{
return await _context.ToDoItems.ToListAsync();
}
// GET: api/ToDoItem/5
[HttpGet("{id}")]
public async Task<ActionResult<ToDoItemModel>> GetToDoItemModel(int id)
{
var toDoItemModel = await _context.ToDoItems.FindAsync(id);
if (toDoItemModel == null)
{
return NotFound();
}
return toDoItemModel;
}
// PUT: api/ToDoItem/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")]
public async Task<IActionResult> PutToDoItemModel(int id, ToDoItemModel toDoItemModel)
{
if (id != toDoItemModel.ItemId)
{
return BadRequest();
}
_context.Entry(toDoItemModel).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ToDoItemModelExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/ToDoItem
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost]
public async Task<ActionResult<ToDoItemModel>> PostToDoItemModel(ToDoItemModel toDoItemModel)
{
_context.ToDoItems.Add(toDoItemModel);
await _context.SaveChangesAsync();
return CreatedAtAction("GetToDoItemModel", new { id = toDoItemModel.ItemId }, toDoItemModel);
}
// DELETE: api/ToDoItem/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteToDoItemModel(int id)
{
var toDoItemModel = await _context.ToDoItems.FindAsync(id);
if (toDoItemModel == null)
{
return NotFound();
}
_context.ToDoItems.Remove(toDoItemModel);
await _context.SaveChangesAsync();
return NoContent();
}
private bool ToDoItemModelExists(int id)
{
return _context.ToDoItems.Any(e => e.ItemId == id);
}
}
}
使用包管理器控制台中的“add-migration”命令创建迁移脚本。
在项目中创建了一个迁移文件夹。项目的文件夹结构现在如下所示。
使用包管理器控制台中的“update-database”命令创建数据库和表。使用 Microsoft SQL Server 对象资源管理器查看创建的数据库和表。
使用Postman测试 API。如果您在注册应用程序之前尝试访问 ToDoItem,您将获得 401 未经授权的状态代码。
用户注册后,用户可以登录并获取有效的 JWT 令牌。
然后可以使用它来访问 ToDoItem Web API。
结论
在本文中,我展示了如何使用 .Net 5.0 构建待办事项列表应用程序后端。我还展示了如何使用 JWT 令牌仅授权登录用户使用 ToDo 应用程序。在下一篇文章中,我们将使用 Angular 实现前端。您可以在我的GitHub 存储库中找到源代码 。
常见问题FAQ
- 程序仅供学习研究,请勿用于非法用途,不得违反国家法律,否则后果自负,一切法律责任与本站无关。
- 请仔细阅读以上条款再购买,拍下即代表同意条款并遵守约定,谢谢大家支持理解!
