构建安全且实时的应用与服务
1. 从ASP.NET Core应用到云身份验证
在开发ASP.NET Core Web应用时,我们可以将空的应用框架连接到支持云的第三方身份提供者。这样做能让应用摆脱手动管理身份验证的负担,还能利用承载令牌和OIDC标准。虽然通常不建议使用框架和模板,因为它们可能会导致代码臃肿,但在这个例子中,模板包含了我们本来就需要创建的样式表和布局,所以还算不错。
不过,依赖特定于操作系统的安全功能在云环境中会引发诸多问题。例如,在非Windows系统上启动应用时,可能会看到如下警告信息:
warn:
Microsoft.Extensions.DependencyInjection.DataProtectionServices[59]
Neither user profile nor HKLM registry available.
Using an ephemeral key repository.
Protected data will be unavailable when application exits.
warn:
Microsoft.AspNetCore.DataProtection.Repositories.EphemeralXmlRepository
[50]
Using an in-memory repository.
Keys will not be persisted to storage.
问题的核心在于加密密钥和数据保护的使用。在传统的大型Windows服务器上运行.NET应用时,我们可以依赖操作系统来管理加密密钥。但在云环境中,如果应用有多个实例,比如在云端运行20个实例,未认证的用户访问实例1,被重定向到身份提供者,返回时访问实例2,若OIDC流程中的信息在实例1被加密,而实例2无法解密,就会导致应用在运行时出现严重故障。
解决办法是将安全密钥的存储和检索作为后端服务。可以使用像Vault这样的第三方产品,也可以使用Redis等分布式缓存来存储短期密钥。以Steeltoe.Security.DataProtection.Redis这个NuGet模块为例,它能将数据保护API的存储从本地磁盘(非云原生方式)转移到外部的Redis分布式缓存中。
在
Startup
类的
ConfigureServices
方法中配置外部数据保护的代码如下:
services.AddMvc();
services.AddRedisConnectionMultiplexer(Configuration);
services.AddDataProtection()
.PersistKeysToRedis()
.SetApplicationName("myapp-redis-keystore");
services.AddDistributedRedisCache(Configuration);
services.AddSession();
然后在
Configure
方法中调用
app.UseSession()
来完成外部会话状态的设置。
2. 保护ASP.NET Core微服务
对于无UI的微服务(通常称为“无头”服务),那些需要与用户直接交互或通过浏览器进行重定向的身份验证流程就不适用了。下面介绍几种保护微服务的方法:
2.1 使用完整OIDC安全流程保护服务
一种常见的方法是实现专门为服务设计的OIDC身份验证流程。用户通过浏览器重定向与网站和身份提供者交互完成身份验证,网站建立有效的声明身份后,会向身份提供者请求访问令牌,同时提供身份令牌和所需资源的信息。
这个流程本质上是网站询问身份提供者:“用户X能否访问资源Y?如果可以,请给我一个确认此信息的令牌。”服务会验证返回的令牌,如果网站提供的访问令牌未授予用户对相关资源的操作权限,服务将以401 Unauthorized或403 Forbidden拒绝HTTP调用。
但这种方法也有缺点,每个访问令牌都需要验证,很多情况下还需将令牌直接发送给身份提供者进行验证,这使得身份提供者成为系统中每个事务的关键部分,增加了风险和单点故障的可能性。
2.2 使用客户端凭证保护服务
客户端凭证模式是保护服务的简单方法之一。首先,通过SSL与服务通信;其次,使用服务的代码负责传输凭证,这些凭证通常是用户名和密码,或者更适合无人工交互场景的客户端密钥和客户端密钥。在云环境中,很多公共API都要求提供客户端密钥和密钥,这就是客户端凭证模式的应用。
客户端密钥和密钥通常以自定义HTTP头的形式传输,以
X-
为前缀,如
X-MyApp-ClientSecret
和
X-MyApp-ClientKey
。不过,这种方法也有缺点,比如无法有效应对客户端滥用系统、拒绝服务攻击或凭证泄露等问题。
2.3 使用承载令牌保护服务
通过对OpenID Connect的探索,我们知道传输可移植、可独立验证的令牌是其身份验证流程的关键技术。符合JSON Web Token规范的承载令牌可以独立于OIDC使用,无需浏览器重定向或假设有人工用户参与。
之前使用的OIDC中间件基于
Microsoft.AspNetCore.Authentication.JwtBearer
NuGet包中的JWT中间件。使用该中间件保护服务的步骤如下:
1. 参考之前的示例创建一个空服务。
2. 添加对JWT承载身份验证NuGet包的引用。
3. 在服务的
Startup
类的
Configure
方法中启用和配置JWT承载身份验证:
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
ValidateIssuer = false,
ValidIssuer = "https://fake.issuer.com",
ValidateAudience = false,
ValidAudience = "https://sampleservice.example.com",
ValidateLifetime = true,
}
});
在接收承载令牌时,我们可以控制多种验证类型,如发行者签名密钥、发行者、受众和令牌生命周期。例如,验证令牌生命周期时,通常需要设置一些选项来适应令牌发行者和受保护服务之间的时钟偏差。
为了确保承载令牌由已知和受信任的发行者颁发,需要验证发行者签名密钥。创建签名密钥的代码如下:
string SecretKey = "seriouslyneverleavethissittinginyourcode";
SymmetricSecurityKey signingKey =
new SymmetricSecurityKey(
Encoding.ASCII.GetBytes(SecretKey));
注意,不要将密钥直接存储在代码中,应从环境变量或其他外部产品(如Vault或Redis)获取。
使用密钥轮换技术可以降低安全风险。即使攻击者获取了密钥,由于密钥会定期更换,他们也只能在短时间内伪造令牌。
使用承载令牌保护服务时,只需在需要保护的控制器方法上添加
[Authorize]
属性,JWT验证中间件就会对这些方法进行验证。未标记的方法默认允许未经身份验证的访问(也可更改此行为)。
下面是一个简单的控制台应用示例,用于消费受保护的服务:
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, "AppUser_Bob"),
new Claim(JwtRegisteredClaimNames.Jti,
Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat,
ToUnixEpochDate(DateTime.Now).ToString(),
ClaimValueTypes.Integer64),
};
var jwt = new JwtSecurityToken(
issuer: "issuer",
audience: "audience",
claims: claims,
notBefore: DateTime.UtcNow,
expires: DateTime.UtcNow.Add(TimeSpan.FromMinutes(20)),
signingCredentials: creds);
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", encodedJwt);
var result =
httpClient.GetAsync("http://localhost:5000/api/secured").Result;
Console.WriteLine(result.StatusCode);
Console.WriteLine(result.Content.ToString());
服务中一个受保护的控制器方法,用于枚举客户端发送的声明:
[Authorize]
[HttpGet]
public string Get()
{
foreach (var claim in HttpContext.User.Claims) {
Console.WriteLine($"{claim.Type}:{claim.Value}");
}
return "This is from the super secret area";
}
如果想更精细地控制哪些客户端可以调用哪些控制器方法,可以使用策略。策略是在确定授权时作为谓词执行的自定义代码。例如,定义一个名为
CheeseburgerPolicy
的策略,并创建一个受保护的控制器方法,要求令牌满足该策略的条件:
[Authorize( Policy = "CheeseburgerPolicy")]
[HttpGet("policy")]
public string GetWithPolicy()
{
return "This is from the super secret area w/policy enforcement.";
}
在
ConfigureServices
方法中配置策略的示例如下:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddOptions();
services.AddAuthorization( options => {
options.AddPolicy("CheeseburgerPolicy",
policy =>
policy.RequireClaim("icanhazcheeseburger", "true"));
});
}
修改控制台应用,添加符合策略要求的声明,就可以调用受策略保护的控制器方法。
下面是保护微服务的方法对比表格:
| 保护方法 | 优点 | 缺点 |
| ---- | ---- | ---- |
| 完整OIDC安全流程 | 可利用OIDC标准,身份验证流程完善 | 每个令牌都需验证,增加身份提供者负担和单点故障风险 |
| 客户端凭证模式 | 实现简单 | 难以应对客户端滥用、攻击和凭证泄露问题 |
| 承载令牌 | 可独立验证,适合无浏览器交互场景 | 需要管理密钥安全 |
保护微服务的流程如下:
graph LR
A[选择保护方法] --> B{完整OIDC安全流程}
A --> C{客户端凭证模式}
A --> D{承载令牌}
B --> E[实现OIDC身份验证流程]
C --> F[通过SSL通信并传输凭证]
D --> G[配置JWT中间件和策略]
3. 实时应用的定义
在探讨实时服务之前,需要先明确“实时”的定义。“实时”这个术语和“微服务”一样,被过度使用且含义多样。Definithing.com将其定义为计算机系统以接收数据的相同速率更新信息。也有定义认为,能在几毫秒内处理输入并产生输出的就是实时系统,但对于一些超低延迟要求的系统,可能认为几百微秒的处理时间才算实时。
例如,之前创建的事件处理器能在几毫秒内处理输入(成员位置事件)、检测接近情况并发出接近检测事件,按照上述定义,可以将其视为实时系统。更宽泛地说,实时可以定义为事件在接收和处理之间几乎没有延迟,“几乎没有”的定义需要开发团队根据系统需求和应用领域来确定,而不是随意设定。
常见的实时应用示例有导弹制导系统、自动驾驶汽车、无人机自动驾驶软件和实时视频流模式识别软件等。但像航空公司订票系统,很多时候并不符合实时应用的特点,因为订票后可能要24小时才能收到登机牌,航班延误或登机口变更的通知也可能在信息变得无关紧要后才发出。
不符合实时应用的特征如下:
- 应用收集输入后等待一段时间再输出。
- 应用仅按时间间隔或在外部刺激下输出,且刺激时间有规律或随机。
实时系统的一个常见特征是,相关方通过推送通知得知与自己有关的事件,而不是主动轮询或定时查询更新。
4. 云端的WebSockets
在开发过程中,我们经常使用像RabbitMQ这样的消息服务器通过消息队列进行消息传递。当开发者考虑实时应用时,往往会想到使用WebSockets将数据和通知推送到实时的基于Web的用户界面。
几年前,能动态更新和响应的网站还被视为“未来趋势”,如今我们已习以为常。比如在购物网站上与客服进行实时聊天已不再新奇。但传统的WebSockets编程模型在云端存在不足,后续会通过构建一个实时应用示例,展示在事件源系统中添加实时消息传递的强大功能。
构建安全且实时的应用与服务(续)
5. 构建实时应用示例
为了展示在事件源系统中添加实时消息传递的强大功能,我们来构建一个简单的实时应用示例。假设我们有一个股票管理系统,当股票价格发生变化时,需要实时通知客户端。
首先,我们需要创建一个 WebSocket 服务。以下是一个简单的 C# 示例代码:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.WebSockets;
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
public class Startup
{
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseWebSockets();
app.Run(async context =>
{
if (context.WebSockets.IsWebSocketRequest)
{
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
await Echo(context, webSocket);
}
else
{
context.Response.StatusCode = 400;
}
});
}
private async Task Echo(Microsoft.AspNetCore.Http.HttpContext context, WebSocket webSocket)
{
var buffer = new byte[1024 * 4];
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
while (!result.CloseStatus.HasValue)
{
// 模拟股票价格变化
string stockPriceUpdate = $"Stock price updated: {new Random().Next(100, 200)}";
var messageBytes = System.Text.Encoding.UTF8.GetBytes(stockPriceUpdate);
await webSocket.SendAsync(new ArraySegment<byte>(messageBytes, 0, messageBytes.Length), WebSocketMessageType.Text, true, CancellationToken.None);
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
}
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}
}
在上述代码中,我们创建了一个简单的 WebSocket 服务,当客户端连接到该服务时,会不断模拟股票价格变化并将更新信息发送给客户端。
客户端代码示例(JavaScript):
const socket = new WebSocket('ws://localhost:5000');
socket.addEventListener('open', (event) => {
console.log('Connected to the WebSocket server');
});
socket.addEventListener('message', (event) => {
console.log('Received stock price update:', event.data);
});
socket.addEventListener('close', (event) => {
console.log('Disconnected from the WebSocket server');
});
客户端代码通过 WebSocket 连接到服务器,并监听服务器发送的消息,当接收到股票价格更新信息时,将其打印到控制台。
实时应用的构建步骤如下:
graph LR
A[创建 WebSocket 服务] --> B[处理客户端连接]
B --> C[模拟数据变化]
C --> D[发送更新信息给客户端]
E[创建客户端代码] --> F[连接到服务器]
F --> G[监听服务器消息]
6. 实时应用与安全的结合
在构建实时应用时,安全同样至关重要。我们可以将之前介绍的安全技术应用到实时应用中。例如,使用承载令牌对 WebSocket 连接进行身份验证。
在 WebSocket 服务端代码中添加身份验证逻辑:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.WebSockets;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.ASCII.GetBytes("seriouslyneverleavethissittinginyourcode")),
ValidateIssuer = false,
ValidIssuer = "https://fake.issuer.com",
ValidateAudience = false,
ValidAudience = "https://sampleservice.example.com",
ValidateLifetime = true
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("WebSocketPolicy", policy =>
{
policy.RequireAuthenticatedUser();
});
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
app.UseAuthorization();
app.UseWebSockets();
app.Run(async context =>
{
if (context.WebSockets.IsWebSocketRequest)
{
if (context.User.Identity.IsAuthenticated)
{
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
await Echo(context, webSocket);
}
else
{
context.Response.StatusCode = 401;
}
}
else
{
context.Response.StatusCode = 400;
}
});
}
private async Task Echo(Microsoft.AspNetCore.Http.HttpContext context, WebSocket webSocket)
{
var buffer = new byte[1024 * 4];
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
while (!result.CloseStatus.HasValue)
{
// 模拟股票价格变化
string stockPriceUpdate = $"Stock price updated: {new Random().Next(100, 200)}";
var messageBytes = System.Text.Encoding.UTF8.GetBytes(stockPriceUpdate);
await webSocket.SendAsync(new ArraySegment<byte>(messageBytes, 0, messageBytes.Length), WebSocketMessageType.Text, true, CancellationToken.None);
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
}
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}
}
在上述代码中,我们添加了 JWT 身份验证和授权逻辑,只有经过身份验证的用户才能建立 WebSocket 连接。
客户端在连接 WebSocket 时需要携带有效的 JWT 令牌:
const token = 'your_valid_jwt_token';
const socket = new WebSocket(`ws://localhost:5000?token=${token}`);
socket.addEventListener('open', (event) => {
console.log('Connected to the WebSocket server');
});
socket.addEventListener('message', (event) => {
console.log('Received stock price update:', event.data);
});
socket.addEventListener('close', (event) => {
console.log('Disconnected from the WebSocket server');
});
实时应用与安全结合的步骤如下:
| 步骤 | 描述 |
| ---- | ---- |
| 1 | 在服务端配置 JWT 身份验证和授权策略 |
| 2 | 在服务端代码中检查用户是否经过身份验证 |
| 3 | 客户端在连接 WebSocket 时携带有效的 JWT 令牌 |
实时应用与安全结合的流程如下:
graph LR
A[客户端携带令牌连接 WebSocket] --> B{服务端验证令牌}
B -->|验证通过| C[建立 WebSocket 连接]
B -->|验证失败| D[返回 401 错误]
C --> E[模拟数据变化并发送更新信息]
7. 总结
综上所述,构建安全且实时的应用需要综合考虑多个方面。在安全方面,我们可以使用 OIDC、客户端凭证模式和承载令牌等方法保护微服务,同时要注意密钥的安全管理和密钥轮换。在实时应用方面,要明确“实时”的定义,利用 WebSockets 等技术实现数据的实时推送。将安全技术应用到实时应用中,可以确保数据的安全性和完整性。通过合理运用这些技术和方法,我们可以构建出既安全又高效的实时应用和服务。
超级会员免费看

被折叠的 条评论
为什么被折叠?



