JWT 安全 第二部分,安全 REST 服务





5.00/5 (8投票s)
了解如何创建 JWT 并将其与 WebApi、REST 和 MVC 一起使用,所有这些都使用 .NET Core 构建
引言
这是关于 JWT(JSON Web Token)的三篇博文中的第二篇。在第一篇博文中,我解释了如何创建 JWT。在这篇博文中,我们将保护 REST 服务并讨论以下主题:
- 设置和配置 JWT
- 默认安全
- 设置 Swagger 以支持 JWT
- 带 JWT 的 HTTP 客户端
- 基于声明的访问
- 基于声明的内容
通用 JWT 解决方案概述
在深入探讨细节之前,先回顾一下第一部分。JWT 发行者现在已运行。它根据用户凭据颁发 JWT。
设置和配置 JWT
下一步是保护 REST 服务,首先添加 `Microsoft.AspNetCore.Authentication.JwtBearer` 包。JWT 包需要在 *startup.cs* 中进行配置。首先,我们在“*appsettings.json*”中设置参数。
...
"JwtTokenValidationSettings": {
"ValidIssuer": "JwtServer",
"ValidateIssuer": true,
"SecretKey": "@everone:KeepitSecret!"
},
...
`SecretKey` 值必须与 JWT 发行者服务器中的密钥匹配,否则用户将保持未认证状态,访问将被拒绝。DI(依赖注入)提供对配置的访问。
public void ConfigureServices(IServiceCollection services)
{
...
// setup JWT Token validation
services.Configure<JwtTokenValidationSettings>
(Configuration.GetSection(nameof(JwtTokenValidationSettings)));
services.AddSingleton<IJwtTokenValidationSettings,
JwtTokenValidationSettingsFactory>();
...
`JwtTokenValidationSettingsFactory` 实现接口,并具有 `TokenValidationParameters` 函数。
public class JwtTokenValidationSettings
{
public String ValidIssuer { get; set; }
public Boolean ValidateIssuer { get; set; }
public String ValidAudience { get; set; }
public Boolean ValidateAudience { get; set; }
public String SecretKey { get; set; }
}
public interface IJwtTokenValidationSettings
{
String ValidIssuer { get; }
Boolean ValidateIssuer { get; }
String ValidAudience { get; }
Boolean ValidateAudience { get; }
String SecretKey { get; }
TokenValidationParameters CreateTokenValidationParameters();
}
public class JwtTokenValidationSettingsFactory : IJwtTokenValidationSettings
{
private readonly JwtTokenValidationSettings settings;
public String ValidIssuer => settings.ValidIssuer;
public Boolean ValidateIssuer => settings.ValidateIssuer;
public String ValidAudience => settings.ValidAudience;
public Boolean ValidateAudience => settings.ValidateAudience;
public String SecretKey => settings.SecretKey;
public JwtTokenValidationSettingsFactory
(IOptions<JwtTokenValidationSettings> options)
{
settings = options.Value;
}
public TokenValidationParameters CreateTokenValidationParameters()
{
var result = new TokenValidationParameters
{
ValidateIssuer = ValidateIssuer,
ValidIssuer = ValidIssuer,
ValidateAudience = ValidateAudience,
ValidAudience = ValidAudience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey)),
RequireExpirationTime = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
return result;
}
}
`TokenValidationParameters` 函数(顾名思义)返回 JWT 验证参数。这些参数在 *startup.cs* 中使用。
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
...
// Create TokenValidation factory with DI priciple
var tokenValidationSettings =
app.ApplicationServices.GetService<IJwtTokenValidationSettings>();
// Setup JWT security
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = false, // not sure
TokenValidationParameters =
tokenValidationSettings.CreateTokenValidationParameters()
});
...
就是这样!现在您可以使用 `[Authorize]` 属性来保护控制器或其操作。
[Authorize]
[Route("api/[controller]")]
public class EmployeeController : Controller
{
...
默认安全
使用 MVC 框架,您可以设置自定义过滤器。默认情况下,保护所有控制器只需在启动时设置一个自定义过滤器。
public void ConfigureServices(IServiceCollection services)
{
...
// Secure all controllers by default
var authorizePolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
// Add Mvc with options
services.AddMvc(config =>
{ config.Filters.Add(new AuthorizeFilter(authorizePolicy)); });
...
设置 Swagger 以支持 JWT
Swagger 设置需要对 JWT 进行一些调整。此调整允许您在测试期间添加 JWT。Swagger 将 JWT 添加到请求头中,REST 服务接收令牌并进行安全设置。
public void ConfigureServices(IServiceCollection services)
{
...
// Register the Swagger generator with JWT support
services.AddSwaggerGen(c =>
{
// Tweak for JWT support
c.AddSecurityDefinition("Bearer", new Swashbuckle.AspNetCore.Swagger.ApiKeyScheme()
{
Description = "Authorization format : Bearer {token}",
Name = "Authorization",
In = "header",
Type = "apiKey"
});
c.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info
{ Title = "Resources Api", Version = "v1" });
});
...
其余的 Swagger 设置是正常的。
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
...
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
//c.ShowRequestHeaders();
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Resources Api v1");
});
...
是时候测试了
现在一切都已准备好通过 Swagger 进行测试。使用简单的 `LoginStatus` 函数,您可以轻松查看安全是否有效。
[Route("api/[controller]")]
public class EmployeeController : Controller
{
...
[HttpGet("loginstatus")]
public IActionResult LoginStatus()
{
var isAuthenticated = this.HttpContext.User.Identities.Any(u => u.IsAuthenticated);
var email = this.User.FindFirst(c => c.Type.ContainsEx("email"))?.Value;
var result = new
{
IsAuthenticated = isAuthenticated,
Email = email
};
return Ok(result);
}
我启动了 Swagger 并尝试了 `loginstatus`。
响应代码是 **401**,表示“**未授权**”。我没有添加 JWT,所以这是正确的结果!让我们看看如果我们提供 JWT 给 Swagger 会发生什么。
首先,使用“employee@xyz.com”和“password”作为凭据从 JWT 发行者获取 JWT。
Swagger 中的值是“`Bearer `”,响应代码组合在一起。
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJKd3RTZXJ2ZXIiLCJ zdWIiOiJlbXBsb3llZSIsImVtYWlsIjoiZW1wbG95ZWVAeHl6LmNvbSIsImp0aSI6IjZjNT QzMzU1LWE4YjItNDI1MC05YTczLTdlZDFmMTNhYzIwZSIsImlhdCI6MTUwNDAxNzc1Miwia HR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWlt cy9yb2xlIjoiRW1wbG95ZWUiLCJuYmYiOjE1MDQwMTc3NTIsImV4cCI6MTUwNDAxOTU1Mn0. 0HYDe78_HZ1qVV_uIg5FMqVJujzQC3Pk5dHUX1YvwC0
现在已将 JWT 添加到 Swagger,我们再次尝试。
响应代码为 200(OK),响应体显示身份验证已设置,控制器可以读取声明。太棒了!
带 JWT 的 HTTP 客户端
我添加了一个小型控制台应用程序,它清楚地演示了 JWT 和 HTTP 客户端如何协同工作。
第一步是从发行者检索 JWT。
private static async Task<String> Login(String email, String password) { var url = "https://:49842/"; var apiUrl = "/api/security/login/"; using (var client = new HttpClient() { BaseAddress = new Uri(url) }) { client.DefaultRequestHeaders.Accept.Add (new MediaTypeWithQualityHeaderValue("application/json")); var loginResource = new { Email = email, Password = password }; var resourceDocument = JsonConvert.SerializeObject(loginResource); using (var content = new StringContent (resourceDocument, Encoding.UTF8, "application/json")) { using (var response = await client.PostAsync(apiUrl, content)) { if (response.StatusCode == System.Net.HttpStatusCode.OK) { var result = await response.Content.ReadAsStringAsync(); return result; } else return null; } } } }
使用 JWT,我们可以调用 REST 服务。
private static async Task<String> GetEmployees(String jwt)
{
var url = "https://:50249/";
var apiUrl = $"/api/employee/";
using (var client = new HttpClient() { BaseAddress = new Uri(url) })
{
client.BaseAddress = new Uri(url);
client.DefaultRequestHeaders.Accept.Add
(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {jwt}");
using (var response = await client.GetAsync(apiUrl))
{
if (response.StatusCode == System.Net.HttpStatusCode.OK)
return await response.Content.ReadAsStringAsync();
else return null;
}
}
}
请注意 `login` 和 `GetEmployees` 的不同 URL,并仔细查看第 10 行。JWT 以与 Swagger 相同的方式添加到请求头中,“`Bearer <token>`”。
运行控制台应用程序
static void Main(String[] args)
{
Console.WriteLine("Request Token");
var jwt = Login("employee@xyz.com", "password").Result;
Console.WriteLine($"Token : {jwt}");
Console.WriteLine("");
var document = GetEmployees(jwt).Result;
Console.WriteLine($"Employees: {document}");
Console.WriteLine("");
Console.WriteLine($"{Environment.NewLine}Ready, press key to close");
Console.ReadKey();
}
结果是
CBAC(基于声明的访问)
安全模型支持 CBAC,这与更传统的 RBAC(基于角色的访问)不同。我不会开始讨论 RBAC 或 CBAC 哪个更好。我认为基于声明的模型更通用,更适合与不同系统集成。对于 RBAC 的忠实拥护者,有好消息,您可以完美地将基于角色的系统集成到基于声明的环境中,并且还添加了基于角色的支持以实现向后兼容。
策略要求声明
安全模型引入了策略的概念。策略由一个或多个声明组成,并在启动时注册。
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthorization(options =>
{
options.AddPolicy("HR Only", policy => policy.RequireRole("HR-Worker"));
options.AddPolicy("HR-Manager Only",
policy => policy.RequireClaim("CeoApproval", "true"));
});
...
策略可以由角色或声明满足。复杂的组合也是可能的,但在此演示中不使用它们。您可以通过角色或策略进行授权。
[HttpGet("RoleBasedDemo")]
[Authorize(Roles = "HR-Worker")]
public IActionResult RoleBasedDemo()
{
return Ok("I am role based");
}
[HttpDelete("{id}")]
[Authorize(Policy = "HR-Manager Only")]
public async Task<IActionResult> Delete(Guid id)
{
...
}
但是,您不能将声明用作授权属性,必须使用带有声明的策略。
基于声明的内容
有时,资源的内容取决于客户端。例如,任何人都可以查看员工,但只有人力资源部门可以查看工资。您可以通过检查用户声明或角色(如果您愿意)来实现此功能。
private String RemoveSensitiveFields(EmployeeResource resource)
{
// The dynamic Linq Select(params) works only on an IQueryable list
// that's why 1 one item is added to a list
var items = new List<EmployeeResource>(new[] { resource }).AsQueryable();
// Find all property names
var propertyNames = resource.GetType().GetProperties
(BindingFlags.Public | BindingFlags.Instance).Select(p => p.Name).ToList();
// Salary only visible to HR department
if (!User.HasClaim("Department", "HR"))
propertyNames.Remove(nameof(EmployeeResource.Salary));
// Dynamic Linq supports dynamic selector
var selector = $"new({String.Join(",", propertyNames)})";
// Create dynamic object with authorized fields
var reducedResource = items.Select(selector).First();
// Create JSON
var result = JsonConvert.SerializeObject
(reducedResource, new JsonSerializerSettings()
{ Formatting = Formatting.Indented });
return result;
}
`RemoveSensitiveFields` 中的关键元素位于第 11 和 18 行。该函数依赖于“`System.Linq.Dynamic.Core`”包。该包为 `IQueryable` 项目提供了 `Select(params)` 扩展方法。属性名称通过反射提取,如果客户端不是 HR 部门的成员,则会删除“`Salary”字段。
控制台应用程序演示了基于声明的内容。
private static void ClaimBasedContentDemo()
{
// each token represents a different identity
var tokens = new String[]
{
Login("hrworker@xyz.com", "password").Result,
Login("employee@xyz.com", "password").Result
};
foreach (var token in tokens)
{
Console.WriteLine(GetLoginStatus(token).Result);
Console.WriteLine(GetEmployee(token, "jadds4z@1688.com").Result);
Console.WriteLine("");
}
}
输出
正如屏幕截图所示,只有 HR 成员才能查看工资。
结论
JWT(JSON Web Tokens)是保护 REST 服务的可靠方式。JWT 易于设置,并且与 .NET Core 集成良好。由于其自包含的设计,JWT 保持 REST 服务无状态,无需身份验证服务器。JWT 同时支持传统的基于角色的方法和更现代的基于声明的方法。
Visual Studio 启动项目
有时,Visual Studio 的启动项目会丢失,导致应用程序无法运行。右键单击解决方案并选择“**设置启动项目...**”。
并修复启动设置
根据您的需要选择 `WebApp` 或 `Jwt.ConsoleDemo`。
延伸阅读
版本
- 1.0 2017 年 8 月 31 日:初始发布
- 1.1 2017 年 9 月 5 日:源代码已升级为 .NET Core 2.0