我回答了这个问题: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