C# - 基于.NetCore开发的“寸金游”(后端开发)(第二部分)
十一、 【单点登录】JWT与用户身份验证
JWT原理剖析
JWT是干什么用的?
- JSON Web Token
- JWT的作用是用户授权(Athorization),而不是用户的身份认证(Authentication)。
授权 ?认证
传统的Session登录
- 用户登录后,服务器会保存登陆的session信息。
- Session ID会通过cookie传递给前端。
- http请求会附带cookie。
- 有状态登录。
JWT彻底改变了用户授权与认证的过程
- 替换cookie
- JWT信息只需要保存在客户端
- 无状态登录
有状态登录 ?无状态登录
有状态登录(session)
无状态登录(JWT)
有状态登录 ?无状态登录
- Session需要保存在服务器上,而Session则保存在cookie中。
- JWT信息只需要保存在客户端。
- 无状态登陆优势∶分布式部署。
JWT与单点登录实例解释
JWT的原理
- JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。
- { "姓名": "张三", "角色": "管理员", "到期时间": "2018年7月1日0点0分" }
- 以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。
- 为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。
- 服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
区别
- session 存储在服务端占用服务器资源,而 JWT 存储在客户端。
- session 存储在 Cookie 中,存在伪造跨站请求伪造攻击的风险。
- session 只存在一台服务器上,那么下次请求就必须请求这台服务器,不利于分布式应用。
- 存储在客户端的 JWT 比存储在服务端的 session 更具有扩展性。
常用的单点登录(SSO)系统
JWT的优点
- 无状态,简单、方便,完美支持分布式部署。
- 非对称加密,Token安全性高。
JWT的缺点
- 无状态,token一经发布则无法取消。(无解)
- 明文传递,Token安全性低。(使用Https即可解决)
启用无状态登陆系统(模拟用户登陆)
流程
- 使用用户名和密码登陆,获得JWT。
- 用户凭借JWT访问数据资源。
- 模拟用户登陆。
安装JWT框架Microsoft.AspNetCore.Authentication.JwtBearer
新建身份认证控制器AuthenticateController.cs
using _02NET___CJ_ASP_Travel.Dtos;
using Microsoft.AspNetCore.Authorization;
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;
namespace _02NET___CJ_ASP_Travel.Controllers
{
[ApiController]
[Route("auth")]
public class AuthenticateController : ControllerBase
{
private readonly IConfiguration _configuration;
public AuthenticateController(IConfiguration configuration)
{
_configuration = configuration;
}
[AllowAnonymous]
[HttpPost("login")]
public IActionResult login([FromBody] LoginDto loginDto)
{
// 1 验证用户名密码
// 2 创建jwt
// header
var signingAlgorithm = SecurityAlgorithms.HmacSha256;
// payload
var claims = new[]
{
// sub
new Claim(JwtRegisteredClaimNames.Sub, "fake_user_id")
};
// signiture
var secretByte = Encoding.UTF8.GetBytes(_configuration["Authentication:SecretKey"]);
var signingKey = new SymmetricSecurityKey(secretByte);
var signingCredentials = new SigningCredentials(signingKey, signingAlgorithm);
var token = new JwtSecurityToken(
issuer: _configuration["Authentication:Issuer"],
audience: _configuration["Authentication:Audience"],
claims,
notBefore: DateTime.UtcNow,
expires: DateTime.UtcNow.AddDays(1),
signingCredentials
);
var tokenStr = new JwtSecurityTokenHandler().WriteToken(token);
// 3 return 200 ok + jwt
return Ok(tokenStr);
}
}
}
创建LoginDto.cs处理json数据反序列化
namespace _02NET___CJ_ASP_Travel.Dtos
{
public class LoginDto
{
[Required]
public string Email { get; set; }
[Required]
public string Password { get; set; }
}
}
私钥可以保存在配置文件中appsettings.json
"Authentication": {
"SecretKey": "Key",
"Issuer": "baidu.com",
"Audience": "baidu.com"
}
启动API授权
注册身份认证服务(Startup.cs)ConfigureServices
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
var secretByte = Encoding.UTF8.GetBytes(Configuration["Authentication:SecretKey"]);
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidIssuer = Configuration["Authentication:Issuer"],
ValidateAudience = true,
ValidAudience = Configuration["Authentication:Audience"],
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(secretByte)
};
});
服务启动配置Configure
// 你在哪?
app.UseRouting();
// 你是谁?
app.UseAuthentication();
// 你可以干什么?有什么权限?
app.UseAuthorization();
TouristRoutesController.cs - CreateTouristRoute方法
- 加上
[Authorize]
[HttpPost]
[Authorize]
//创建旅游路线
public async Task<IActionResult> CreateTouristRoute([FromBody] TouristRouteForCreationDto touristRouteForCreationDto)
添加用户角色
Claim
- 资源的所有权。
- 表述用户的身份、说明用户的角色、表示用户所具有的权限。
- 最小不可分割单位,使用的灵活度相当高,可以自由组合。
在开发网站的时候
- 验证与授权完全分开。
- 使用的是基于Claims的身份认证体系。
- JWT只是Claim的其中一种应用方式而已。
AuthenticateController.cs
- 新增管理员身份
new Claim(ClaimTypes.Role, "Admin")
// payload
var claims = new[]
{
// sub
new Claim(JwtRegisteredClaimNames.Sub, "fake_user_id")
new Claim(ClaimTypes.Role, "Admin")
};
TouristRoutesController.cs - CreateTouristRoute方法
[Authorize]
内加上(Roles = "Admin")
- 这样就只有网站管理员才有权限访问该Api。
[HttpPost]
[Authorize(Roles = "Admin")]
//创建旅游路线
public async Task<IActionResult> CreateTouristRoute([FromBody] TouristRouteForCreationDto touristRouteForCreationDto)
用户模型设计与数据库更新
安装microsoft.aspnetcore.identity.entityframeworkcore
AppDbContext.cs
IdentityDbContext
代替DbContext
继承
public class AppDbContext :IdentityDbContext<IdentityUser> /*: DbContext*/
添加服务依赖(Startup.cs)ConfigureServices
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>();
进行数据迁移工作
Package Manage Console (PMC)
- 如果报错,请降低包版本。
add-migration initialMigration
update-database
以Asp开头的表即为刚刚数据迁移工作所建立的数据表
用户注册
api
localhost/auth/register
AuthenticateController.cs
private readonly UserManager<IdentityUser> _userManager;
//用户注册
[AllowAnonymous]
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterDto registerDto)
{
// 1 使用用户名创建用户对象
var user = new IdentityUser()
{
UserName = registerDto.Email,
Email = registerDto.Email
};
// 2 hash密码,保存用户
var result = await _userManager.CreateAsync(user, registerDto.Password);
if (!result.Succeeded)
{
return BadRequest();
}
// 3 return
return Ok();
}
注入服务依赖
//注入服务依赖
public AuthenticateController(
IConfiguration configuration,
UserManager<IdentityUser> userManager
)
{
_configuration = configuration;
_userManager = userManager;
}
创建RegisterDto
using System.ComponentModel.DataAnnotations;
namespace _02NET___CJ_ASP_Travel.Dtos
{
public class RegisterDto
{
[Required]
public string Email { get; set; }
[Required]
public string Password { get; set; }
[Required]
[Compare(nameof(Password), ErrorMessage = "密码输入不一致")]
public string ConfirmPassword { get; set; }
}
}
用户登陆
添加服务依赖
private readonly SignInManager<IdentityUser> _signInManager;
//注入服务依赖
public AuthenticateController(
IConfiguration configuration,
UserManager<IdentityUser> userManager,
SignInManager<IdentityUser> signInManager
)
{
_configuration = configuration;
_userManager = userManager;
_signInManager = signInManager;
}
验证用户名密码(AuthenticateController.cs)
[AllowAnonymous]
[HttpPost("login")]
public async Task<IActionResult> login([FromBody] LoginDto loginDto)
{
// 1 验证用户名密码
var loginResult = await _signInManager.PasswordSignInAsync(
loginDto.Email,
loginDto.Password,
false,
false
);
if (!loginResult.Succeeded)
{
return BadRequest();
}
var user = await _userManager.FindByNameAsync(loginDto.Email);
//----------------省略
"fake_user_id"
修改为user.Id
// payload
var claims = new List<Claim>
{
// sub
new Claim(JwtRegisteredClaimNames.Sub, user.Id),
new Claim(ClaimTypes.Role, "Admin")
};
var roleNames = await _userManager.GetRolesAsync(user);
foreach (var roleName in roleNames)
{
var roleClaim = new Claim(ClaimTypes.Role, roleName);
claims.Add(roleClaim);
}
给所有api添加上 [Authorize(AuthenticationSchemes = "Bearer")]
namespace FakeXiecheng.API.Controllers
{
[Route("api/[controller]")] // api/touristroute
[ApiController]
public class TouristRoutesController : ControllerBase
{
private ITouristRouteRepository _touristRouteRepository;
private readonly IMapper _mapper;
public TouristRoutesController(
ITouristRouteRepository touristRouteRepository,
IMapper mapper
)
{
_touristRouteRepository = touristRouteRepository;
_mapper = mapper;
}
// api/touristRoutes?keyword=传入的参数
[HttpGet]
[HttpHead]
public async Task<IActionResult> GerTouristRoutes(
[FromQuery] TouristRouteResourceParamaters paramaters
//[FromQuery] string keyword,
//string rating // 小于lessThan, 大于largerThan, 等于equalTo lessThan3, largerThan2, equalTo5
)// FromQuery vs FromBody
{
var touristRoutesFromRepo = await _touristRouteRepository.GetTouristRoutesAsync(paramaters.Keyword, paramaters.RatingOperator, paramaters.RatingValue);
if (touristRoutesFromRepo == null || touristRoutesFromRepo.Count() <= 0)
{
return NotFound("没有旅游路线");
}
var touristRoutesDto = _mapper.Map<IEnumerable<TouristRouteDto>>(touristRoutesFromRepo);
return Ok(touristRoutesDto);
}
// api/touristroutes/{touristRouteId}
[HttpGet("{touristRouteId}", Name = "GetTouristRouteById")]
public async Task<IActionResult> GetTouristRouteById(Guid touristRouteId)
{
var touristRouteFromRepo = await _touristRouteRepository.GetTouristRouteAsync(touristRouteId);
if (touristRouteFromRepo == null)
{
return NotFound($"旅游路线{touristRouteId}找不到");
}
var touristRouteDto = _mapper.Map<TouristRouteDto>(touristRouteFromRepo);
return Ok(touristRouteDto);
}
[HttpPost]
[Authorize(AuthenticationSchemes = "Bearer")]
[Authorize]
public async Task<IActionResult> CreateTouristRoute([FromBody] TouristRouteForCreationDto touristRouteForCreationDto)
{
var touristRouteModel = _mapper.Map<TouristRoute>(touristRouteForCreationDto);
_touristRouteRepository.AddTouristRoute(touristRouteModel);
await _touristRouteRepository.SaveAsync();
var touristRouteToReture = _mapper.Map<TouristRouteDto>(touristRouteModel);
return CreatedAtRoute(
"GetTouristRouteById",
new { touristRouteId = touristRouteToReture.Id },
touristRouteToReture
);
}
[HttpPut("{touristRouteId}")]
[Authorize(AuthenticationSchemes = "Bearer")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> UpdateTouristRoute(
[FromRoute] Guid touristRouteId,
[FromBody] TouristRouteForUpdateDto touristRouteForUpdateDto
)
{
if (!(await _touristRouteRepository.TouristRouteExistsAsync(touristRouteId)))
{
return NotFound("旅游路线找不到");
}
var touristRouteFromRepo = await _touristRouteRepository.GetTouristRouteAsync(touristRouteId);
// 1. 映射dto
// 2. 更新dto
// 3. 映射model
_mapper.Map(touristRouteForUpdateDto, touristRouteFromRepo);
await _touristRouteRepository.SaveAsync();
return NoContent();
}
[HttpPatch("{touristRouteId}")]
[Authorize(AuthenticationSchemes = "Bearer")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> PartiallyUpdateTouristRoute(
[FromRoute] Guid touristRouteId,
[FromBody] JsonPatchDocument<TouristRouteForUpdateDto> patchDocument
)
{
if (!(await _touristRouteRepository.TouristRouteExistsAsync(touristRouteId)))
{
return NotFound("旅游路线找不到");
}
var touristRouteFromRepo = await _touristRouteRepository.GetTouristRouteAsync(touristRouteId);
var touristRouteToPatch = _mapper.Map<TouristRouteForUpdateDto>(touristRouteFromRepo);
patchDocument.ApplyTo(touristRouteToPatch, ModelState);
if (!TryValidateModel(touristRouteToPatch))
{
return ValidationProblem(ModelState);
}
_mapper.Map(touristRouteToPatch, touristRouteFromRepo);
await _touristRouteRepository.SaveAsync();
return NoContent();
}
[HttpDelete("{touristRouteId}")]
[Authorize(AuthenticationSchemes = "Bearer")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> DeleteTouristRoute([FromRoute] Guid touristRouteId)
{
if (!(await _touristRouteRepository.TouristRouteExistsAsync(touristRouteId)))
{
return NotFound("旅游路线找不到");
}
var touristRoute = await _touristRouteRepository.GetTouristRouteAsync(touristRouteId);
_touristRouteRepository.DeleteTouristRoute(touristRoute);
await _touristRouteRepository.SaveAsync();
return NoContent();
}
[HttpDelete("({touristIDs})")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> DeleteByIDs(
[ModelBinder(BinderType = typeof(ArrayModelBinder))][FromRoute] IEnumerable<Guid> touristIDs)
{
if (touristIDs == null)
{
return BadRequest();
}
var touristRoutesFromRepo = await _touristRouteRepository.GetTouristRoutesByIDListAsync(touristIDs);
_touristRouteRepository.DeleteTouristRoutes(touristRoutesFromRepo);
await _touristRouteRepository.SaveAsync();
return NoContent();
}
}
}
定制用户模型并添加初始化用户数据
创建自定义用户模型ApplicationUser.cs
namespace _03NET___CJ_ASP_Travel3.Models
{
public class ApplicationUser : IdentityUser
{
public string Address { get; set; }
// ShoppingCart
// Order
public virtual ICollection<IdentityUserRole<string>> UserRoles { get; set; }
}
}
初始化用户与角色的种子数据(AppDbContext.cs)
// 初始化用户与角色的种子数据
// 1. 更新用户与角色的外键关系
modelBuilder.Entity<ApplicationUser>(b => {
b.HasMany(x => x.UserRoles)
.WithOne()
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});
// 2. 添加角色
var adminRoleId = "308660dc-ae51-480f-824d-7dca6714c3e2"; // guid
modelBuilder.Entity<IdentityRole>().HasData(
new IdentityRole
{
Id = adminRoleId,
Name = "Admin",
NormalizedName = "Admin".ToUpper()
}
);
// 3. 添加用户
var adminUserId = "90184155-dee0-40c9-bb1e-b5ed07afc04e";
ApplicationUser adminUser = new ApplicationUser
{
Id = adminUserId,
UserName = "admin@fakexiecheng.com",
NormalizedUserName = "admin@fakexiecheng.com".ToUpper(),
Email = "admin@fakexiecheng.com",
NormalizedEmail = "admin@fakexiecheng.com".ToUpper(),
TwoFactorEnabled = false,
EmailConfirmed = true,
PhoneNumber = "123456789",
PhoneNumberConfirmed = false
};
PasswordHasher<ApplicationUser> ph = new PasswordHasher<ApplicationUser>();
adminUser.PasswordHash = ph.HashPassword(adminUser, "Fake123$");
modelBuilder.Entity<ApplicationUser>().HasData(adminUser);
// 4. 给用户加入管理员权限
// 通过使用 linking table:IdentityUserRole
modelBuilder.Entity<IdentityUserRole<string>>()
.HasData(new IdentityUserRole<string>()
{
RoleId = adminRoleId,
UserId = adminUserId
});
base.OnModelCreating(modelBuilder);
数据更新
Package Manage Console (PMC)
- 如果报错,请降低包版本。
add-migration initialMigration
update-database
十二、【购物系统从0到1】功能完整的购物车开发
开发概要与接口设计
如何设计我们的购物车
与其他系统产生的关联关系
购物车与其他系统的关系
- 购物车与会员系统
- 购物车与商品系统
- 购物车与价格系统
- 购物车与订单系统
购物车基本功能
Restful风格的购物车api接口设计
购物车模型设计与数据库更新
购物系统的流程
Lineltem
使其联动起来。
模型优先
(数据模型)Lineltem.cs
namespace _03NET___CJ_ASP_Travel3.Models
{
public class LineItem
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[ForeignKey("TouristRouteId")]
public Guid TouristRouteId { get; set; }
public TouristRoute TouristRoute { get; set; }
public Guid? ShoppingCartId { get; set; }
//public Guid? OrderId { get; set; }
[Column(TypeName = "decimal(18, 2)")]
public decimal OriginalPrice { get; set; }
[Range(0.0, 1.0)]
public double? DiscountPresent { get; set; }
}
}
ShoppingCart.cs
namespace _03NET___CJ_ASP_Travel3.Models
{
public class ShoppingCart
{
[Key]
public Guid Id { get; set; }
public string UserId { get; set; }
public ApplicationUser User { get; set; }
public ICollection<LineItem> ShoppingCartItems { get; set; }
}
}
添加购物车的外键关系(ApplicationUser.cs)
// ShoppingCart
public ShoppingCart ShoppingCart { get; set; }
添加引用(AppDbContext.cs)
public DbSet<ShoppingCart> ShoppingCarts { get; set; }
public DbSet<LineItem> LineItems { get; set; }
数据更新
Package Manage Console (PMC)
- 如果报错,请降低包版本。
add-migration initialMigration
update-database
获得当前用户的购物车
新建ShoppingCartController.cs
using _04NET___CJ_ASP_Travel4.Services;
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace _04NET___CJ_ASP_Travel4.Controllers
{
[ApiController]
[Route("api/shoppingCart")]
public class ShoppingCartController : ControllerBase
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ITouristRouteRepository _touristRouteRepository;
private readonly IMapper _mapper;
public ShoppingCartController(
IHttpContextAccessor httpContextAccessor,
ITouristRouteRepository touristRouteRepository,
IMapper mapper
)
{
_httpContextAccessor = httpContextAccessor;
_touristRouteRepository = touristRouteRepository;
_mapper = mapper;
}
[HttpGet]
[Authorize(AuthenticationSchemes = "Bearer")]
public async Task<IActionResult> GetShoppingCart()
{
// 1 获得当前用户
var userId = _httpContextAccessor
.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
// 2 使用userid获得购物车
var shoppingCart = await _touristRouteRepository.GetShoppingCartByUserId(userId);
return Ok(_mapper.Map<ShoppingCartDto>(shoppingCart));
}
}
}
新增接口函数(ITouristRouteRepository.cs)
Task<ShoppingCart> GetShoppingCartByUserId(string userId);
Task CreateShoppingCart(ShoppingCart shoppingCart);
实现接口 TouristRouteRepository.cs
public async Task<ShoppingCart> GetShoppingCartByUserId(string userId)
{
return await _context.ShoppingCarts
.Include(s => s.User)
.Include(s => s.ShoppingCartItems).ThenInclude(li => li.TouristRoute)
.Where(s => s.UserId == userId)
.FirstOrDefaultAsync();
}
public async Task CreateShoppingCart(ShoppingCart shoppingCart)
{
await _context.ShoppingCarts.AddAsync(shoppingCart);
}
初始化用户购物车(AuthenticateController.cs)
private readonly ITouristRouteRepository _touristRouteRepository;
public AuthenticateController(
IConfiguration configuration,
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
ITouristRouteRepository touristRouteRepository
)
{
_configuration = configuration;
_userManager = userManager;
_signInManager = signInManager;
_touristRouteRepository = touristRouteRepository;
}
//-----------------------省略
// 3 初始化购物车
var shoppingCart = new ShoppingCart()
{
Id = Guid.NewGuid(),
UserId = user.Id
};
await _touristRouteRepository.CreateShoppingCart(shoppingCart);
await _touristRouteRepository.SaveAsync();
// 4 return
return Ok();
}
}
}
ShoppingCartDto
namespace _04NET___CJ_ASP_Travel4.Dtos
{
public class ShoppingCartDto
{
public Guid Id { get; set; }
public string UserId { get; set; }
public ICollection<LineItemDto> ShoppingCartItems { get; set; }
}
}
新建LineItemDto
namespace _04NET___CJ_ASP_Travel4.Dtos
{
public class LineItemDto
{
public int Id { get; set; }
public Guid TouristRouteId { get; set; }
public TouristRouteDto TouristRoute { get; set; }
public Guid? ShoppingCartId { get; set; }
//public Guid? OrderId { get; set; }
public decimal OriginalPrice { get; set; }
public double? DiscountPresent { get; set; }
}
}
新建ShoppingCartProfile.cs
namespace _04NET___CJ_ASP_Travel4.Profiles
{
public class ShoppingCartProfile : Profile
{
public ShoppingCartProfile()
{
CreateMap<ShoppingCart, ShoppingCartDto>();
CreateMap<LineItem, LineItemDto>();
}
}
}
测试
注册用户
https://localhost:44381/auth/register
{
"email": "alex1234@163.com",
"password":"Fake123&",
"ConfirmPassword":"Fake123&"
}
用户登录
https://localhost:44381/auth/login
{
"email": "alex1234@163.com",
"password":"Fake123&"
}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2YzQwY2U4NS04NTQ0LTRkYzItOGE5NS1iNTU1MzVhNjgwNmMiLCJuYmYiOjE2NzU1Nzk1MTYsImV4cCI6MTY3NTY2NTkxNiwiaXNzIjoiYmFpZHUuY29tIiwiYXVkIjoiYmFpZHUuY29tLmNvbSJ9.Kw4NiXbnJEp2AjivnDKdgW8bj6btjsS_M2wT91j2YsQ
请求https://localhost:44381/api/shoppingCart
向购物车加入商品
添加商品函数ShoppingCartController.cs
[HttpPost("items")]
[Authorize(AuthenticationSchemes = "Bearer")]
public async Task<IActionResult> AddShoppingCartItem(
[FromBody] AddShoppingCartItemDto addShoppingCartItemDto
)
{
// 1 获得当前用户
var userId = _httpContextAccessor
.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
// 2 使用userid获得购物车
var shoppingCart = await _touristRouteRepository
.GetShoppingCartByUserId(userId);
// 3 创建lineItem
var touristRoute = await _touristRouteRepository
.GetTouristRouteAsync(addShoppingCartItemDto.TouristRouteId);
if(touristRoute == null)
{
return NotFound("旅游路线不存在");
}
var lineItem = new LineItem()
{
TouristRouteId = addShoppingCartItemDto.TouristRouteId,
ShoppingCartId = shoppingCart.Id,
OriginalPrice = touristRoute.OriginalPrice,
DiscountPresent = touristRoute.DiscountPresent
};
// 4 添加lineitem,并保存数据库
await _touristRouteRepository.AddShoppingCartItem(lineItem);
await _touristRouteRepository.SaveAsync();
return Ok(_mapper.Map<ShoppingCartDto>(shoppingCart));
}
新建AddShoppingCartItemDto.cs
namespace _04NET___CJ_ASP_Travel4.Dtos
{
public class AddShoppingCartItemDto
{
public Guid TouristRouteId { get; set; }
}
}
添加和实现新的接口函数ITouristRouteRepository.cs / TouristRouteRepository.cs
Task AddShoppingCartItem(LineItem lineItem);
public async Task AddShoppingCartItem(LineItem lineItem)
{
await _context.LineItems.AddAsync(lineItem);
}
请求https://localhost:44381/api/shoppingCart/items
从购物车删除商品
删除商品函数ShoppingCartController.cs
//删除商品
[HttpDelete("items/{itemId}")]
[Authorize(AuthenticationSchemes = "Bearer")]
public async Task<IActionResult> DeleteShoppingCartItem([FromRoute] int itemId)
{
// 1 获取lineitem数据
var lineItem = await _touristRouteRepository
.GetShoppingCartItemByItemId(itemId);
if (lineItem == null)
{
return NotFound("购物车商品找不到");
}
_touristRouteRepository.DeleteShoppingCartItem(lineItem);
await _touristRouteRepository.SaveAsync();
return NoContent();
}
添加和实现新的接口函数ITouristRouteRepository.cs / TouristRouteRepository.cs
Task<LineItem> GetShoppingCartItemByItemId(int lineItemId);
void DeleteShoppingCartItem(LineItem lineItem);
public async Task<LineItem> GetShoppingCartItemByItemId(int lineItemId)
{
return await _context.LineItems
.Where(li => li.Id == lineItemId)
.FirstOrDefaultAsync();
}
public async void DeleteShoppingCartItem(LineItem lineItem)
{
_context.LineItems.Remove(lineItem);
}
测试 - 请求https://localhost:44381/api/shoppingCart/items/1
- 返回204
从购物车批量删除商品
批量删除商品函数ShoppingCartController.cs
[HttpDelete("items/({itemIDs})")]
[Authorize(AuthenticationSchemes = "Bearer")]
public async Task<IActionResult> RemoveShoppingCartItems(
[ModelBinder(BinderType = typeof(ArrayModelBinder))]
[FromRoute] IEnumerable<int> itemIDs
)
{
var lineitems = await _touristRouteRepository
.GeshoppingCartsByIdListAsync(itemIDs);
_touristRouteRepository.DeleteShoppingCartItems(lineitems);
await _touristRouteRepository.SaveAsync();
return NoContent();
}
添加和实现新的接口函数ITouristRouteRepository.cs / TouristRouteRepository.cs
//批量删除
Task<IEnumerable<LineItem>> GeshoppingCartsByIdListAsync(IEnumerable<int> ids);
void DeleteShoppingCartItems(IEnumerable<LineItem> lineItems);
//批量删除
public async Task<IEnumerable<LineItem>> GeshoppingCartsByIdListAsync(
IEnumerable<int> ids)
{
return await _context.LineItems
.Where(li => ids.Contains(li.Id))
.ToListAsync();
}
public void DeleteShoppingCartItems(IEnumerable<LineItem> lineItems)
{
_context.LineItems.RemoveRange(lineItems);
}
十三、【购物系统从0到1】极简主义的订单系统
开发概要与接口设计
如何设计电商网站的订单系统
- 订单系统的角色
- 订单系统核心功能,业务流程
- 订单的状态处理
- 订单API接口设计
订单系统的角色
订单系统核心功能
订单状态
RestFul风格的订单API接口设计
订单模型开发与数据库更新
新建Model - Order.cs
namespace _04NET___CJ_ASP_Travel4.Models
{
public enum OrderStateEnum
{
Pending, // 订单已生成
Processing, // 支付处理中
Completed, // 交易成功
Declined, // 交易失败
Cancelled, // 订单取消
Refund, // 已退款
}
public class Order
{
[Key]
public Guid Id { get; set; }
public string UserId { get; set; }
public ApplicationUser User { get; set; }
public ICollection<LineItem> OrderItems { get; set; }
public OrderStateEnum State { get; set; }
public DateTime CreateDateUTC { get; set; }
public string TransactionMetadata { get; set; }
}
}
ApplicationUser.cs
public ICollection<Order> Orders { get; set; }
AppDbContext.cs
public DbSet<Order> Orders { get; set; }
更新数据库
add-migration OrderMigration
update-database
订单的有限状态
有限状态机
状态机4要素
使用Stateless实现订单状态机
Model - Order.cs
public enum OrderStateTriggerEnum
{
PlaceOrder, // 支付
Approve, // 收款成功
Reject, // 收款失败
Cancel, // 取消
Return // 退货
}
使用Stateless实现订单状态机
order.cs
using Stateless;
public class Order
{
//状态机
public Order()
{
StateMachineInit();
}
StateMachine<OrderStateEnum, OrderStateTriggerEnum> _machine;
private void StateMachineInit()
{
_machine = new StateMachine<OrderStateEnum, OrderStateTriggerEnum>
(OrderStateEnum.Pending);
_machine.Configure(OrderStateEnum.Pending)
.Permit(OrderStateTriggerEnum.PlaceOrder, OrderStateEnum.Processing)
.Permit(OrderStateTriggerEnum.Cancel, OrderStateEnum.Cancelled);
_machine.Configure(OrderStateEnum.Processing)
.Permit(OrderStateTriggerEnum.Approve, OrderStateEnum.Completed)
.Permit(OrderStateTriggerEnum.Reject, OrderStateEnum.Declined);
_machine.Configure(OrderStateEnum.Declined)
.Permit(OrderStateTriggerEnum.PlaceOrder, OrderStateEnum.Processing);
_machine.Configure(OrderStateEnum.Completed)
.Permit(OrderStateTriggerEnum.Return, OrderStateEnum.Refund);
}
}
购物车下单、结算
购物车结算函数(ShoppingCartController.cs)
[HttpPost("checkout")]
[Authorize(AuthenticationSchemes = "Bearer")]
public async Task<IActionResult> Checkout()
{
// 1 获得当前用户
var userId = _httpContextAccessor
.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
// 2 使用userid获得购物车
var shoppingCart = await _touristRouteRepository.GetShoppingCartByUserId(userId);
// 3 创建订单
var order = new Order()
{
Id = Guid.NewGuid(),
UserId = userId,
State = OrderStateEnum.Pending,
OrderItems = shoppingCart.ShoppingCartItems,
CreateDateUTC = DateTime.UtcNow,
};
shoppingCart.ShoppingCartItems = null;
// 4 保存数据
await _touristRouteRepository.AddOrderAsync(order);
await _touristRouteRepository.SaveAsync();
// 5 return
return Ok(_mapper.Map<OrderDto>(order));
}
添加和实现新的接口函数ITouristRouteRepository.cs / TouristRouteRepository.cs
Task AddOrderAsync(Order order);
public async Task AddOrderAsync(Order order)
{
await _context.Orders.AddAsync(order);
}
返回响应OrderDto.cs
namespace _04NET___CJ_ASP_Travel4.API.Dtos
{
public class OrderDto
{
public Guid Id { get; set; }
public string UserId { get; set; }
public ICollection<LineItemDto> OrderItems { get; set; }
public string State { get; set; }
public DateTime CreateDateUTC { get; set; }
public string TransactionMetadata { get; set; }
}
}
OrderProfile.cs
namespace _04NET___CJ_ASP_Travel4.Profiles
{
public class OrderProfile : Profile
{
public OrderProfile()
{
CreateMap<Order, OrderDto>()
.ForMember(
dest => dest.State,
opt =>
{
opt.MapFrom(src => src.State.ToString());
}
);
}
}
}
获得用户订单
新建OrdersController.cs
namespace _04NET___CJ_ASP_Travel4.Controllers
{
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ITouristRouteRepository _touristRouteRepository;
private readonly IMapper _mapper;
public OrdersController(
IHttpContextAccessor httpContextAccessor,
ITouristRouteRepository touristRouteRepository,
IMapper mapper
)
{
_httpContextAccessor = httpContextAccessor;
_touristRouteRepository = touristRouteRepository;
_mapper = mapper;
}
[HttpGet]
[Authorize(AuthenticationSchemes = "Bearer")]
public async Task<IActionResult> GetOrders()
{
// 1. 获得当前用户
var userId = _httpContextAccessor
.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
// 2. 使用用户id来获取订单历史记录
var orders = await _touristRouteRepository.GetOrdersByUserId(userId);
return Ok(_mapper.Map<IEnumerable<OrderDto>>(orders));
}
[HttpGet("{orderId}")]
[Authorize(AuthenticationSchemes = "Bearer")]
public async Task<IActionResult> GerOrderById([FromRoute] Guid orderId)
{
// 1. 获得当前用户
var userId = _httpContextAccessor
.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
var order = await _touristRouteRepository.GetOrderById(orderId);
return Ok(_mapper.Map<OrderDto>(order));
}
}
}
添加和实现新的接口函数ITouristRouteRepository.cs / TouristRouteRepository.cs
Task<IEnumerable<Order>> GetOrdersByUserId(string userId);
Task<Order> GetOrderById(Guid orderId);
public async Task<IEnumerable<Order>> GetOrdersByUserId(string userId)
{
return await _context.Orders.Where(o => o.UserId == userId).ToListAsync();
}
public async Task<Order> GetOrderById(Guid orderId)
{
return await _context.Orders
.Include(o => o.OrderItems).ThenInclude(oi => oi.TouristRoute)
.Where(o => o.Id == orderId)
.FirstOrDefaultAsync();
}
十四、【RESTful技能进阶】数据分页显示
- 如何接受分页参数
- 如何返回分页信息
- 进行模块化分页处理
分页与项目架构浅析
数据分页Pagination
分页思路
项目架构
完成简单分页
TouristRouteResourceParamaters.cs
private int _pageNumber = 1;
public int PageNumber {
get
{
return _pageNumber;
}
set
{
if(value >= 1)
{
_pageNumber = value;
}
}
}
private int _pageSize = 10;
const int maxPageSize = 50;
public int PageSize {
get
{
return _pageSize;
}
set
{
if (value >= 1)
{
_pageSize = (value > maxPageSize) ? maxPageSize : value;
}
}
}
TouristRoutesController.cs 新增两参数
var touristRoutesFromRepo = await _touristRouteRepository
.GetTouristRoutesAsync(
paramaters.Keyword,
paramaters.RatingOperator,
paramaters.RatingValue,
//分页
paramaters.PageSize,
paramaters.PageNumber
);
ITouristRouteRepository.cs 新增两参数
Task<IEnumerable<TouristRoute>> GetTouristRoutesAsync(
string keyword, string ratingOperator, int? ratingValue
//分页
, int pageSize, int pageNumber);
TouristRouteRepository.cs
public async Task<IEnumerable<TouristRoute>> GetTouristRoutesAsync(
string keyword,
string ratingOperator,
int? ratingValue,
//分页
int pageSize,
int pageNumber
)
//省略---------------------------
// pagination
// skip
var skip = (pageNumber - 1) * pageSize;
result = result.Skip(skip);
// 以pagesize为标准显示一定量的数据
result = result.Take(pageSize);
测试 - 请求https://localhost:44381/api/touristRoutes?pagenumber=5&pagesize=1
- 即使不加后缀条件,也默认显示第一页的数据。
分页进阶:模组化
分页组件类:PaginationList <T>
Helper - PaginationList.cs
namespace _04NET___CJ_ASP_Travel4.Helper
{
public class PaginationList<T> : List<T>
{
public int CurrentPage { get; set; }
public int PageSize { get; set; }
public PaginationList(int currentPage, int pageSize, List<T> items)
{
CurrentPage = currentPage;
PageSize = pageSize;
AddRange(items);
}
public static async Task<PaginationList<T>> CreateAsync(
int currentPage, int pageSize, IQueryable<T> result)
{
// pagination
// skip
var skip = (currentPage - 1) * pageSize;
result = result.Skip(skip);
// 以pagesize为标准显示一定量的数据
result = result.Take(pageSize);
// include vs join
var items = await result.ToListAsync();
return new PaginationList<T>(currentPage, pageSize, items);
}
}
}
修改ITouristRouteRepository.cs和TouristRouteRepository.cs
IEnumerable
改PaginationList
/* Task<IEnumerable<TouristRoute>> GetTouristRoutesAsync(
string keyword, string ratingOperator, int? ratingValue
//分页
, int pageSize, int pageNumber);*/
Task<PaginationList<TouristRoute>> GetTouristRoutesAsync(
string keyword, string ratingOperator, int? ratingValue
//分页
, int pageSize, int pageNumber);
//`IEnumerable`改`PaginationList`
public async Task<PaginationList<TouristRoute>> GetTouristRoutesAsync(
string keyword,
string ratingOperator,
int? ratingValue,
//分页
int pageSize,
int pageNumber
)
删除对分页的单独处理
/* // pagination
// skip
var skip = (pageNumber - 1) * pageSize;
result = result.Skip(skip);
// 以pagesize为标准显示一定量的数据
result = result.Take(pageSize);*
工厂函数创建实例
// include vs join
return await PaginationList<TouristRoute>.CreateAsync(pageNumber, pageSize, result);
测试 - 请求https://localhost:44381/api/touristRoutes?pagenumber=1&pagesize=5
复用模组化分页
新建PaginationResourceParamaters.cs
- 剪切
TouristRouteResourceParamaters.cs
中的所有分页方法。
namespace _04NET___CJ_ASP_Travel4.ResourceParameters
{
public class PaginationResourceParamaters
{
private int _pageNumber = 1;
public int PageNumber
{
get
{
return _pageNumber;
}
set
{
if (value >= 1)
{
_pageNumber = value;
}
}
}
private int _pageSize = 10;
const int maxPageSize = 50;
public int PageSize
{
get
{
return _pageSize;
}
set
{
if (value >= 1)
{
_pageSize = (value > maxPageSize) ? maxPageSize : value;
}
}
}
}
}
修改TouristRoutesController.cs的GerTouristRoutes方法
[FromQuery] PaginationResourceParamaters paramaters2
public async Task<IActionResult> GerTouristRoutes(
[FromQuery] TouristRouteResourceParamaters paramaters,
//新增代码-------------------
[FromQuery] PaginationResourceParamaters paramaters2
)
{
var touristRoutesFromRepo = await _touristRouteRepository
.GetTouristRoutesAsync(
paramaters.Keyword,
paramaters.RatingOperator,
paramaters.RatingValue,
//分页
paramaters2.PageSize,
paramaters2.PageNumber
);
//-------------省略
}
OrdersController.cs - GetOrders
- 加入分页参数
[FromQuery] PaginationResourceParamaters paramaters
[HttpGet]
[Authorize(AuthenticationSchemes = "Bearer")]
public async Task<IActionResult> GetOrders([FromQuery] PaginationResourceParamaters paramaters)
{
// 1. 获得当前用户
var userId = _httpContextAccessor
.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
// 2. 使用用户id来获取订单历史记录
var orders = await _touristRouteRepository.GetOrdersByUserId(
userId, paramaters.PageSize, paramaters.PageNumber);
return Ok(_mapper.Map<IEnumerable<OrderDto>>(orders));
}
修改ITouristRouteRepository.cs和TouristRouteRepository.cs
/* Task<IEnumerable<Order>> GetOrdersByUserId(string userId);*/
Task<PaginationList<Order>> GetOrdersByUserId(string userId, int pageSize, int pageNumber);
public async Task<PaginationList<Order>> GetOrdersByUserId(
string userId, int pageSize, int pageNumber)
{
//return await _context.Orders.Where(o => o.UserId == userId).ToListAsync();
IQueryable<Order> result = _context.Orders.Where(o => o.UserId == userId);
return await PaginationList<Order>.CreateAsync(pageNumber, pageSize, result);
}
分页导航
响应中包含哪些分页数据呢
- 数据列表将会出现在响应主体中。
- 而分页的信息与数据列表彻底分开。
- 以metadata元数据的形式在header中输出。
分页的元数据pagination metadata
在RESTful中为什么要将分页信息与数据列表分开?
- 请求使用
application/json
,目的是获取资源。 - 分页信息并不是资源,所以称为元数据metadata而不是资源数据resource。
- 分页导航属于api成孰度level3级别。
- HATEOAS: API的自我发现机制。
拓展PaginationList<T>
:给header添加导航
改造PaginationList
PaginationList.cs
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace _04NET___CJ_ASP_Travel4.Helper
{
public class PaginationList<T> : List<T>
{
public int TotalPages { get; private set; }
public int TotalCount { get; private set; }
public bool HasPrevious => CurrentPage > 1;
public bool HasNext => CurrentPage < TotalPages;
public int CurrentPage { get; set; }
public int PageSize { get; set; }
public PaginationList(int totalCount, int currentPage, int pageSize, List<T> items)
{
CurrentPage = currentPage;
PageSize = pageSize;
AddRange(items);
TotalCount = totalCount;
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
}
public static async Task<PaginationList<T>> CreateAsync(
int currentPage, int pageSize, IQueryable<T> result)
{
var totalCount = await result.CountAsync();
// pagination
// skip
var skip = (currentPage - 1) * pageSize;
result = result.Skip(skip);
// 以pagesize为标准显示一定量的数据
result = result.Take(pageSize);
// include vs join
var items = await result.ToListAsync();
return new PaginationList<T>(totalCount, currentPage, pageSize, items);
}
}
}
新建Helper - ResourceUriType.cs
namespace _04NET___CJ_ASP_Travel4.Helper
{
public enum ResourceUriType
{
PreviousPage,
NextPage
}
}
_urlHelper需注册服务(Start.cs)ConfigureServices
- 注册
IAtionContextAccessor
services.AddHttpClient();
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
创建分页URL导航
添加响应头部 x-pagination
TourisRoutesController.cs - 改造GerTouristRoutes方法
private readonly IUrlHelper _urlHelper;
private string GenerateTouristRouteResourceURL(
TouristRouteResourceParamaters paramaters,
PaginationResourceParamaters paramaters2,
ResourceUriType type
)
{
return type switch
{
ResourceUriType.PreviousPage => _urlHelper.Link("GetTouristRoutes",
new
{
keyword = paramaters.Keyword,
rating = paramaters.Rating,
pageNumber = paramaters2.PageNumber - 1,
pageSize = paramaters2.PageSize
}),
ResourceUriType.NextPage => _urlHelper.Link("GetTouristRoutes",
new
{
keyword = paramaters.Keyword,
rating = paramaters.Rating,
pageNumber = paramaters2.PageNumber + 1,
pageSize = paramaters2.PageSize
}),
_ => _urlHelper.Link("GetTouristRoutes",
new
{
keyword = paramaters.Keyword,
rating = paramaters.Rating,
pageNumber = paramaters2.PageNumber,
pageSize = paramaters2.PageSize
})
};
}
// api/touristRoutes?keyword=传入的参数
[HttpGet(Name = "GetTouristRoutes")]
[HttpHead]
public async Task<IActionResult> GerTouristRoutes(
[FromQuery] TouristRouteResourceParamaters paramaters,
[FromQuery] PaginationResourceParamaters paramaters2
//[FromQuery] string keyword,
//string rating // 小于lessThan, 大于largerThan, 等于equalTo lessThan3, largerThan2, equalTo5
)// FromQuery vs FromBody
{
var touristRoutesFromRepo = await _touristRouteRepository
.GetTouristRoutesAsync(
paramaters.Keyword,
paramaters.RatingOperator,
paramaters.RatingValue,
//分页
paramaters2.PageSize,
paramaters2.PageNumber
);
if (touristRoutesFromRepo == null || touristRoutesFromRepo.Count() <= 0)
{
return NotFound("没有旅游路线");
}
var touristRoutesDto = _mapper.Map<IEnumerable<TouristRouteDto>>(touristRoutesFromRepo);
var previousPageLink = touristRoutesFromRepo.HasPrevious
? GenerateTouristRouteResourceURL(
paramaters, paramaters2, ResourceUriType.PreviousPage)
: null;
var nextPageLink = touristRoutesFromRepo.HasNext
? GenerateTouristRouteResourceURL(
paramaters, paramaters2, ResourceUriType.NextPage)
: null;
// x-pagination
var paginationMetadata = new
{
previousPageLink,
nextPageLink,
totalCount = touristRoutesFromRepo.TotalCount,
pageSize = touristRoutesFromRepo.PageSize,
currentPage = touristRoutesFromRepo.CurrentPage,
totalPages = touristRoutesFromRepo.TotalPages
};
Response.Headers.Add("x-pagination",
Newtonsoft.Json.JsonConvert.SerializeObject(paginationMetadata));
return Ok(touristRoutesDto);
}
测试 - 请求https://localhost:44381/api/touristRoutes?pagenumber=1&pagesize=5
x-pagination
- 得到x-pagination信息
{
"previousPageLink":null,
"nextPageLink":"https://localhost:44381/api/TouristRoutes?pageNumber=2&pageSize=5",
"totalCount":16,
"pageSize":5,
"currentPage":1,
"totalPages":4
}
Comments | NOTHING