【问题标题】:ASP.NET Core 3.1 - How to persist JWT Tokens once AuthenticatedASP.NET Core 3.1 - 如何在经过身份验证后持久保存 JWT 令牌
【发布时间】:2020-07-10 09:13:01
【问题描述】:

我一直在尝试让 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 中设置此 HTTP 标头变量 3.1? 然后服务器是否会使用此标头来验证令牌,以及 再次传递它以在后续请求中使用?

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

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

        return Ok(new { Token = token })

AJAX 调用

我有几个表单和几个 AJAX 调用,如何以手动方式实现这一点似乎相当乏味。

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

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 回控制器。如何包含令牌?

控制器

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

菜单中的链接 - HTTP GET

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

【问题讨论】:

    标签: c# http asp.net-core jwt


    【解决方案1】:

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

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

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

    饼干 另一种选择是将令牌存储在 Cookie 中; cookie 将与每个 http 请求一起传递,客户端没有什么特别的需要发生。这种方法不易受到 XSS 攻击。但是很容易出现CSRF。但是 CORS 再次可以帮助解决这个问题。

    最好将 Cookie 设置为 HttpOnly,这样 cookie 将仅通过 HTTPS 传递。 Read more here

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

    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 = "" });
        }
    

    【讨论】:

    • 在 SPA 网站中,将令牌存储在 JavaScript 变量中比在 localStorage/sessionStorage 中更安全。对于服务器端 Razor 页面,最好使用仅限 HTTPS 的 cookie。
    猜你喜欢
    • 2021-12-17
    • 1970-01-01
    • 2019-04-16
    • 2019-03-10
    • 2020-08-22
    • 2018-02-24
    • 2019-02-22
    • 2018-08-03
    • 2019-01-04
    相关资源
    最近更新 更多