查看: 66|回复: 0

Asp.Net Core 5 REST API 使用 JWT 身份验证

[复制链接]

3

主题

5

帖子

11

积分

新手上路

Rank: 1

积分
11
发表于 2023-6-23 17:58:15 | 显示全部楼层 |阅读模式
翻译自 Mohamad Lawand 2021年1月22日的文章 《Asp Net Core 5 Rest API Authentication with JWT Step by Step》  
在本文中,我将向您展示如何向我们的 http://Asp.Net Core REST API 添加 JWT 身份验证。
我们将介绍的主题包含注册、登录功能以及如何使用 JWT (Json Web Tokens)和 Bearer 身份验证。
你也可以在 YouTube 上观看完整的视频,还可以下载源代码。
这是 API 开发系列的第二部分,本系列还包含:

  • Part 1:Asp.Net Core 5 REST API - Step by Step
  • Part 3:Asp Net Core 5 REST API 中使用 RefreshToken 刷新 JWT - Step by Step
我们将基于上一篇文章中创建的 Todo REST API 应用程序进行当前的讲述,您可以通过阅读上一篇文章并与我一起构建应用程序,或者可以从 github 下载上一篇中的源代码。



前一篇文章中的代码准备好以后,就让我们开始本文吧。
首先,我们需要安装一些依赖包以使用身份验证:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.UI然后,我们需要更新 appsettings.json,在 appsettings 中添加 JWT 的设置部分,在该设置中添加一个 JWT secret(密钥)。
"JwtConfig": {
    "Secret" : "ijurkbdlhmklqacwqzdxmkkhvqowlyqa"
},为了生成 secret,我们可以使用一个免费的 Web 工具(https://www.browserling.com/tools/random-string)来生成一个随机的 32 个字符的字符串。
我们在 appsettings 中添加完随机生成的 32 个字符的字符串后,接着需要在根目录中创建一个名为 Configuration 的新文件夹。
在这个 Configuration 文件夹中,我们将创建一个名为 JwtConfig 的新类:
public class JwtConfig
{
    public string Secret { get; set; }
}
现在我们需要更新 Startup 类,在 ConfigureServices 方法内,我们需要添加以下内容,以便将 JWT 配置注入到应用程序中:
services.Configure<JwtConfig>(Configuration.GetSection("JwtConfig"));
将这些配置添加到我们的 Startup 类中,即可在 Asp.Net Core 中间件和 IoC 容器中注册配置。
下一步是在我们的 Startup 类中添加和配置身份验证,在我们的 ConfigureServices 方法中,我们需要添加以下内容:
// 在本段中,我们将配置身份验证并设置默认方案
services.AddAuthentication(options => {
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwt => {
    var key = Encoding.ASCII.GetBytes(Configuration["JwtConfig:Secret"]);

    jwt.SaveToken = true;
    jwt.TokenValidationParameters = new TokenValidationParameters {
        ValidateIssuerSigningKey = true, //这将使用我们在 appsettings 中添加的 secret 来验证 JWT token 的第三部分,并验证 JWT token 是由我们生成的
        IssuerSigningKey = new SymmetricSecurityKey(key), //将密钥添加到我们的 JWT 加密算法中
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateLifetime = true,
        RequireExpirationTime = false
    };
});

services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
            .AddEntityFrameworkStores<ApiDbContext>();
更新好 ConfigureServices 之后,我们需要更新 Configure 方法,添加身份验证:
app.UseAuthentication();
配置添加完成后,我们需要构建应用程序,检查是否所有的内容都可以正常构建:
dotnet build
dotnet run下一步是更新我们的 ApiDbContext,以便使用 Asp.Net 为我们提供的身份提供程序,导航到 Data 文件夹中的ApiDbContext,然后按以下内容更新 ApiDbContext 类:
public class ApiDbContext : IdentityDbContext
通过从 IdentityDbContext 而不是 DbContext 继承,EntityFramework 将知道我们正在使用身份验证,并且将为我们构建基础设施以使用默认身份表。
要在我们的数据库中生成身份表,我们需要准备迁移脚本并运行它们。也就是说,我们需要在终端中输入并运行以下命令:
dotnet ef migrations add "Adding authentication to our Api"
dotnet ef database update迁移完成后,我们可以使用 Dbeaver 打开数据库 app.db,我们可以看到 EntityFramework 已经为我们创建了身份表。
下一步是设置控制器并为用户构建注册流程。我们需要在 Controllers 文件夹中创建一个新的控制器,并创建对应的 DTO 类(Data Transfer Objects)。
先在根目录中的 Configuration 文件夹中添加一个名为 AuthResult 的类:
// Configuration\AuthResult.cs

public class AuthResult
{
    public string Token { get; set; }
    public bool Success { get; set; }
    public List<string> Errors { get; set; }
}
然后我将添加一些文件夹来组织 DTOs,在 Models 文件夹中添加一个名为 DTOs 的文件夹,然后在此文件夹中创建两个子文件夹 RequestsResponses
我们需要添加供我们在控制器中的注册 Action 使用的 UserRegistrationDto。导航到 Models/DTO/Requests,添加一个新类 UserRegistrationDto。
// Models\DTOs\Requests\UserRegistrationDto.cs

public class UserRegistrationDto
{
    [Required]
    public string Username { get; set; }
    [Required]
    [EmailAddress]
    public string Email { get; set; }
    [Required]
    public string Password { get; set; }
}
添加 RegistrationResponse 响应类。
// Models\DTOs\Responses\RegistrationResponse.cs

public class RegistrationResponse : AuthResult
{

}
现在,我们需要添加用户注册控制器,在控制器文件夹中添加一个新类,命名为 AuthManagementController,并使用以下代码更新它:
// Controllers\AuthManagementController.cs

[Route("api/[controller]")] // api/authmanagement
[ApiController]
public class AuthManagementController : ControllerBase
{
    private readonly UserManager<IdentityUser> _userManager;
    private readonly JwtConfig _jwtConfig;

    public AuthManagementController(
            UserManager<IdentityUser> userManager,
            IOptionsMonitor<JwtConfig> optionsMonitor)
    {
        _userManager = userManager;
        _jwtConfig = optionsMonitor.CurrentValue;
    }

    [HttpPost]
    [Route("Register")]
    public async Task<IActionResult> Register([FromBody] UserRegistrationDto user)
    {
        // 检查传入请求是否有效
        if(ModelState.IsValid)
        {
            // 检查使用相同电子邮箱的用户是否存在
            var existingUser = await _userManager.FindByEmailAsync(user.Email);

            if(existingUser != null)
            {
                return BadRequest(new RegistrationResponse()
                {
                    Errors = new List<string>()
                    {
                        "Email already in use"
                    },
                    Success = false
                });
            }

            var newUser = new IdentityUser() { Email = user.Email, UserName = user.Username };
            var isCreated = await _userManager.CreateAsync(newUser, user.Password);
            if(isCreated.Succeeded)
            {
                var jwtToken =  GenerateJwtToken( newUser);

                return Ok(new RegistrationResponse()
                {
                    Success = true,
                    Token = jwtToken
                });
            }
            else
            {
                return BadRequest(new RegistrationResponse()
                {
                    Errors = isCreated.Errors.Select(x => x.Description).ToList(),
                    Success = false
                });
            }
        }

        return BadRequest(new RegistrationResponse()
        {
            Errors = new List<string>()
            {
                "Invalid payload"
            },
            Success = false
        });
    }

    private string GenerateJwtToken(IdentityUser user)
    {
        //现在,是时候定义 jwt token 了,它将负责创建我们的 tokens
        var jwtTokenHandler = new JwtSecurityTokenHandler();

        // 从 appsettings 中获得我们的 secret
        var key = Encoding.ASCII.GetBytes(_jwtConfig.Secret);

        // 定义我们的 token descriptor
        // 我们需要使用 claims (token 中的属性)给出关于 token 的信息,它们属于特定的用户,
        // 因此,可以包含用户的 Id、名字、邮箱等。
        // 好消息是,这些信息由我们的服务器和 Identity framework 生成,它们是有效且可信的。
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new []
            {
                new Claim("Id", user.Id),
                new Claim(JwtRegisteredClaimNames.Email, user.Email),
                new Claim(JwtRegisteredClaimNames.Sub, user.Email),
                // Jti 用于刷新 token,我们将在下一篇中讲到
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
            }),
            // token 的过期时间需要缩短,并利用 refresh token 来保持用户的登录状态,
            // 不过由于这只是一个演示应用,我们可以对其进行延长以适应我们当前的需求
            Expires = DateTime.UtcNow.AddHours(6),
            // 这里我们添加了加密算法信息,用于加密我们的 token
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };

        var token = jwtTokenHandler.CreateToken(tokenDescriptor);
        var jwtToken = jwtTokenHandler.WriteToken(token);

        return jwtToken;
    }
}
添加完注册的 Action 后,我们可以在 Postman 中对其进行测试并获得 JWT token。
接下来是创建用户登录请求:
// Models\DTOs\Requests\UserLoginRequest.cs

