当前位置:Gxlcms > asp.net > 详解ASP.NET Core Token认证

详解ASP.NET Core Token认证

时间:2021-07-01 10:21:17 帮助过:12人阅读

令牌认证(Token Authentication)已经成为单页应用(SPA)和移动应用事实上的标准。即使是传统的B/S应用也能利用其优点。优点很明白:极少的服务端数据管理、可扩展性、可以使用单独的认证服务器和应用服务器分离。

如果你对令牌(token)不是太了解,可以看这篇文章( overview of token authentication and JWTs)

令牌认证在asp.net core中集成。其中包括保护Bearer Jwt的路由功能,但是移除了生成token和验证token的部分,这些可以自定义或者使用第三方库来实现,得益于此,MVC和Web api项目可以使用令牌认证,而且很简单。下面将一步一步实现,代码可以在( 源码)下载。

ASP.NET Core令牌验证

首先,背景知识:认证令牌,例如JWTs,是通过http 认证头传递的,例如:

  1. GET /foo
  2. Authorization: Bearer [token]

令牌可以通过浏览器cookies。传递方式是header或者cookies取决于应用和实际情况,对于移动app,使用headers,对于web,推荐在html5 storage中使用cookies,来防止xss攻击。

asp.net core对jwts令牌的验证很简单,特别是你通过header传递。

1、生成 SecurityKey,这个例子,我生成对称密钥验证jwts通过HMAC-SHA256加密方式,在startup.cs中:

  1. // secretKey contains a secret passphrase only your server knows
  2. var secretKey = "mysupersecret_secretkey!123";
  3. var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));

验证 header中传递的JWTs

在 Startup.cs中,使用Microsoft.AspNetCore.Authentication.JwtBearer中的UseJwtBearerAuthentication 方法获取受保护的api或者mvc路由有效的jwt。

  1. var tokenValidationParameters = new TokenValidationParameters
  2. {
  3. // The signing key must match!
  4. ValidateIssuerSigningKey = true,
  5. IssuerSigningKey = signingKey,
  6. // Validate the JWT Issuer (iss) claim
  7. ValidateIssuer = true,
  8. ValidIssuer = "ExampleIssuer",
  9. // Validate the JWT Audience (aud) claim
  10. ValidateAudience = true,
  11. ValidAudience = "ExampleAudience",
  12. // Validate the token expiry
  13. ValidateLifetime = true,
  14. // If you want to allow a certain amount of clock drift, set that here:
  15. ClockSkew = TimeSpan.Zero
  16. };
  17. app.UseJwtBearerAuthentication(new JwtBearerOptions
  18. {
  19. AutomaticAuthenticate = true,
  20. AutomaticChallenge = true,
  21. TokenValidationParameters = tokenValidationParameters
  22. });

通过这个中间件,任何[Authorize]的请求都需要有效的jwt:

签名有效;

过期时间;

有效时间;

Issuer 声明等于“ExampleIssuer”

订阅者声明等于 “ExampleAudience”

如果不是合法的JWT,请求终止,issuer声明和订阅者声明不是必须的,它们用来标识应用和客户端。

在cookies中验证JWTs

ASP.NET Core中的cookies 认证不支持传递jwt。需要自定义实现 ISecureDataFormat接口的类。现在,你只是验证token,不是生成它们,只需要实现Unprotect方法,其他的交给System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler这个类处理。

  1. using System;
  2. using System.IdentityModel.Tokens.Jwt;
  3. using System.Security.Claims;
  4. using Microsoft.AspNetCore.Authentication;
  5. using Microsoft.AspNetCore.Http.Authentication;
  6. using Microsoft.IdentityModel.Tokens;
  7. namespace SimpleTokenProvider
  8. {
  9. public class CustomJwtDataFormat : ISecureDataFormat<AuthenticationTicket>
  10. {
  11. private readonly string algorithm;
  12. private readonly TokenValidationParameters validationParameters;
  13. public CustomJwtDataFormat(string algorithm, TokenValidationParameters validationParameters)
  14. {
  15. this.algorithm = algorithm;
  16. this.validationParameters = validationParameters;
  17. }
  18. public AuthenticationTicket Unprotect(string protectedText)
  19. => Unprotect(protectedText, null);
  20. public AuthenticationTicket Unprotect(string protectedText, string purpose)
  21. {
  22. var handler = new JwtSecurityTokenHandler();
  23. ClaimsPrincipal principal = null;
  24. SecurityToken validToken = null;
  25. try
  26. {
  27. principal = handler.ValidateToken(protectedText, this.validationParameters, out validToken);
  28. var validJwt = validToken as JwtSecurityToken;
  29. if (validJwt == null)
  30. {
  31. throw new ArgumentException("Invalid JWT");
  32. }
  33. if (!validJwt.Header.Alg.Equals(algorithm, StringComparison.Ordinal))
  34. {
  35. throw new ArgumentException($"Algorithm must be '{algorithm}'");
  36. }
  37. // Additional custom validation of JWT claims here (if any)
  38. }
  39. catch (SecurityTokenValidationException)
  40. {
  41. return null;
  42. }
  43. catch (ArgumentException)
  44. {
  45. return null;
  46. }
  47. // Validation passed. Return a valid AuthenticationTicket:
  48. return new AuthenticationTicket(principal, new AuthenticationProperties(), "Cookie");
  49. }
  50. // This ISecureDataFormat implementation is decode-only
  51. public string Protect(AuthenticationTicket data)
  52. {
  53. throw new NotImplementedException();
  54. }
  55. public string Protect(AuthenticationTicket data, string purpose)
  56. {
  57. throw new NotImplementedException();
  58. }
  59. }
  60. }

