ASP.NET CORE 2.0 分布式会话管理与 AWS DynamoDB



我正在寻找使用 Amazon Dynamodb 在 CORE 2.0 ASP.NET 中实现分布式会话管理。但找不到任何文档或示例源代码。

如何使用 ASP.NET CORE 2.0 和 DynamoDB 实施分布式会话管理?

2023 年更新:如果有人仍在寻找解决方案,AWS 为此宣布了一个官方软件包,这无疑比我 2018 年的自制软件更好(下图(。在此处阅读发布博客:https://aws.amazon.com/blogs/developer/introducing-the-aws-net-distributed-cache-provider-for-dynamodb-preview/

原答案:

我已经实施了适用于 ASP.NET 核心分布式会话状态的 AWS DynamoDB,包括在 DynamoDB 中存储会话 Cookie 加密密钥(您必须将密钥存储在某个位置,以便应用程序的不同实例可以相互解码 Cookie(。

请注意,这是一个"裸骨"实现,我还没有让它可测试,或者使用 DI 等。这两个类是DynamoDbCache和DdbXmlRepository。

DynamoDBCache.cs:

using System;
using System.Threading;
using System.Threading.Tasks;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DocumentModel;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Caching.DynamoDb
{
public class DynamoDbCache : IDistributedCache
{
private static IAmazonDynamoDB _client;
private static Table _table;
private string _tableName = "ASP.NET_SessionState";
private string _ttlfield = "TTL";
private int _sessionMinutes = 20;
private enum ExpiryType
{
Sliding,
Absolute
}
public DynamoDbCache(IOptions<DynamoDbCacheOptions> optionsAccessor, IAmazonDynamoDB dynamoDb)
{
_client = dynamoDb;
if (optionsAccessor != null)
{
_tableName = optionsAccessor.Value.TableName;
_ttlfield = optionsAccessor.Value.TtlAttribute;
_sessionMinutes = (int)optionsAccessor.Value.IdleTimeout.TotalMinutes;
}
if (_client == null)
{
_client = new AmazonDynamoDBClient();
}
if (_table == null)
{
_table = Table.LoadTable(_client, _tableName);
}
}
public byte[] Get(string key)
{
return GetAsync(key).Result;
}
public async Task<byte[]> GetAsync(string key, CancellationToken token = default(CancellationToken))
{
var value = await _table.GetItemAsync(key);
if (value == null || value["Session"] == null)
{
return null;
}
return value["Session"].AsByteArray();
}
public void Refresh(string key)
{
var value = _table.GetItemAsync(key).Result;
if (value == null || value["ExpiryType"] == null || value["ExpiryType"] != "Sliding")
{
return;
}
value[_ttlfield] = DateTimeOffset.Now.ToUniversalTime().ToUnixTimeSeconds() + (_sessionMinutes * 60);
Task.Run(() => Set(key, value["Session"].AsByteArray(), new DistributedCacheEntryOptions { SlidingExpiration = new TimeSpan(0, _sessionMinutes, 0) }));
}
public async Task RefreshAsync(string key, CancellationToken token = default(CancellationToken))
{
var value = _table.GetItemAsync(key).Result;
if (value == null || value["ExpiryType"] == null || value["ExpiryType"] != "Sliding")
{
return;
}
value[_ttlfield] = DateTimeOffset.Now.ToUniversalTime().ToUnixTimeSeconds() + (_sessionMinutes * 60);
await SetAsync(key, value["Session"].AsByteArray(), new DistributedCacheEntryOptions { SlidingExpiration = new TimeSpan(0, _sessionMinutes, 0) });
}
public void Remove(string key)
{
_table.DeleteItemAsync(key).Wait();
}
public async Task RemoveAsync(string key, CancellationToken token = default(CancellationToken))
{
await _table.DeleteItemAsync(key);
}
public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
{
SetAsync(key, value, options).Wait();
}
public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken))
{
ExpiryType expiryType;
var epoctime = GetEpochExpiry(options, out expiryType);
var _ssdoc = new Document();
_ssdoc.Add("SessionId", key);
_ssdoc.Add("Session", value);
_ssdoc.Add("CreateDate", DateTime.Now.ToUniversalTime().ToString("o"));
_ssdoc.Add("ExpiryType", expiryType.ToString());
_ssdoc.Add(_ttlfield, epoctime);
await _table.PutItemAsync(_ssdoc);
}
private long GetEpochExpiry(DistributedCacheEntryOptions options, out ExpiryType expiryType)
{
if (options.SlidingExpiration.HasValue)
{
expiryType = ExpiryType.Sliding;
return DateTimeOffset.Now.ToUniversalTime().ToUnixTimeSeconds() + (long)options.SlidingExpiration.Value.TotalSeconds;
}
else if (options.AbsoluteExpiration.HasValue)
{
expiryType = ExpiryType.Absolute;
return options.AbsoluteExpiration.Value.ToUnixTimeSeconds();
}
else if (options.AbsoluteExpirationRelativeToNow.HasValue)
{
expiryType = ExpiryType.Absolute;
return DateTimeOffset.Now.Add(options.AbsoluteExpirationRelativeToNow.Value).ToUniversalTime().ToUnixTimeSeconds();
}
else
{
throw new Exception("Cache expiry option must be set to Sliding, Absolute or Absolute relative to now");
}
}
}
}

和 DdbXmlRepository:

using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;
using Microsoft.AspNetCore.DataProtection.Repositories;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Website.Session
{
public class DdbXmlRepository : IXmlRepository
{
private static IAmazonDynamoDB _dynamoDb;
public DdbXmlRepository(IAmazonDynamoDB dynamoDb)
{
_dynamoDb = dynamoDb;
}
public IReadOnlyCollection<XElement> GetAllElements()
{
var context = new DynamoDBContext(_dynamoDb);
var search = context.ScanAsync<XmlKey>(new List<ScanCondition>());
var results = search.GetRemainingAsync().Result;
return results.Select(x => XElement.Parse(x.Xml)).ToList();
}
public void StoreElement(XElement element, string friendlyName)
{
var key = new XmlKey
{
Xml = element.ToString(SaveOptions.DisableFormatting),
FriendlyName = friendlyName
};
var context = new DynamoDBContext(_dynamoDb);
context.SaveAsync(key).Wait();
}
}
[DynamoDBTable("AspXmlKeys")]
public class XmlKey
{
[DynamoDBHashKey]
public string KeyId { get; set; } = Guid.NewGuid().ToString();
public string Xml { get; set; }
public string FriendlyName { get; set; }
}
}

您还需要一个服务收集扩展:

using System;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.DynamoDb;
namespace Microsoft.Extensions.DependencyInjection
{
public static class DynamoDbCacheServiceCollectionExtensions
{
/// <summary>
/// Adds Amazon DynamoDB caching services to the specified <see cref="IServiceCollection" />.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
/// <param name="setupAction">An <see cref="Action{DynamoDbCacheOptions}"/> to configure the provided
/// <see cref="DynamoDbCacheOptions"/>.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddDistributedDynamoDbCache(this IServiceCollection services, Action<DynamoDbCacheOptions> setupAction)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (setupAction == null)
{
throw new ArgumentNullException(nameof(setupAction));
}
services.AddOptions();
services.Configure(setupAction);
services.Add(ServiceDescriptor.Singleton<IDistributedCache, DynamoDbCache>());
return services;
}
}
}

最后,我使用了一个选项类来设置表名、默认到期等内容:

using Microsoft.Extensions.Options;
using System;
namespace Microsoft.Extensions.Caching.DynamoDb
{
public class DynamoDbCacheOptions : IOptions<DynamoDbCacheOptions>
{
public string TableName { get; set; } = "ASP.NET_SessionState";
public TimeSpan IdleTimeout { get; set; } = new TimeSpan(0, 20, 0);
public string TtlAttribute { get; set; } = "TTL";
DynamoDbCacheOptions IOptions<DynamoDbCacheOptions>.Value
{
get { return this; }
}
}
}

在你的启动中,你需要用这样的代码在ConfigureServices中连接它:

services.AddDefaultAWSOptions(Configuration.GetAWSOptions());
services.AddAWSService<IAmazonDynamoDB>();
services.AddSingleton<IXmlRepository, DdbXmlRepository>();

services.AddDistributedDynamoDbCache(o => {
o.TableName = "TechSummitSessionState";
o.IdleTimeout = TimeSpan.FromMinutes(30);
});
services.AddSession(o => {
o.IdleTimeout = TimeSpan.FromMinutes(30);
o.Cookie.HttpOnly = false;
});
services.AddDataProtection()
.AddKeyManagementOptions(o => o.XmlRepository = sp.GetService<IXmlRepository>());

希望这有帮助! 它适用于我的应用程序:-(

最新更新