ASP的JWT身份验证。NET Web API

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

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

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

389700 次浏览

我认为你应该使用一些3d政党服务器来支持JWT令牌,在WEB API 2中没有开箱即用的JWT支持。

然而,有一个OWIN项目支持某种格式的签名令牌(不是JWT)。它是一种简化的OAuth协议,为网站提供一种简单的身份验证形式。

你可以阅读更多关于它的信息,例如在这里

它相当长,但大部分是关于控制器和ASP的细节。NET标识,您可能根本不需要。最重要的是

步骤9:添加对OAuth持名令牌生成的支持

步骤12:测试后端API

在那里你可以读到如何设置端点(例如"/token"),你可以从前端访问(和请求格式的细节)。

其他步骤提供了有关如何将该端点连接到数据库等的详细信息,您可以选择所需的部分。

我回答了这个问题:如何确保ASP。NET Web API 4年前使用HMAC。

现在,安全方面发生了很多变化,尤其是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. 声明:JSON格式,以Base64编码。
  3. 签名:基于头和声明创建和签名,用Base64编码。

如果你使用网站jwt.io和上面的令牌,你可以解码令牌,如下所示:

 jwt截图。“/></a> . io”的原始jwt源代码和解码后的JSON</p>
<p>从技术上讲,JWT使用的签名来自报头和声明,并使用报头中指定的安全算法(例如:HMACSHA256)。因此,如果您在其声明中存储任何敏感信息,JWT必须通过HTTPs传输。</p>
<p>现在,为了使用JWT身份验证,如果您有一个遗留的Web Api系统,那么实际上不需要OWIN中间件。简单的概念是如何提供JWT令牌,以及如何在请求到来时验证令牌。就是这样。</p>
<p>在<a href=演示我已经创建(github)中,为了保持JWT令牌的轻量级性,我只存储了usernameexpiration time。但是通过这种方式,您必须重新构建新的本地身份(主体)以添加更多信息,如角色,如果您想进行角色授权等。但是,如果您想在JWT中添加更多信息,这取决于您:它非常灵活。

而不是使用OWIN中间件,你可以通过使用控制器动作简单地提供一个JWT令牌端点:

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请求或Basic Authentication端点来提供JWT令牌。

如何基于username生成令牌?

你可以使用来自微软的名为System.IdentityModel.Tokens.Jwt的NuGet包来生成令牌,如果你喜欢,甚至可以使用另一个包。在演示中,我使用HMACSHA256SymmetricKey:

/// <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。您可以执行更多的验证,比如检查user是否存在于您的系统中,如果需要,还可以添加其他自定义验证。

验证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

我用最少的努力就做到了这一点(就像用ASP一样简单)。网络核心)。

为此,我使用OWIN Startup.cs文件和Microsoft.Owin.Security.Jwt库。

为了让应用程序命中Startup.cs,我们需要修改Web.config:

<configuration>
<appSettings>
<add key="owin:AutomaticAppStartup" value="true" />
...

下面是Startup.cs的样子:

using MyApp.Helpers;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Jwt;
using Owin;


[assembly: OwinStartup(typeof(MyApp.App_Start.Startup))]


namespace MyApp.App_Start
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters()
{
ValidAudience = ConfigHelper.GetAudience(),
ValidIssuer = ConfigHelper.GetIssuer(),
IssuerSigningKey = ConfigHelper.GetSymmetricSecurityKey(),
ValidateLifetime = true,
ValidateIssuerSigningKey = true
}
});
}
}
}

你们很多人都使用ASP。NET Core,所以正如你所看到的,它和我们那里的并没有太大的不同。

一开始它真的让我很困惑,我试图实现自定义提供者等等。但我没想到事情会这么简单。OWIN真棒!

只有一件事要提——在我启用OWIN启动后,NSWag库不再为我工作了(例如,你们中的一些人可能想为Angular应用自动生成typescript HTTP代理)。

解决方案也很简单——我用Swashbuckle替换了NSWag,没有任何进一步的问题。


好,现在共享ConfigHelper代码:

