验证与授权
0. HttpContext.User
当Asp.net服务器收到一个请求后,就会创建并维护一个HttpContext对象。
而有关用户身份的内容保存在 User 字段中。
1. 身份验证
由于Http是无状态的,因此服务器需要特殊的手段来识别Http请求来自哪个用户。
通常我们在请求报文中包含有关用户身份的信息(可能是一个加密串,或者Cookies),在经过后端处理后转换成用户信息。
身份验证中间件通常在管道的末尾,该中间件会识别 HttpContext.Request 中有关身份验证的内容,经过处理后将验证结果保存在 HttpContext.User
中,供“授权中间件”使用。
Asp.net 定义了一个通用的身份验证中间件,当执行身份验证时,Authentication 选择合适的验证方案,并执行改方案中的处理过程,处理过程会返回一个 Authentication.Reuslt 的结果,将其保存到 HttpContext.User 中。下面是 Authentication 中间件的 Invoke 方法一部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Invoke ...
AuthenticationScheme authenticateSchemeAsync = await this.Schemes.GetDefaultAuthenticateSchemeAsync();
if (authenticateSchemeAsync != null)
{
AuthenticateResult result = await context.AuthenticateAsync(authenticateSchemeAsync.Name);
if (result?.Principal != null)
context.User = result.Principal; // 将验证结果写入 User
if (result != null && result.Succeeded)
{
AuthenticationFeatures instance = new AuthenticationFeatures(result);
context.Features.Set<IHttpAuthenticationFeature>((IHttpAuthenticationFeature) instance);
context.Features.Set<IAuthenticateResultFeature>((IAuthenticateResultFeature) instance);
}
}
await this._next(context);
我们可以定义多个不同的身份验证方案
2. 授权
当获取到用户信息后,我们希望根据用户的身份,提供权限的服务。例如基于用户的权限管理,基于角色的权限管理,以及基于策略权限管理。
授权中间件基于 HttpContext.User 中的信息来进行权限控制,因此授权过程与身份验证无关。
授权中间件还提供了 Authorize 属性来方便的指定 控制器 的权限。
3. JwtBearer
jwt 是一种 身份验证 方案。使用下面的代码将jwt方案添加到Authentication中。
1
2
3
4
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options=>{
// jwt config
});
JwtToken 是一个包含用户信息以及其他验证信息的加密字符串,他包含三个部分,使用 .
来分割。
JwtToken 被放在请求头中的 Authentication 中。
这里不讨论 Jwt 的内容。总之,当服务端收到用户请求后,Authentication 中间件会调用 Jwt 的处理程序,该处理程序使用密钥解密 Token,得到用户信息。之后 Authentication 将该信息存到 HttpContext.User 中。
由于 JwtToken 包含了用户信息,因此服务端不需要存储 Token,只需要解密并验证信息是否合法即可。
4. Identity 框架
identity 是 asp.net 提供的官方的身份管理框架。该框架包含了一个应用有关用户的所有常用功能(邮箱验证、密码更改、两步验证、外部登陆等等等)。 因此 Identity 是包含了 验证、授权 的。
但是 Identity 也是高度可自定义的,对于前后端分离的项目,我们不需要 Identity 的UI功能,可以使用 AddIdentityApiEndpoints
。
Identity 框架提供了两个用来操作用户、角色的管理器对象:UserManager RoleManager
使用构造函数注入,可以使用他们。
5. jwt + identity
要使用 Identity ,需要添加 Efcore 和 Identity 相关的 nuget 包。这里就不赘述了,自行添加即可。
这里使用两层的 ORM-Application 架构。
构建用户角色实体
- 创建类库项目 Data
- 创建自己的 用户 角色 实体,必须继承自 IdentityUser 和 IdentityRole 类型。
- 生成数据库上下文类型,必须继承自 IdentityDbContext<TUser, TRole, string>
IdentityDbContext<TUser, TRole, string> 中的string代表了用户表、角色表的主键类型。我们使用Guid作为主键,因此是string类型,当然你可以使用int类型,并设置自增来作为主键。
- 使用上面的 DbContext 创建迁移,Ef 迁移工具会自动生成数据库表。
项目中引入身份服务
下面的操作在 WebApi 项目中的 Program.cs 中
- 添加数据库
builder.AddDbContext<TDbContext>()
,配置好数据库连接(这里不赘述了,去微软官方文档找找教程) - 添加终结点身份管理服务
builder.AddIdentityApiEndpoints<TUser>(Action op1).AddRoles<TRole>().AddEntityFrameworkStores<TDbContext>().AddDefaultTokenProviders()
这里比较长,由于角色管理也是自定义的额外配置,因此使用 AddRoles 来添加. op1 的位置可以配置登陆密码的要求等其他配置。
如果你使用
AddIdentity
,他会自动注册一个身份验证配置,用来将未授权访问映射到 /Account/Login 终结点,我们不需要这个功能,而且他会覆盖我们设定的 Jwt 验证配置,导致 401 的未授权返回 变成 404 的未找到页面。原因就是我们没有定义登陆页面。
添加身份验证中间件并配置 Jwt 验证程序
1 2
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(Action op1);
添加中间件
1
2
3
4
var app = builder.Build();
// ... 通常身份验证中间件最后添加
app.UseAuthentication();
app.UseAuthorization();
6. 相关 API 参考
用户注册
关于用户名密码的传递,可以在前端添加加密。
这个例子里后端只存储 Hash(密码+盐) 作为密码,因此前端如何加密密码后端不关心。
PS. Identity 框架会自动处理明文密码的加盐Hash,因此添加用户时直接传递”明文”即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// AuthController 请在构造函数中注入 UserManager 和 RoleManager
// JwtHepler 相关的函数请自行 Google
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> RegisterAdmin([FromBody] RegisterModel info)
{
var user = new ExUser
{
Id = Guid.NewGuid().ToString(),
UserName = info.Username,
Description = info.Description
};
var roles = new List<string>();
foreach (var r in info.Roles)
{
if (_roleManager.RoleExistsAsync(r).Result)
roles.Add(r);
}
var result = await _userManager.CreateAsync(user, info.Password);
if (result.Succeeded)
{
var result2 = await _userManager.AddToRolesAsync(user, roles);
if (result2.Succeeded)
return Ok();
await _userManager.DeleteAsync(user);
return BadRequest();
}
return BadRequest();
}
用户登陆
LoginModel 是登陆信息对应的模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Login
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login([FromBody] LoginModel info)
{
var user = await _userManager.FindByNameAsync(info.Username);
if (user != null && await _userManager.CheckPasswordAsync(user, info.Password))
{
var roles = await _userManager.GetRolesAsync(user);
// 验证成功
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
foreach (var r in roles)
{
claims.Add(new Claim(ClaimTypes.Role, r));
}
var token = PwdHelper.GeneratedJwtToken(claims);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
});
}
return Unauthorized();
}
添加角色(管理员权限)
Roles 可指定多个角色
更多使用方式参考微软官方文档
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[Authorize(AuthenticationSchemes=JwtBearerDefaults.AuthenticationScheme, Roles = "admin")]
[HttpPost]
public IActionResult AddRole([FromBody] RoleModel info)
{
// 名称检测
if (_roleManager.RoleExistsAsync(info.Name).Result)
return BadRequest();
var role = new ExRole()
{
Id = Guid.NewGuid().ToString(),
Name = info.Name,
Description = info.Description,
};
var result = _roleManager.CreateAsync(role).Result;
if (result.Succeeded)
{
return Ok();
}
return BadRequest();
}
配置 Swagger UI 身份验证
下面配置可以让 Swagger UI 中显示 身份验证按钮。便于我们测试。
将通过登陆接口获取的 Token 以下面的方式添加到 下方,即可在后续的请求中携带验证信息。
Bearer [your token]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Description = "Bearer Token",
Name = "Authorization",
BearerFormat = "JWT",
Scheme = "Bearer",
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme()
{
Reference = new OpenApiReference()
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer",
}
},
new string[] { }
}
});
});