public class UserLoginRequest
{
    [Required]
    [EmailAddress]
    public string Email { get; set; }
    [Required]
    public string Password { get; set; }
}
然后,我们需要在 AuthManagementController 中添加 Login 方法:
[HttpPost]
[Route("Login")]
public async Task<IActionResult> Login([FromBody] UserLoginRequest user)
{
    if(ModelState.IsValid)
    {
        // 检查使用相同电子邮箱的用户是否存在
        var existingUser = await _userManager.FindByEmailAsync(user.Email);

        if(existingUser == null)
        {
            // 出于安全原因,我们不想透露太多关于请求失败的信息
            return BadRequest(new RegistrationResponse()
            {
                Errors = new List<string>()
                {
                    "Invalid login request"
                },
                Success = false
            });
        }

        // 现在我们需要检查用户是否输入了正确的密码
        var isCorrect = await _userManager.CheckPasswordAsync(existingUser, user.Password);

        if(!isCorrect)
        {
            // 出于安全原因,我们不想透露太多关于请求失败的信息
            return BadRequest(new RegistrationResponse()
            {
                Errors = new List<string>()
                {
                    "Invalid login request"
                },
                Success = false
            });
        }

        var jwtToken = GenerateJwtToken(existingUser);

        return Ok(new RegistrationResponse()
        {
            Success = true,
            Token = jwtToken
        });
    }

    return BadRequest(new RegistrationResponse()
    {
        Errors = new List<string>()
        {
            "Invalid payload"
        },
        Success = false
    });
}
现在,我们可以在 Postman 中对其进行测试,我们将会看到 JWT token 已经成功生成。
下一步是保护我们的控制器,需要做的就是向控制器添加 Authorize 属性。
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Route("api/[controller]")] // api/todo
[ApiController]
public class TodoController : ControllerBase
此时,如果我们再对 Todo 进行测试,则由于未获得授权,我们将会无法执行任何请求。为了发送带授权的请求,我们需要添加带有 Bearer token 的授权 Header,以便 http://Asp.Net 可以验证它,并授予我们执行操作的权限。



译者注:
添加 Bearer token 请求头的方法是:在 Headers 中,添加一个名称为 Authorization 的 Header 项,值为 Bearer <token>(需将 <token> 替换为真实的 token 值)。使用 Postman 测试时,可参考 Postman 官方文档:https://learning.postman.com/docs/sending-requests/authorization/#bearer-token。
至此,我们已经完成了使用 JWT 为 REST API 添加身份验证的功能。
感谢您花时间阅读本文。
本文是 API 开发系列的第二部分,本系列还包含:

  • Part 1:Asp.Net Core 5 REST API - Step by Step
  • Part 3:Asp Net Core 5 REST API 中使用 RefreshToken 刷新 JWT - Step by Step

作者 : Mohamad Lawand
译者 : 技术译民
出品 : 技术译站
链接 : 英文原文
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表