Post

验证与授权

0. HttpContext.User

微软官方文档 HttpContext

当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 架构。

构建用户角色实体

  1. 创建类库项目 Data
  2. 创建自己的 用户 角色 实体,必须继承自 IdentityUser 和 IdentityRole 类型。
  3. 生成数据库上下文类型,必须继承自 IdentityDbContext<TUser, TRole, string>

IdentityDbContext<TUser, TRole, string> 中的string代表了用户表、角色表的主键类型。我们使用Guid作为主键,因此是string类型,当然你可以使用int类型,并设置自增来作为主键。

  1. 使用上面的 DbContext 创建迁移,Ef 迁移工具会自动生成数据库表。

项目中引入身份服务

下面的操作在 WebApi 项目中的 Program.cs 中

  1. 添加数据库 builder.AddDbContext<TDbContext>() ,配置好数据库连接(这里不赘述了,去微软官方文档找找教程)
  2. 添加终结点身份管理服务 builder.AddIdentityApiEndpoints<TUser>(Action op1).AddRoles<TRole>().AddEntityFrameworkStores<TDbContext>().AddDefaultTokenProviders()

这里比较长,由于角色管理也是自定义的额外配置,因此使用 AddRoles 来添加. op1 的位置可以配置登陆密码的要求等其他配置。

如果你使用 AddIdentity,他会自动注册一个身份验证配置,用来将未授权访问映射到 /Account/Login 终结点,我们不需要这个功能,而且他会覆盖我们设定的 Jwt 验证配置,导致 401 的未授权返回 变成 404 的未找到页面。原因就是我们没有定义登陆页面。

  1. 添加身份验证中间件并配置 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]

SwaggerBearer

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[] { }
        }
    });
});
This post is licensed under CC BY 4.0 by the author.