小编典典

ASP.NET Web API 的 JWT 身份验证

all

我正在尝试在我的 Web API 应用程序中支持 JWT 不记名令牌(JSON Web 令牌),但我迷路了。

我看到了对 .NET Core 和 OWIN 应用程序的支持。
我目前在 IIS 中托管我的应用程序。

如何在我的应用程序中实现此身份验证模块?有什么方法可以使用<authentication>类似于我使用表单/Windows 身份验证的方式的配置?


阅读 86

收藏
2022-04-12

共1个答案

小编典典

现在,安全方面发生了很多变化,尤其是 JWT 越来越流行。在这个答案中,我将尝试以最简单和基本的方式解释如何使用 JWT,这样我们就不会迷失在
OWIN、Oauth2、ASP.NET Identity 等的丛林中。

如果您不了解 JWT 令牌,则需要查看:

https://www.rfc-editor.org/rfc/rfc7519

基本上,JWT 令牌如下所示:

<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>

例子:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ

JWT 令牌包含三个部分:

  1. 标头:以 Base64 编码的 JSON 格式
  2. 声明:以 Base64 编码的 JSON 格式。
  3. 签名:基于以 Base64 编码的 Header 和 Claims 创建和签名。

如果您将网站jwt.io与上面的令牌一起使用,您可以对令牌进行解码并看到如下所示:

jwt.io 的屏幕截图,其中包含原始 jwt 源和它所代表的解码
JSON

从技术上讲,JWT
使用从标头和声明中签名的签名,并使用标头中指定的安全算法(例如:HMACSHA256)。因此,如果您在其声明中存储任何敏感信息,则必须通过 HTTP 传输
JWT。

现在,为了使用 JWT 身份验证,如果您有旧版 Web Api 系统,则实际上不需要 OWIN 中间件。简单的概念是如何提供 JWT
令牌以及如何在请求到来时验证令牌。就是这样。

我创建的演示 (github)中,为了保持 JWT
令牌的轻量级,我只存储usernameexpiration time.
但是这样,你必须重新构建新的本地身份(主体)来添加更多信息,比如角色,如果你想做角色授权等。但是,如果你想在 JWT
中添加更多信息,这取决于你:它非常灵活。

您可以使用控制器操作简单地提供 JWT 令牌端点,而不是使用 OWIN 中间件:

public class TokenController : ApiController
{
    // This is naive endpoint for demo, it should use Basic authentication
    // to provide token or POST request
    [AllowAnonymous]
    public string Get(string username, string password)
    {
        if (CheckUser(username, password))
        {
            return JwtManager.GenerateToken(username);
        }

        throw new HttpResponseException(HttpStatusCode.Unauthorized);
    }

    public bool CheckUser(string username, string password)
    {
        // should check in the database
        return true;
    }
}

这是一个幼稚的行为;在生产中,您应该使用 POST 请求或基本身份验证端点来提供 JWT 令牌。

如何基于 生成令牌username

您可以使用System.IdentityModel.Tokens.Jwt从 Microsoft 调用的 NuGet
包来生成令牌,如果您愿意,甚至可以使用其他包。在演示中,我使用HMACSHA256with SymmetricKey

/// <summary>
/// Use the below code to generate symmetric Secret Key
///     var hmac = new HMACSHA256();
///     var key = Convert.ToBase64String(hmac.Key);
/// </summary>
private const string Secret = "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==";

public static string GenerateToken(string username, int expireMinutes = 20)
{
    var symmetricKey = Convert.FromBase64String(Secret);
    var tokenHandler = new JwtSecurityTokenHandler();

    var now = DateTime.UtcNow;
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name, username)
        }),

        Expires = now.AddMinutes(Convert.ToInt32(expireMinutes)),

        SigningCredentials = new SigningCredentials(
            new SymmetricSecurityKey(symmetricKey), 
            SecurityAlgorithms.HmacSha256Signature)
    };

    var stoken = tokenHandler.CreateToken(tokenDescriptor);
    var token = tokenHandler.WriteToken(stoken);

    return token;
}

提供 JWT 令牌的端点已完成。

请求到来时如何验证 JWT?

演示中,我构建
JwtAuthenticationAttribute了继承自IAuthenticationFilter(有关身份验证过滤器的更多详细信息,请参见此处)。

使用此属性,您可以验证任何操作:您只需将此属性放在该操作上。

public class ValueController : ApiController
{
    [JwtAuthentication]
    public string Get()
    {
        return "value";
    }
}

如果要验证 WebAPI 的所有传入请求(不特定于控制器或操作),也可以使用 OWIN 中间件或 DelegateHander

以下是身份验证过滤器的核心方法:

private static bool ValidateToken(string token, out string username)
{
    username = null;

    var simplePrinciple = JwtManager.GetPrincipal(token);
    var identity = simplePrinciple.Identity as ClaimsIdentity;

    if (identity == null || !identity.IsAuthenticated)
        return false;

    var usernameClaim = identity.FindFirst(ClaimTypes.Name);
    username = usernameClaim?.Value;

    if (string.IsNullOrEmpty(username))
       return false;

    // More validate to check whether username exists in system

    return true;
}

protected Task<IPrincipal> AuthenticateJwtToken(string token)
{
    string username;

    if (ValidateToken(token, out username))
    {
        // based on username to get more information from database 
        // in order to build local identity
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, username)
            // Add more claims if needed: Roles, ...
        };

        var identity = new ClaimsIdentity(claims, "Jwt");
        IPrincipal user = new ClaimsPrincipal(identity);

        return Task.FromResult(user);
    }

    return Task.FromResult<IPrincipal>(null);
}

工作流程是使用 JWT 库(上面的 NuGet 包)来验证 JWT
令牌,然后返回ClaimsPrincipal。您可以执行更多验证,例如检查系统上是否存在用户,并根据需要添加其他自定义验证。

验证 JWT 令牌并取回本金的代码:

public static ClaimsPrincipal GetPrincipal(string token)
{
    try
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var jwtToken = tokenHandler.ReadToken(token) as JwtSecurityToken;

        if (jwtToken == null)
            return null;

        var symmetricKey = Convert.FromBase64String(Secret);

        var validationParameters = new TokenValidationParameters()
        {
            RequireExpirationTime = true,
            ValidateIssuer = false,
            ValidateAudience = false,
            IssuerSigningKey = new SymmetricSecurityKey(symmetricKey)
        };

        SecurityToken securityToken;
        var principal = tokenHandler.ValidateToken(token, validationParameters, out securityToken);

        return principal;
    }
    catch (Exception)
    {
        //should write log
        return null;
    }
}

如果验证了 JWT 令牌并返回了委托人,您应该构建一个新的本地身份并将更多信息放入其中以检查角色授权。

请记住在全局范围内添加config.Filters.Add(new AuthorizeAttribute());(默认授权),以防止对您的资源的任何匿名请求。

您可以使用 Postman 来测试演示

请求令牌(我上面提到的幼稚,仅用于演示):

GET http://localhost:{port}/api/token?username=cuong&password=1

将 JWT 令牌放入授权请求的标头中,例如:

GET http://localhost:{port}/api/value

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s

该演示可以在这里找到:https
://github.com/cuongle/WebApi.Jwt

2022-04-12