ASP.NET Web API 的 JWT 身份验证
Posted
技术标签:
【中文标题】ASP.NET Web API 的 JWT 身份验证【英文标题】:JWT authentication for ASP.NET Web API 【发布时间】:2017-03-09 22:16:03 【问题描述】:我正在尝试在我的 Web API 应用程序中支持 JWT 不记名令牌(JSON Web 令牌),但我迷路了。
我看到了对 .NET Core 和 OWIN 应用程序的支持。 我目前在 IIS 中托管我的应用程序。
如何在我的应用程序中实现此身份验证模块?有什么方法可以像使用表单/Windows 身份验证一样使用<authentication>
配置吗?
【问题讨论】:
【参考方案1】:我回答了这个问题:How to secure an ASP.NET Web API 4 年前使用 HMAC。
现在,安全方面发生了很多变化,尤其是 JWT 越来越受欢迎。在这个答案中,我将尝试以最简单和基本的方式解释如何使用 JWT,这样我们就不会迷失在 OWIN、Oauth2、ASP.NET 身份的丛林中...... :)
如果你不了解 JWT 令牌,你需要看看:
https://www.rfc-editor.org/rfc/rfc7519
基本上,JWT 令牌如下所示:
<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>
例子:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ P>
一个 JWT 令牌包含三个部分:
-
标头:采用 Base64 编码的 JSON 格式
声明:以 Base64 编码的 JSON 格式。
签名:基于以 Base64 编码的 Header 和 Claims 创建和签名。
如果您使用带有上述令牌的网站jwt.io,您可以将令牌解码并看到如下:
从技术上讲,JWT 使用从标头和声明中签名的签名,并使用标头中指定的安全算法(例如:HMACSHA256)。因此,如果您在其声明中存储任何敏感信息,则必须通过 HTTPs 传输 JWT。
现在,为了使用 JWT 身份验证,如果您拥有旧版 Web Api 系统,则实际上并不需要 OWIN 中间件。简单的概念是如何提供 JWT 令牌以及如何在请求到来时验证令牌。就是这样。
在demo I've created (github)中,为了保持JWT令牌的轻量级,我只存储username
和expiration time
。但是这样,你必须重新构建新的本地身份(主体)来添加更多信息,比如角色,如果你想做角色授权等。但是,如果你想在 JWT 中添加更多信息,这取决于你:它非常灵活。
您可以使用控制器操作简单地提供 JWT 令牌端点,而不是使用 OWIN 中间件:
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 请求或基本身份验证端点来提供 JWT 令牌。
如何根据username
生成token?
您可以使用来自 Microsoft 的名为 System.IdentityModel.Tokens.Jwt
的 NuGet 包来生成令牌,如果您愿意,甚至可以使用其他包。在演示中,我使用HMACSHA256
和SymmetricKey
:
/// <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?
在demo,我建了
JwtAuthenticationAttribute
继承自 IAuthenticationFilter
(有关身份验证过滤器的更多详细信息,请参见 here)。
使用此属性,您可以验证任何操作:您只需将该属性放在该操作上。
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
。您可以执行更多验证,例如检查系统上是否存在用户,并根据需要添加其他自定义验证。
验证 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 来测试demo:
请求令牌(如我上面提到的幼稚,仅用于演示):
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
【讨论】:
@Cuong Le 解释得很好,但我想补充更多:如果您使用 OWIN,请检查 Microsoft.Owin.Security.Jwt 中提供的 UseJwtBearerAuthentication,您可以在 WebAPI 上使用这个 owin 中间件来验证每个自动传入请求。使用owin启动类注册中间件 @AmirPopovich 您不需要在响应上设置令牌,令牌需要存储在客户端的其他地方,对于 Web,您可以放在本地存储中,每当您发送 HTTP 请求时,将令牌放在标题上。 哇,这是我很久以来看到的最简单的解释了。如果可以的话 +100 @Homam:很抱歉这个迟到的答案,最好的生成方法是:varhmac = new HMACSHA256();var key = Convert.ToBase64String(hmac.Key);
任何使用来自 CuongLe 的 repo 的演示代码的人都会注意到存在一个错误,即没有授权标头的请求不会被处理,这意味着任何没有授权标头的查询都可以通过(不是那么安全的端点!)。 @magicleon 有一个拉取请求来解决这个问题:github.com/cuongle/WebApi.Jwt/pull/4【参考方案2】:
我已经设法以最小的努力实现了它(就像使用 ASP.NET Core 一样简单)。
为此,我使用 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 Startup NSWag
库后停止为我工作(例如,你们中的一些人可能想要为 Angular 应用程序自动生成 typescript HTTP 代理)。
解决方案也很简单 - 我将 NSWag
替换为 Swashbuckle
并且没有任何其他问题。
好的,现在分享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;
另一个重要方面 - 我通过 Authorization 标头发送 JWT 令牌,因此打字稿代码如下所示:
(以下代码由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')
【讨论】:
I replaced NSWag with Swashbuckle and didn't have any further issues.
Swashbuckle 有能力生成打字稿文件还是你自己添加的?
@crush swashbucle 是一个提供 json 的后端库,就像 nuget nswag 库一样更好。为了生成打字稿文件,你仍然应该使用 npm 的 nswag 包。
对,我的项目中已经有 swashbuckle 有一段时间了,听起来你在暗示它可以生成 TypeScript 模型而不是 nswag。我不是 nswag 的粉丝……它很重。我已经创建了自己的 C#->TypeScript 转换,该转换与 Swashbuckle 挂钩 - 生成文件作为构建后过程,并将它们发布到我们项目的 npm 提要。我只是想确保我没有忽略一个已经在做同样事情的 Swashbuckle 项目。【参考方案3】:
这是在 ASP.NET Core Web API 中使用 JWT 令牌的基于声明的身份验证的一个非常简单且安全的实现。
首先,您需要公开一个端点,该端点返回一个 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
希望对你有帮助
【讨论】:
【参考方案4】:在我的例子中,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;
【讨论】:
【参考方案5】:我认为你应该使用一些 3d 方服务器来支持 JWT 令牌,并且在 WEB API 2 中没有开箱即用的 JWT 支持。
但是,有一个 OWIN 项目支持某种格式的签名令牌(不是 JWT)。它作为简化的 OAuth 协议工作,仅为网站提供一种简单的身份验证形式。
您可以阅读有关它的更多信息,例如here.
它相当长,但大多数部分都是您可能根本不需要的控制器和 ASP.NET 标识的详细信息。最重要的是
第 9 步:添加对 OAuth 不记名令牌生成的支持
第 12 步:测试后端 API
在那里您可以阅读如何设置可以从前端访问的端点(例如“/token”)(以及有关请求格式的详细信息)。
其他步骤提供了有关如何将该端点连接到数据库等的详细信息,您可以选择所需的部分。
【讨论】:
【参考方案6】:您可以按照此代码获取令牌控制器,或访问此处了解更多详细信息:How to Secure API using JWT Tokens. Building CRUD API using JWT Tokens with ASP.NET Core and Entity Framework Core and Swagger
从这里你可以非常简单地学习使用 JWT Token
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);
【讨论】:
【参考方案7】:您无需使用奇怪的 JwtSecurityTokenHandler API
使用 JwtUtils 带有简单 API 的 Nuget 包
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");
【讨论】:
以上是关于ASP.NET Web API 的 JWT 身份验证的主要内容,如果未能解决你的问题,请参考以下文章
如何在使用 JWT 的 asp.net 核心 web 应用和 web api 中使用谷歌身份验证
将 JWT 身份验证实现从 .net core 2 转移到 asp.net web api 2
具有混合身份验证 JWT 和 SAML 的 ASP.NET Web API 2.2 OWIN
text 使用ASP.NET Core 2 Web API,Angular 5,.NET核心身份和Facebook登录进行JWT身份验证