在startup.cs中调用

  1. var tokenValidationParameters = new TokenValidationParameters
  2. {
  3. // The signing key must match!
  4. ValidateIssuerSigningKey = true,
  5. IssuerSigningKey = signingKey,
  6. // Validate the JWT Issuer (iss) claim
  7. ValidateIssuer = true,
  8. ValidIssuer = "ExampleIssuer",
  9. // Validate the JWT Audience (aud) claim
  10. ValidateAudience = true,
  11. ValidAudience = "ExampleAudience",
  12. // Validate the token expiry
  13. ValidateLifetime = true,
  14. // If you want to allow a certain amount of clock drift, set that here:
  15. ClockSkew = TimeSpan.Zero
  16. };
  17. app.UseCookieAuthentication(new CookieAuthenticationOptions
  18. {
  19. AutomaticAuthenticate = true,
  20. AutomaticChallenge = true,
  21. AuthenticationScheme = "Cookie",
  22. CookieName = "access_token",
  23. TicketDataFormat = new CustomJwtDataFormat(
  24. SecurityAlgorithms.HmacSha256,
  25. tokenValidationParameters)
  26. });

如果请求中包含名为access_token的cookie验证为合法的JWT,这个请求就能返回正确的结果,如果需要,你可以加上额外的jwt chaims,或者复制jwt chaims到ClaimsPrincipal在CustomJwtDataFormat.Unprotect方法中,上面是验证token,下面将在asp.net core中生成token。

ASP.NET Core生成Tokens

在asp.net 4.5中,这个UseOAuthAuthorizationServer中间件可以轻松的生成tokens,但是在asp.net core取消了,下面写一个简单的token生成中间件,最后,有几个现成解决方案的链接,供你选择。

简单的token生成节点

首先,生成 POCO保存中间件的选项. 生成类:TokenProviderOptions.cs

  1. using System;
  2. using Microsoft.IdentityModel.Tokens;
  3. namespace SimpleTokenProvider
  4. {
  5. public class TokenProviderOptions
  6. {
  7. public string Path { get; set; } = "/token";
  8. public string Issuer { get; set; }
  9. public string Audience { get; set; }
  10. public TimeSpan Expiration { get; set; } = TimeSpan.FromMinutes(5);
  11. public SigningCredentials SigningCredentials { get; set; }
  12. }
  13. }

现在自己添加一个中间件,asp.net core 的中间件类一般是这样的:

  1. using System.IdentityModel.Tokens.Jwt;
  2. using System.Security.Claims;
  3. using System.Threading.Tasks;
  4. using Microsoft.AspNetCore.Http;
  5. using Microsoft.Extensions.Options;
  6. using Newtonsoft.Json;
  7. namespace SimpleTokenProvider
  8. {
  9. public class TokenProviderMiddleware
  10. {
  11. private readonly RequestDelegate _next;
  12. private readonly TokenProviderOptions _options;
  13. public TokenProviderMiddleware(
  14. RequestDelegate next,
  15. IOptions<TokenProviderOptions> options)
  16. {
  17. _next = next;
  18. _options = options.Value;
  19. }
  20. public Task Invoke(HttpContext context)
  21. {
  22. // If the request path doesn't match, skip
  23. if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal))
  24. {
  25. return _next(context);
  26. }
  27. // Request must be POST with Content-Type: application/x-www-form-urlencoded
  28. if (!context.Request.Method.Equals("POST")
  29. || !context.Request.HasFormContentType)
  30. {
  31. context.Response.StatusCode = 400;
  32. return context.Response.WriteAsync("Bad request.");
  33. }
  34. return GenerateToken(context);
  35. }
  36. }
  37. }

这个中间件类接受TokenProviderOptions作为参数,当有请求且请求路径是设置的路径(token或者api/token),Invoke方法执行,token节点只对 POST请求而且包括form-urlencoded内容类型(Content-Type: application/x-www-form-urlencoded),因此调用之前需要检查下内容类型。

