c# - 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 的末尾来操纵呈现给用户的菜单/链接,但是应该如何完成呢?
解决方案
经过大量阅读后,我找到了一些答案以及可行的解决方案。
HTTP HEADERS 获得令牌后,需要保留令牌才能访问系统。使用 HTTP 标头存储令牌不会持续存在,因为 HTTP 协议 1.0 和 1.1 和 1.2 将在某个时候关闭 TCP 套接字以及它所具有的状态,即令牌。不适合不控制 Http 连接的 WebClients,但可以用于移动开发、Android 或 IOS,如果您可以控制 HttpHeaders。
本地存储
您可以使用浏览器localStorage
或sessionStorage
,但这些都有一些安全风险,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 = "" });
}
推荐阅读
- usb - Windows CE 上的未知 USB 设备
- amazon-web-services - 带有 WWW 的 URL 仅适用于首次加载 Elastic Beanstalk
- linux - Bash - 从文件中读取行时打印第一列和第二列
- python - Django:自动建议使用 Elasticsearch DSL
- numpy - 在 PyCharm 中导入 NumPy 时出错
- android - CountDownTimer 错误地将文本设置为比要求的更多视图
- html - R/Shiny : RenderUI 在循环中生成多个对象
- ruby-on-rails - Rails 5 和日期选择器
- python-3.x - 从 dict 或 list 获取 3 个不同数据帧的值的最佳方法
- swift - 如何在我现有的 XIB 中调用 XIB?