首页 > 解决方案 > ASP.NET Core 3.1 - 如何在经过身份验证后持久保存 JWT 令牌

问题描述

我一直在尝试让 JWT 身份验证正常工作,但尚不清楚这需要如何完成,以及在 ASP.NET Core 3.1 中执行此操作的最佳方法是什么。

我正在使用基于 Cookie 的身份验证,我假设它与会话 ID 相关联,会话 ID 与正在运行的服务器实例相关联。如果我想使用具有不同 IP 地址和端口的多台服务器,我假设 cookie 将不再起作用,因此需要其他可以跨系统验证的东西。

我一直在关注各种 Web 示例,但不清楚一旦用户“已通过身份验证” - 登录后,我拥有 JWT 令牌之后该怎么做。用户登录后,他们可以通过以下方式访问系统的任何部分:html 链接菜单)。

如何在所有后续请求中传递令牌?

在用户通过身份验证并将令牌存储在浏览器 sessionStore 或 localStorage 或 Cookie 后,我是否将用户重定向到欢迎页面?处理这个问题的最佳方法是什么。

options.success = function (obj) {
     sessionStorage.setItem("token", obj.token);
     sessionStorage.setItem("userName",$("#userName").val());
}

HTTP 标头

Authorization HTTP Header变量是否可以工作,是否会在浏览器的所有后续请求中发送,充当 HTTP 客户端。这个 HTTP 头会持续多久,一旦 TCP 套接字关闭,它会丢失吗?如何在 ASP.NET Core 3.1 中设置此 HTTP 标头变量?然后服务器会使用这个 Header 来验证令牌,并再次将其传递给后续请求吗?

目前我有这个,一旦用户通过身份验证,它就会在正文中返回令牌:

        var claims = await GetClaims(user);
        var token = GenerateSecurityToken(claims);

        return Ok(new { Token = token })

AJAX 调用

我有几个表单和几个 AJAX 调用,如何将其作为手动方法实现似乎相当乏味。

有没有办法从类似于@Html.AntiForgeryToken() 我所有 Ajax 调用中使用的 AntiForgery 令牌的隐藏表单变量中获取 JWT 令牌?

jQuery 使用隐藏的表单变量:

request = $.ajax({
    async: true,
    url: url,
    type: "POST",
    contentType: "application/json; charset=utf-8",
    dataType: "json",
    headers: {
        RequestVerificationToken:
        $('input:hidden[name="__RequestVerificationToken"]').val()
    },
    WHAT DO I ADD FOR JWT ? 
    data: JSON.stringify(data)
}).done(function() {
    completion();
}).fail(function() {
    // fail
});

HTML 表单

我有 Razor Pages 并有一些表单,然后 POST 回控制器。如何包含令牌?

控制器

除了我在 Startup.cs 中的内容之外,在使用 JWT 时还有什么需要执行的吗?我知道我需要处理令牌刷新,但我会离开一个单独的问题。

菜单中的链接 - HTTP GET

我可以通过将令牌添加到 URL 的末尾来操纵呈现给用户的菜单/链接,但是应该如何完成呢?

标签: c#httpasp.net-corejwt

解决方案


经过大量阅读后,我找到了一些答案以及可行的解决方案。

HTTP HEADERS 获得令牌后,需要保留令牌才能访问系统。使用 HTTP 标头存储令牌不会持续存在,因为 HTTP 协议 1.0 和 1.1 和 1.2 将在某个时候关闭 TCP 套接字以及它所具有的状态,即令牌。不适合不控制 Http 连接的 WebClients,但可以用于移动开发、Android 或 IOS,如果您可以控制 HttpHeaders。

本地存储 您可以使用浏览器localStoragesessionStorage,但这些都有一些安全风险,JavaScript 可以读取值 -XSS攻击。

COOKIES 另一种选择是将令牌存储在 Cookie 中;cookie 将与每个 http 请求一起传递,客户端不需要为此发生任何特殊情况。这种方法不易受到 XSS 攻击。但很容易发生CSRF。但是 CORS 再次可以帮助解决这个问题。

最好将 Cookie 设置为 HttpOnly,这样 cookie 将仅通过 HTTPS 传递。在这里阅读更多

这是我根据我在 这里找到的一篇文章的实现