最重要的是GenerateToken,这个方法需要验证用户的身份,生成jwt,传回jwt:

  1. private async Task GenerateToken(HttpContext context)
  2. {
  3. var username = context.Request.Form["username"];
  4. var password = context.Request.Form["password"];
  5. var identity = await GetIdentity(username, password);
  6. if (identity == null)
  7. {
  8. context.Response.StatusCode = 400;
  9. await context.Response.WriteAsync("Invalid username or password.");
  10. return;
  11. }
  12. var now = DateTime.UtcNow;
  13. // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.
  14. // You can add other claims here, if you want:
  15. var claims = new Claim[]
  16. {
  17. new Claim(JwtRegisteredClaimNames.Sub, username),
  18. new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
  19. new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(now).ToString(), ClaimValueTypes.Integer64)
  20. };
  21. // Create the JWT and write it to a string
  22. var jwt = new JwtSecurityToken(
  23. issuer: _options.Issuer,
  24. audience: _options.Audience,
  25. claims: claims,
  26. notBefore: now,
  27. expires: now.Add(_options.Expiration),
  28. signingCredentials: _options.SigningCredentials);
  29. var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
  30. var response = new
  31. {
  32. access_token = encodedJwt,
  33. expires_in = (int)_options.Expiration.TotalSeconds
  34. };
  35. // Serialize and return the response
  36. context.Response.ContentType = "application/json";
  37. await context.Response.WriteAsync(JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented }));
  38. }

大部分代码都很官方,JwtSecurityToken 类生成jwt,JwtSecurityTokenHandler将jwt编码,你可以在claims中添加任何chaims。验证用户身份只是简单的验证,实际情况肯定不是这样的,你可以集成 identity framework或者其他的,对于这个实例只是简单的硬编码:

  1. private Task<ClaimsIdentity> GetIdentity(string username, string password)
  2. {
  3. // DON'T do this in production, obviously!
  4. if (username == "TEST" && password == "TEST123")
  5. {
  6. return Task.FromResult(new ClaimsIdentity(new System.Security.Principal.GenericIdentity(username, "Token"), new Claim[] { }));
  7. }
  8. // Credentials are invalid, or account doesn't exist
  9. return Task.FromResult<ClaimsIdentity>(null);
  10. }

添加一个将DateTime生成timestamp的方法:

  1. public static long ToUnixEpochDate(DateTime date)
  2. => (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);

现在,你可以将这个中间件添加到startup.cs中了:

  1. using System.Text;
  2. using Microsoft.AspNetCore.Builder;
  3. using Microsoft.AspNetCore.Hosting;
  4. using Microsoft.Extensions.Configuration;
  5. using Microsoft.Extensions.DependencyInjection;
  6. using Microsoft.Extensions.Logging;
  7. using Microsoft.Extensions.Options;
  8. using Microsoft.IdentityModel.Tokens;
  9. namespace SimpleTokenProvider
  10. {
  11. public partial class Startup
  12. {
  13. public Startup(IHostingEnvironment env)
  14. {
  15. var builder = new ConfigurationBuilder()
  16. .AddJsonFile("appsettings.json", optional: true);
  17. Configuration = builder.Build();
  18. }
  19. public IConfigurationRoot Configuration { get; set; }
  20. public void ConfigureServices(IServiceCollection services)
  21. {
  22. services.AddMvc();
  23. }
  24. // The secret key every token will be signed with.
  25. // In production, you should store this securely in environment variables
  26. // or a key management tool. Don't hardcode this into your application!
  27. private static readonly string secretKey = "mysupersecret_secretkey!123";
  28. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
  29. {
  30. loggerFactory.AddConsole(LogLevel.Debug);
  31. loggerFactory.AddDebug();
  32. app.UseStaticFiles();
  33. // Add JWT generation endpoint:
  34. var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));
  35. var options = new TokenProviderOptions
  36. {
  37. Audience = "ExampleAudience",
  38. Issuer = "ExampleIssuer",
  39. SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256),
  40. };
  41. app.UseMiddleware<TokenProviderMiddleware>(Options.Create(options));
  42. app.UseMvc();
  43. }
  44. }
  45. }

测试一下,推荐使用chrome 的postman:

  1. POST /token
  2. Content-Type: application/x-www-form-urlencoded
  3. username=TEST&password=TEST123

结果:
OK

Content-Type: application/json
 
{
  "access_token": "eyJhb...",
  "expires_in": 300
}

你可以使用jwt工具查看生成的jwt内容。如果开发的是移动应用或者单页应用,你可以在后续请求的header中存储jwt,如果你需要在cookies中存储的话,你需要对代码修改一下,需要将返回的jwt字符串添加到cookie中。
测试下:

其他方案

下面是比较成熟的项目,可以在实际项目中使用:

  • AspNet.Security.OpenIdConnect.Server – ASP.NET 4.x的验证中间件。
  • OpenIddict – 在identity上添加OpenId验证。
  • IdentityServer4 – .NET Core认证中间件(现在测试版本)。

下面的文章可以让你更加的了解认证:

  • Overview of Token Authentication Features
  • How Token Authentication Works in Stormpath
  • Use JWTs the Right Way!

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

人气教程排行