public class ConfigHelper
{
public static string GetIssuer()
{
string result = System.Configuration.ConfigurationManager.AppSettings["Issuer"];
return result;
}


public static string GetAudience()
{
string result = System.Configuration.ConfigurationManager.AppSettings["Audience"];
return result;
}


public static SigningCredentials GetSigningCredentials()
{
var result = new SigningCredentials(GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256);
return result;
}


public static string GetSecurityKey()
{
string result = System.Configuration.ConfigurationManager.AppSettings["SecurityKey"];
return result;
}


public static byte[] GetSymmetricSecurityKeyAsBytes()
{
var issuerSigningKey = GetSecurityKey();
byte[] data = Encoding.UTF8.GetBytes(issuerSigningKey);
return data;
}


public static SymmetricSecurityKey GetSymmetricSecurityKey()
{
byte[] data = GetSymmetricSecurityKeyAsBytes();
var result = new SymmetricSecurityKey(data);
return result;
}


public static string GetCorsOrigins()
{
string result = System.Configuration.ConfigurationManager.AppSettings["CorsOrigins"];
return result;
}
}

另一个重要的方面-我通过授权报头发送了JWT令牌,所以typescript代码如下所示:

(下面的代码是由NSWag生成的)

@Injectable()
export class TeamsServiceProxy {
private http: HttpClient;
private baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;


constructor(@Inject(HttpClient) http: HttpClient, @Optional() @Inject(API_BASE_URL) baseUrl?: string) {
this.http = http;
this.baseUrl = baseUrl ? baseUrl : "https://localhost:44384";
}


add(input: TeamDto | null): Observable<boolean> {
let url_ = this.baseUrl + "/api/Teams/Add";
url_ = url_.replace(/[?&]$/, "");


const content_ = JSON.stringify(input);


let options_ : any = {
body: content_,
observe: "response",
responseType: "blob",
headers: new HttpHeaders({
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": "Bearer " + localStorage.getItem('token')
})
};

参见头部分- "Authorization": "Bearer " + localStorage.getItem('token')

下面是在ASP中使用JWT令牌实现基于声明的身份验证的一个非常简单和安全的实现。NET核心Web API。

首先,你需要公开一个端点,该端点返回一个JWT令牌,并将声明分配给用户:

 /// <summary>
/// Login provides API to verify user and returns authentication token.
/// API Path:  api/account/login
/// </summary>
/// <param name="paramUser">Username and Password</param>
/// <returns>{Token: [Token] }</returns>
[HttpPost("login")]
[AllowAnonymous]
public async Task<IActionResult> Login([FromBody] UserRequestVM paramUser, CancellationToken ct)
{


var result = await UserApplication.PasswordSignInAsync(paramUser.Email, paramUser.Password, false, lockoutOnFailure: false);


if (result.Succeeded)
{
UserRequestVM request = new UserRequestVM();
request.Email = paramUser.Email;




ApplicationUser UserDetails = await this.GetUserByEmail(request);
List<ApplicationClaim> UserClaims = await this.ClaimApplication.GetListByUser(UserDetails);


var Claims = new ClaimsIdentity(new Claim[]
{
new Claim(JwtRegisteredClaimNames.Sub, paramUser.Email.ToString()),
new Claim(UserId, UserDetails.UserId.ToString())
});




//Adding UserClaims to JWT claims
foreach (var item in UserClaims)
{
Claims.AddClaim(new Claim(item.ClaimCode, string.Empty));
}


var tokenHandler = new JwtSecurityTokenHandler();
// this information will be retrived from you Configuration
//I have injected Configuration provider service into my controller
var encryptionkey = Configuration["Jwt:Encryptionkey"];
var key = Encoding.ASCII.GetBytes(encryptionkey);
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = Configuration["Jwt:Issuer"],
Subject = Claims,


// this information will be retrived from you Configuration
//I have injected Configuration provider service into my controller
Expires = DateTime.UtcNow.AddMinutes(Convert.ToDouble(Configuration["Jwt:ExpiryTimeInMinutes"])),


//algorithm to sign the token
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)


};


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


return Ok(new
{
token = tokenString
});
}


return BadRequest("Wrong Username or password");
}

现在你需要在startup.cs中的ConfigureServices中为你的服务添加身份验证,以添加JWT身份验证作为你的默认身份验证服务,如下所示:

