在 mono 下使用微软的 OWIN 认证中间件
使用 Microsoft.Owin.Security 中间件作为 OWIN 应用的标准验证在 IIS 下面工作良好, 不过最近在将 WebAPI 应用迁移到 Linux Mono 的环境时, 发现这个中间件不能运行, 在启动时会抛出下面的异常:
这个异常是说无法加载类型 Microsoft.Owin.Security.DataProtection.DpapiDataProtector
, 通过 ILSpy 分析 Microsoft.Owin.Security.dll 发现, Microsoft.Owin.Security.DataProtection.DpapiDataProtector
使用 System.Security.Cryptography.DpapiDataProtector
实现, 而 System.Security.Cryptography.DpapiDataProtector
使用了 win32 函数实现, 因此,不能直接在非 windows 环境下运行。
不过, Microsoft.Owin.Security 中预留了扩展接口 IDataProtectionProvider
, 可以实现自定义的 IDataProtector
, Mono 内置了 AesManaged
类, 可以用来实现自定义的 IDataProtector
, 示例代码如下:
public class AesDataProtector : IDataProtector {
private readonly byte[] key;
public AesDataProtector(string key) {
using (var sha1 = new SHA256Managed()) {
this.key = sha1.ComputeHash(Encoding.UTF8.GetBytes(key));
}
}
public byte[] Protect(byte[] userData) {
byte[] dataHash;
using (var sha = new SHA256Managed()) {
dataHash = sha.ComputeHash(userData);
}
using (AesManaged aesAlg = new AesManaged()) {
aesAlg.Key = key;
aesAlg.GenerateIV();
using (var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV))
using (var msEncrypt = new MemoryStream()) {
msEncrypt.Write(aesAlg.IV, 0, 16);
using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
using (var bwEncrypt = new BinaryWriter(csEncrypt)) {
bwEncrypt.Write(dataHash);
bwEncrypt.Write(userData.Length);
bwEncrypt.Write(userData);
}
var protectedData = msEncrypt.ToArray();
return protectedData;
}
}
}
public byte[] Unprotect(byte[] protectedData) {
using (AesManaged aesAlg = new AesManaged()) {
aesAlg.Key = key;
using (var msDecrypt = new MemoryStream(protectedData)) {
byte[] iv = new byte[16];
msDecrypt.Read(iv, 0, 16);
aesAlg.IV = iv;
using (var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV))
using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
using (var brDecrypt = new BinaryReader(csDecrypt)) {
var signature = brDecrypt.ReadBytes(32);
var len = brDecrypt.ReadInt32();
var data = brDecrypt.ReadBytes(len);
byte[] dataHash;
using (var sha = new SHA256Managed()) {
dataHash = sha.ComputeHash(data);
}
if (!dataHash.SequenceEqual(signature)) {
throw new SecurityException("Signature does not match the computed hash");
}
return data;
}
}
}
}
}
再来实现一个 IDataProtectionProvider
, 提供 AesDataProtector
实例, 代码如下:
public class AesDataProtectionProvider : IDataProtectionProvider {
private string appName;
public AesDataProtectionProvider() : this(Guid.NewGuid().ToString()) {
}
public AesDataProtectionProvider(string appName) {
if (appName == null) {
throw new ArgumentNullException("appName");
}
this.appName = appName;
}
public IDataProtector Create(params string[] purposes) {
return new AesDataProtector(appName ":" string.Join(",", purposes));
}
}
为了方便使用, 对 Owin.IAppBuilder
做一个扩展方法 UseAesDataProtectionProvider
, 代码如下:
public static void UseAesDataProtectionProvider(this IAppBuilder app) {
const string hostAppNameKey = "host.AppName";
if (app.Properties.ContainsKey(hostAppNameKey)) {
var appName = app.Properties[hostAppNameKey].ToString();
app.SetDataProtectionProvider(new AesDataProtectionProvider(appName));
}
else {
app.SetDataProtectionProvider(new AesDataProtectionProvider());
}
}
有了上面的扩展方法, 使用自己实现的 AesDataProtectionProvider
就非常简单了, 只要在 UseCookieAuthentication
之前加上一句 UseAesDataProtectionProvider
即可, 下面是示例代码:
void Configure(IAppBuilder app) {
// handle static file
app.UseStaticFile(new StaticFileMiddlewareOptions {
RootDirectory = @"../Website",
DefaultFile = "index.html",
MimeTypeProvider = new MimeTypeProvider(),
EnableETag = true,
ETagProvider = new LastWriteTimeETagProvider()
});
// use aes data protection provider;
app.UseAesDataProtectionProvider();
// cookie auth;
app.UseCookieAuthentication(new CookieAuthenticationOptions{
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType
});
// web-api
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
本文的全部源代码已经上传到至 github , 也做了一个 nuget 包方便大家使用。