Asp Net Core - 使用 JWT 进行 Rest API 授权(角色、声明和策略)- 一步步
在本文中,我们将介绍 AspNet Core 授权(角色、声明和策略)。分别介绍它们各自的适用场景,并帮助您更好地理解它们之间的协同作用。
今天我们要讨论的内容是:
- 身份验证与授权
- 什么是身份验证
- 什么是授权
- 授权类型
- 什么是角色
- 什么是索赔
- 什么是政策
- 原料
- 代码和实现
您可以在 YouTube 上观看完整视频
您可以在 GitHub 上找到源代码
https://github.com/mohamadlawand087/v48-AspNetCore-Authorisation
这是 API 开发系列的第 4 部分,您可以通过以下链接查看各个部分:
第 1 部分:https://dev.to/moe23/asp-net-core-5-rest-api-step-by-step-2mb6
第 2 部分:https
://dev.to/moe23/asp-net-core-5-rest-api-authentication-with-jwt-step-by-step-140d 第 3 部分:https://dev.to/moe23/refresh-jwt-with-refresh-tokens-in-asp-net-core-5-rest-api-step-by-step-3en5
身份验证与授权
在我们深入探讨这个话题之前,尽管术语听起来相似,但身份验证和授权是登录过程中的独立步骤。
验证
身份验证是验证用户身份的行为。这是任何安全流程的第一步。
登录您的电子邮件或解锁您的手机是一种身份验证形式,您需要提供某种凭证,以便系统允许您进入并查看您的信息。
身份验证可以采取多种形式:
- 密码。 用户名和密码是最常见的身份验证因素。如果用户输入正确的数据,系统将假定身份有效并授予访问权限。
- 一次性密码。 仅授予一次会话或交易的访问权限。
- 身份验证应用程序。 通过授予访问权限的外部方生成安全代码。
- 生物识别技术。 用户通过指纹或眼部扫描来访问系统。
在某些情况下,系统要求成功验证多个因素才能授予访问权限。这种多因素身份验证 (MFA) 要求通常用于增强安全性,超越单靠密码所能提供的安全性。
授权:
我们首先需要定义什么是身份验证,更重要的是,它不是什么。身份验证指的是确定用户能够做什么的过程。
换句话说,授权证明您有权提出请求。当您尝试进入音乐会或活动的后台时,您无需证明您的身份——只需出示门票即可,这证明您有权进入您想进入的地方。
授权与认证相互独立,但授权需要认证机制。
角色:
它们是一组在应用程序中执行某些活动的权限。我们可以将角色视为一个布尔值,表示我们是否拥有这个角色(true 或 false)。
我们对角色的处理方式是,将功能赋予角色,一旦我们将用户分配给角色,这些功能集合就会被赋予该用户。一旦我们删除角色,这些功能也会被删除。
角色将保护对功能的访问,如果用户没有正确的角色,则用户将无法执行该功能
声明:
它们与角色完全不同,基于声明的机制比角色更灵活,因为它们是键值对。声明属于用户或实体,用于描述用户或实体。声明本质上是用户属性,它们告知用户的授权。
为了更好地说明,让我们再次检查一下驾驶执照示例
我们可以看到,这个许可证上有 11 条声明,这意味着有 11 条关于用户的信息。如果我们想将其转换为基于代码的结构,它将如下所示
{
"dl":"123456789",
"exp":"07/11/2025",
"ln":"DOE",
"fn":"John",
"dob":"09/05/1993",
"sex":"M",
"hair":"brn",
"eyes":"blue",
"hgt":"6.0"
"wgt":"183lb",
"class":"C"
}
因此,用户登录后就会收到这些声明。
根据我们希望如何实现授权流程,声明可以与角色一起工作,也可以与角色不一起工作。
政策:
它们是用于检查用户信息并检查是否授予或拒绝权限的功能或规则。
策略基本上从上下文开始,根据策略列表检查用户,并根据列表授予或拒绝对所请求资源的权限。
基于角色的授权和基于声明的授权使用需求、需求处理程序和预配置策略。策略由一个或多个需求组成
角色、声明和策略
角色是一种符号类别,用于将具有相同安全权限级别的用户集合在一起。基于角色的授权需要首先识别用户,然后确定用户被分配的角色,最后将这些角色与有权访问资源的角色进行比较。
相反,主张不是基于群体,而是基于身份。
代码
我们将继续构建上一个使用 JWT 令牌授权的项目,您可以在 github 上找到源代码
https://github.com/mohamadlawand087/v8-refreshtokenswithJWT
一旦我们克隆了这个 repo,我们就可以开始构建我们的授权
我们要做的第一件事是更新启动类,使其包含 Roles 到身份提供者中。在 Startup 类的 configureServices 中,我们需要更新以下内容
services.AddIdentity<IdentityUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApiDbContext>();
然后我们需要做的是在控制器文件夹内创建一个名为 SetupController 的新控制器,并添加以下内容
[Route("api/[controller]")] // api/setup
[ApiController]
public class SetupController: ControllerBase
{
private readonly ApiDbContext _context;
private readonly RoleManager<IdentityRole> _roleManager;
private readonly UserManager<IdentityUser> _userManager;
protected readonly ILogger<SetupController> _logger;
public SetupController(
ApiDbContext context,
RoleManager<IdentityRole> roleManager,
UserManager<IdentityUser> userManager,
ILogger<SetupController> logger)
{
_logger = logger;
_roleManager = roleManager;
_userManager = userManager;
_context = context;
}
[HttpGet]
public IActionResult GetAllRoles()
{
var roles = _roleManager.Roles.ToList();
return Ok(roles);
}
[HttpPost]
public async Task<IActionResult> CreateRole(string roleName)
{
var roleExist = await _roleManager.RoleExistsAsync(roleName);
if (!roleExist) {
//create the roles and seed them to the database: Question 1
var roleResult = await _roleManager.CreateAsync (new IdentityRole (roleName));
if (roleResult.Succeeded) {
_logger.LogInformation (1, "Roles Added");
return Ok(new {result = $"Role {roleName} added successfully"});
} else {
_logger.LogInformation (2, "Error");
return BadRequest(new {error = $"Issue adding the new {roleName} role"});
}
}
return BadRequest(new {error = "Role already exist"});
}
// Get all users
[HttpGet]
[Route("GetAllUsers")]
public async Task<IActionResult> GetAllUsers()
{
var users = await _userManager.Users.ToListAsync();
return Ok(users);
}
// Add User to role
[HttpPost]
[Route("AddUserToRole")]
public async Task<IActionResult> AddUserToRole(string email, string roleName)
{
var user = await _userManager.FindByEmailAsync(email);
if(user != null)
{
var result = await _userManager.AddToRoleAsync(user, roleName);
if(result.Succeeded)
{
_logger.LogInformation (1, $"User {user.Email} added to the {roleName} role");
return Ok(new {result = $"User {user.Email} added to the {roleName} role"});
}
else
{
_logger.LogInformation (1, $"Error: Unable to add user {user.Email} to the {roleName} role");
return BadRequest(new {error = $"Error: Unable to add user {user.Email} to the {roleName} role"});
}
}
// User doesn't exist
return BadRequest(new {error = "Unable to find user"});
}
// Get specific user role
[HttpGet]
[Route("GetUserRoles")]
public async Task<IActionResult> GetUserRoles(string email)
{
// Resolve the user via their email
var user = await _userManager.FindByEmailAsync(email);
// Get the roles for the user
var roles = await _userManager.GetRolesAsync(user);
return Ok(roles);
}
// Remove User to role
[HttpPost]
[Route("RemoveUserFromRole")]
public async Task<IActionResult> RemoveUserFromRole(string email, string roleName)
{
var user = await _userManager.FindByEmailAsync(email);
if(user != null)
{
var result = await _userManager.RemoveFromRoleAsync(user, roleName);
if(result.Succeeded)
{
_logger.LogInformation (1, $"User {user.Email} removed from the {roleName} role");
return Ok(new {result = $"User {user.Email} removed from the {roleName} role"});
}
else
{
_logger.LogInformation (1, $"Error: Unable to removed user {user.Email} from the {roleName} role");
return BadRequest(new {error = $"Error: Unable to removed user {user.Email} from the {roleName} role"});
}
}
// User doesn't exist
return BadRequest(new {error = "Unable to find user"});
}
}
完成 SetupController 后,让我们转到 AuthManagement Controller 并更新以下内容
// We need to add the following before the constructor
private readonly RoleManager<IdentityRole> _roleManager;
protected readonly ILogger<AuthManagementController> _logger;
// We need to update the constructor to the following
public AuthManagementController(
UserManager<IdentityUser> userManager,
RoleManager<IdentityRole> roleManager,
IOptionsMonitor<JwtConfig> optionsMonitor,
TokenValidationParameters tokenValidationParams,
ILogger<AuthManagementController> logger,
ApiDbContext apiDbContext)
{
_logger = logger;
_userManager = userManager;
_roleManager = roleManager;
_jwtConfig = optionsMonitor.CurrentValue;
_tokenValidationParams = tokenValidationParams;
_apiDbContext = apiDbContext;
}
// We need to create a GetValidClaims method
private async Task<List<Claim>> GetValidClaims(IdentityUser user)
{
IdentityOptions _options = new IdentityOptions();
var claims = new List<Claim>
{
new Claim("Id", user.Id),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(_options.ClaimsIdentity.UserIdClaimType, user.Id.ToString()),
new Claim(_options.ClaimsIdentity.UserNameClaimType, user.UserName),
};
var userClaims = await _userManager.GetClaimsAsync(user);
var userRoles = await _userManager.GetRolesAsync(user);
claims.AddRange(userClaims);
foreach (var userRole in userRoles)
{
claims.Add(new Claim(ClaimTypes.Role, userRole));
var role = await _roleManager.FindByNameAsync(userRole);
if(role != null)
{
var roleClaims = await _roleManager.GetClaimsAsync(role);
foreach(Claim roleClaim in roleClaims)
{
claims.Add(roleClaim);
}
}
}
return claims;
}
// We need to update the GenerateJwtToken method
var claims = await GetValidClaims(user);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(5), // 5-10
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
接下来我们需要更新 TodoController 属性以向其添加角色
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "AppUser")]
现在让我们尝试一下
- 将创建一个新用户
- 将创建一个名为 AppUser 的角色
- 将角色分配给用户
- 将登录并获取 JWT 令牌
- 将尝试访问 GetItems 端点
现在我们开始添加 ClaimSetup 控制器,在控制器文件夹中添加一个名为 ClaimSetupController 的新类,并添加以下内容
[Route("api/[controller]")] // api/ClaimSetup
[ApiController]
public class ClaimSetupController : ControllerBase
{
private readonly ApiDbContext _context;
private readonly RoleManager<IdentityRole> _roleManager;
private readonly UserManager<IdentityUser> _userManager;
protected readonly ILogger<ClaimSetupController> _logger;
public ClaimSetupController(
ApiDbContext context,
RoleManager<IdentityRole> roleManager,
UserManager<IdentityUser> userManager,
ILogger<ClaimSetupController> logger)
{
_logger = logger;
_roleManager = roleManager;
_userManager = userManager;
_context = context;
}
[HttpGet]
public async Task<IActionResult> GetAllClaims(string email)
{
var user = await _userManager.FindByEmailAsync(email);
var claims = await _userManager.GetClaimsAsync(user);
return Ok(claims);
}
// Add Claim to user
[HttpPost]
[Route("AddClaimToUser")]
public async Task<IActionResult> AddClaimToUser(string email, string claimName, string value)
{
var user = await _userManager.FindByEmailAsync(email);
var userClaim = new Claim(claimName, value);
if(user != null)
{
var result = await _userManager.AddClaimAsync(user, userClaim);
if(result.Succeeded)
{
_logger.LogInformation (1, $"the claim {claimName} add to the User {user.Email}");
return Ok(new {result = $"the claim {claimName} add to the User {user.Email}"});
}
else
{
_logger.LogInformation (1, $"Error: Unable to add the claim {claimName} to the User {user.Email}");
return BadRequest(new {error = $"Error: Unable to add the claim {claimName} to the User {user.Email}"});
}
}
// User doesn't exist
return BadRequest(new {error = "Unable to find user"});
}
}
现在我们需要更新 Startup 类来创建一个声明策略,在根目录下的 Startup.cs 中,我们需要在 configureServices 方法中添加以下内容
services.AddAuthorization(options =>
{
options.AddPolicy("ViewItemsPolicy",
policy => policy.RequireClaim("ViewItems"));
});
接下来,我们需要根据我们想要的操作更新 TodoController
[HttpGet]
[Authorize(Policy = "ViewItemsPolicy")]
public async Task<IActionResult> GetItems()
{
var items = await _context.Items.ToListAsync();
return Ok(items);
}
现在让我们测试一下
- 使用我们之前创建的相同帐户,我们需要向其添加声明
- 我们利用我们创建的新端点http://localhost:8090/api/ClaimSetup/AddClaimToUser并将声明添加到用户帐户
- 我们尝试访问http://localhost:8090/api/todo,任何其他没有声明的用户都无法访问。