services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
//ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Encryptionkey"])),
ValidateAudience = false,
ValidateLifetime = true,
ValidIssuer = configuration["Jwt:Issuer"],
//ValidAudience = Configuration["Jwt:Audience"],
//IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Key"])),
};
});

现在你可以像这样向你的授权服务添加策略:

services.AddAuthorization(options =>
{
options.AddPolicy("YourPolicyNameHere",
policy => policy.RequireClaim("YourClaimNameHere"));
});

另外,你也可以(没有必要)从你的数据库中填充你的所有声明,因为这只会在你的应用程序启动时运行一次,并像这样将它们添加到策略中:

  services.AddAuthorization(async options =>
{
var ClaimList = await claimApplication.GetList(applicationClaim);
foreach (var item in ClaimList)
{
options.AddPolicy(item.ClaimCode, policy => policy.RequireClaim(item.ClaimCode));
}
});

现在你可以把策略过滤器放在任何你想要授权的方法上,就像这样:

 [HttpPost("update")]
[Authorize(Policy = "ACC_UP")]
public async Task<IActionResult> Update([FromBody] UserRequestVM requestVm, CancellationToken ct)
{
//your logic goes here
}

希望这能有所帮助

在我的例子中,JWT是由一个单独的API创建的,所以ASP。NET只需要解码和验证它。与公认的答案相反,我们使用的是RSA,这是一个非对称算法,所以上面提到的SymmetricSecurityKey类将不起作用。

这是结果。

using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Threading;
using System.Threading.Tasks;


public static async Task<JwtSecurityToken> VerifyAndDecodeJwt(string accessToken)
{
try
{
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{securityApiOrigin}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
var openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);
var validationParameters = new TokenValidationParameters()
{
ValidateLifetime = true,
ValidateAudience = false,
ValidateIssuer = false,
RequireSignedTokens = true,
IssuerSigningKeys = openIdConfig.SigningKeys,
};
new JwtSecurityTokenHandler().ValidateToken(accessToken, validationParameters, out var validToken);
// threw on invalid, so...
return validToken as JwtSecurityToken;
}
catch (Exception ex)
{
logger.Info(ex.Message);
return null;
}
}

你可以遵循令牌控制器的代码,更多细节你可以访问这里:如何使用JWT令牌保护API。使用JWT令牌和ASP构建CRUD API。NET Core和实体框架Core和Swagger

从这里,您可以学习以非常简单的方式使用JWT令牌

using JWTToken.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;


namespace JWTToken.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TokenController : ControllerBase
{
public IConfiguration _configuration;
private readonly InventoryContext _context;


public TokenController(IConfiguration config, InventoryContext context)
{
_configuration = config;
_context = context;
}


[HttpPost]
public async Task<IActionResult> Post(UserInfo _userData)
{


if (_userData != null && _userData.Email != null && _userData.Password != null)
{
var user = await GetUser(_userData.Email, _userData.Password);


if (user != null)
{
//create claims details based on the user information
var claims = new[] {
new Claim(JwtRegisteredClaimNames.Sub, _configuration["Jwt:Subject"]),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString()),
new Claim("Id", user.UserId.ToString()),
new Claim("FirstName", user.FirstName),
new Claim("LastName", user.LastName),
new Claim("UserName", user.UserName),
new Claim("Email", user.Email)
};


var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));


var signIn = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);


var token = new JwtSecurityToken(_configuration["Jwt:Issuer"], _configuration["Jwt:Audience"], claims, expires: DateTime.UtcNow.AddDays(1), signingCredentials: signIn);


return Ok(new JwtSecurityTokenHandler().WriteToken(token));
}
else
{
return BadRequest("Invalid credentials");
}
}
else
{
return BadRequest();
}
}


private async Task<UserInfo> GetUser(string email, string password)
{
return await _context.UserInfos.FirstOrDefaultAsync(u => u.Email == email && u.Password == password);
}
}
}

你不需要使用奇怪的JwtSecurityTokenHandler API

使用JwtUtils Nuget包与简单的API

var claims =  new Dictionary<string, object>
{
{ "exp", 1639942616 },
{ "uname", "i.a.ivanov" },
{ "claim1", "claim1_value" },
{ "claims_array", new [] {"claim_item1", "claim_item2"}}
};
       

string token = JWT.HS256.Create(claims, "{TOKEN_SECRET}");