Startup.cs配置服务...

        // openssl rand -hex 16 => 32 bytes when read
        var jwt_key = Configuration.GetSection("JwtOption:IssuerSigningKey").Value;
        var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwt_key));
        var tokenValidationParameters = new TokenValidationParameters
            {
                // The signing key must match!
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = signingKey,

                // Validate the JWT Issuer (iss) claim
                ValidateIssuer = true,
                ValidIssuer = "some uri",

                // Validate the JWT Audience (aud) claim
                ValidateAudience = true,
                ValidAudience = "the web",

                // Validate the token expiry
                ValidateLifetime = true,

                // If you want to allow a certain amount of clock drift, set that here:
                ClockSkew = TimeSpan.Zero
            };

        services.AddSingleton(tokenValidationParameters);

        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
        {
            int minute = 60;
            int hour = minute * 60;
            int day = hour * 24;
            int week = day * 7;
            int year = 365 * day;

            options.LoginPath = "/auth/login";
            options.AccessDeniedPath = "/auth/accessdenied";
            options.Cookie.IsEssential = true;
            options.SlidingExpiration = true;
            options.ExpireTimeSpan = TimeSpan.FromSeconds(day/2);

            options.Cookie.Name = "access_token";

            options.TicketDataFormat = new CustomJwtDataFormat(
                SecurityAlgorithms.HmacSha256,
                tokenValidationParameters);
        });

CustomJwtDataFormat这将验证我们的令牌。

public class CustomJwtDataFormat :ISecureDataFormat<AuthenticationTicket>
{
    private readonly string algorithm;
    private readonly TokenValidationParameters validationParameters;

    public CustomJwtDataFormat(string algorithm, TokenValidationParameters validationParameters)
    {
        this.algorithm = algorithm;
        this.validationParameters = validationParameters;
    }

    public AuthenticationTicket Unprotect(string protectedText)
        => Unprotect(protectedText, null);

    public AuthenticationTicket Unprotect(string protectedText, string purpose)
    {
        var handler = new JwtSecurityTokenHandler();
        ClaimsPrincipal principal = null;
        SecurityToken validToken = null;

        try
        {
            principal = handler.ValidateToken(protectedText, this.validationParameters, out validToken);

            var validJwt = validToken as JwtSecurityToken;

            if (validJwt == null)
            {
                throw new ArgumentException("Invalid JWT");
            }

            if (!validJwt.Header.Alg.Equals(algorithm, StringComparison.Ordinal))
            {
                throw new ArgumentException($"Algorithm must be '{algorithm}'");
            }

            // Additional custom validation of JWT claims here (if any)
        }
        catch (SecurityTokenValidationException e)
        {
            System.Console.WriteLine(e);
            return null;
        }
        catch (ArgumentException e)
        {
            System.Console.WriteLine(e);
            return null;
        }

        // Validation passed. Return a valid AuthenticationTicket:
        return new AuthenticationTicket(principal, new AuthenticationProperties(), "Cookie");
    }

    // This ISecureDataFormat implementation is decode-only
    public string Protect(AuthenticationTicket data)
    {
        throw new NotImplementedException();
    }

    public string Protect(AuthenticationTicket data, string purpose)
    {
        throw new NotImplementedException();
    }
}

LoginController验证用户名和密码后,调用 SignInUser

    private string GenerateSecurityToken(List<Claim> claims)
    {  
        var tokenHandler = new JwtSecurityTokenHandler();
        var expire = System.DateTime.UtcNow.AddMinutes(userService.GetJwtExpireDate());
        var tokenDescriptor = new SecurityTokenDescriptor
        {  
            Subject = new ClaimsIdentity(claims),
            Expires = expire,
            SigningCredentials = new SigningCredentials(tokenValidationParameters.IssuerSigningKey, SecurityAlgorithms.HmacSha256Signature),
            Audience = tokenValidationParameters.ValidAudience,
            Issuer = tokenValidationParameters.ValidIssuer
        };  

        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }

    private async Task<List<Claim>> GetClaims(UserModel user) {
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new Claim(ClaimTypes.Name, user.Email),
            new Claim(ClaimTypes.Email, user.Email),
        };

        // add roles
        var roleList = await userService.UserRoles(user.Email);
        foreach (var role in roleList)
        {
            var claim = new Claim(ClaimTypes.Role, role.Role);
            claims.Add(claim);
        }

        return claims;
    }

    private async Task<IActionResult> SignInUser(UserModel user, bool rememberMe)
    {
        var claims = await GetClaims(user);
        var token = GenerateSecurityToken(claims);

        // return Ok(new { Token = token });
        // HttpContext.Request.Headers.Add("Authorization", $"Bearer {token}");

        // HttpContext.Response.Cookies.Append(
        HttpContext.Response.Cookies.Append("access_token", token, new CookieOptions { HttpOnly = true, Secure = true }); 
        return RedirectToAction("Index", "Home", new { area = "" });
    }

推荐阅读