使用者验证与授权 – Token Based Authentication:发行 JSON Web Token

昨天已经为先前实践的 ASP.NET Core Web API 加入了会员注册与登入的功能,但是在登入的使用者认证是属于 Cookie Based Authentication 因为我们所开发的 Web API 主要是要移动设备使用,参考一些文章的比较,看来 Token Based Authentication 比较合适,所以今天就来学习让我们的 ASP.NET Core Web API 可以发行 JSON Web Token 吧!


有关 Token Based Authentication 请先参考这一篇文章,有关 JSON Web Token 请先参考这一篇文章。然后阿源哥哥再以比较口语化的文字说明一下:

Token Based Authentication

如上图所示,好像大型游乐园的概念,在票卷发行机构《付钱》,当确认金钱真伪和额度后,便会发行一张票卷(或是磁扣手环之类的),票卷内注明发行日期使用期限,可以使用的游乐设施等,接着每次要玩游乐设施时,只要出示该票卷即可,当然设施管理者会先确认票卷真伪、是否为有效期限,以及是否可使用该项设施之后才提供该项设施服务。

相同的道理,服务器端首先由使用者所提供的账号密码确认是否为合法使用者,若是合法使用者就会发行一个含有各项资讯,称为【Token】的字符串给使用者,往后使用者即可带着该【Token】请求执行各项 REST API 服务,而执行前也会由【Token】中所夹带的资讯查验是否该提供该项服务。

JSON Web Token

说明白一点,所谓的【Token】也只是一长串的字符串而已,虽然格式可自订,但是已有机构订出来标准,该标准称为 JSON Web Token 该格式请看下图,再听说明:

送到使客户端的 Token 为加密过后,如图左所示的:aaaaa.bbbbb.ccccc. 分隔的三段字符串,这三段文字解密后如图右三个区段分别代表:

  • HEADER
    表头主要包含了两项资讯:
    1. 杂凑算法 
    2. Token 的型式
  • PAYLOAD
    这一段主要是将来在 Web API 所要包含的资讯,例如主题、姓名、账号、发行单位、权限 ....... 等,主要是看将来应用程序设计需要包含哪些资讯才可让接收的 Web API 可以继续往后的工作。
  • SIGNATURE
    这一段是签章主要是用于防伪用

安装套件

首先请为 Web API 项目安装 Microsoft.AspNetCore.Authentication.JwtBearer 套件,以用来实践产生 JSON Web Token(JWT):

在配置文件中加入与 Token 有关的参数

在 appsettings.json 中加入如下的代码:

{
  ......
  ......
  ......
  
  "Tokens": {
    "读者": "http://demaewebapi.azurewebsites.net",
    "Issuer": "http://demaewebapi.azurewebsites.net",
    "Key": "keigenisagoodman"
  }
}

为了能在所有的 Controller 中存取写在 appsettings.json 中的参数,请在 Startup.cs 中加入服务 services.AddSingleton(Configuration);  如下所示:

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddSingleton(Configuration);


    // 省略



}

实践发行 JSON Web Token

首先在 AccountController 的构建函数中加入可用的服务,如下所示:

public class AccountController : Controller
{
    private UserManager _userManager;
    private SignInManager _signInManager;
    private IPasswordHasher _passwordHasher;
    private IConfigurationRoot _config;

    public AccountController(
        UserManager userManager,
        SignInManager signInManager,
        IPasswordHasher passwordHasher,
        IConfigurationRoot config)
    {
        _userManager = userManager;
        _signInManager = signInManager;
        _passwordHasher = passwordHasher;
        _config = config;
    }

    // 省略

}

然后新增发行 JSON Web Token 的方法,程序如下:

[HttpPost("token")]
public async Task Token([FromBody] LoginModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest();
    }

    var user = await _userManager.FindByNameAsync(model.Email);

    if (user == null || _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, model.Password) != PasswordVerificationResult.Success)
    {
        return BadRequest();
    }

    
    var userClaims = await _userManager.GetClaimsAsync(user);
    var claims = new[]
    {
        new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        new Claim(JwtRegisteredClaimNames.GivenName, user.NickName),
        new Claim(JwtRegisteredClaimNames.Email, user.Email)
    }.Union(userClaims);

             
    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Tokens:Key"]));
    var token = new JwtSecurityToken(
      issuer: _config["Tokens:Issuer"],
      audience: _config["Tokens:读者"],
      claims: claims,
      expires: DateTime.UtcNow.AddMonths(3),              
      signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));

    return Ok(new
    {
        token = new JwtSecurityTokenHandler().WriteToken(token),
        expiration = token.ValidTo
    });
}

上述程序分说明,该产生 Token 的 Post 方法,接收使用者传来的账号和密码(以 LoginModel)首先以 ModelState.IsValid 检查是否有正确填写,若填写不正确回传 BadRequest() ,接着由所传来的使用者账号查询是否有该会员数据,并比对

[HttpPost("token")]
public async Task Token([FromBody] LoginModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest();
    }

    var user = await _userManager.FindByNameAsync(model.Email);

    if (user == null || _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, model.Password) != PasswordVerificationResult.Success)
    {
        return BadRequest();
    }

    
    // 省略



}

[HttpPost("token")]
public async Task Token([FromBody] LoginModel model)
{
    // 省略
    
    var userClaims = await _userManager.GetClaimsAsync(user);
    var claims = new[]
    {
        new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        new Claim(JwtRegisteredClaimNames.GivenName, user.NickName),
        new Claim(JwtRegisteredClaimNames.Email, user.Email)
    }.Union(userClaims);

   // 省略          
   
}

[HttpPost("token")]
public async Task Token([FromBody] LoginModel model)
{
    
    // 省略
             
    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Tokens:Key"]));
    var token = new JwtSecurityToken(
      issuer: _config["Tokens:Issuer"],
      audience: _config["Tokens:读者"],
      claims: claims,
      expires: DateTime.UtcNow.AddMonths(3),              
      signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));

    // 省略
}

最后将组装好的 token 以 WriteToken() 方法写出回传,顺便也把到期日回传出去:

[HttpPost("token")]
public async Task Token([FromBody] LoginModel model)
{
    // 省略

    return Ok(new
    {
        token = new JwtSecurityTokenHandler().WriteToken(token),
        expiration = token.ValidTo
    });
}

实践完成,接着就使用 Postman 测试一下吧:

看来有产生了一大长串的 Token 字符串,接着再产生的 Token 字符串贴到 https://jwt.io/  反解密看看产生的字符串是否为我们想要的:

看来解密后的数据确实是我们在程序中所加入的,值得留意的是签名档的验证,把程序中写在 appsettings.json 的 Tokens.Key 贴入,也确定验证成功,是我们发行的没错。

好吧!今天就学习到这里。明天再来学习如何判断《前端》带来的 Token 确实是我们发行的,且有足够权限使用我们所开发的 Web API。