ASP.NET Identity实现分布式Session,Docker+Nginx+Redis+ASP.NET CORE Identity
零、背景介绍
在学习ASP.NET CORE开发的过程中,身份认证是必须考虑的一项必要的组件。ASP.NET CORE Identity是由微软官方开发的一整套身份认证组件,兼具完整性和自由度。Docker作为目前虚拟化的主流解决方案,可以很快捷地实现应用的打包和部署。Nginx作为反向代理,结合Docker多环境部署,可以实现负载均衡功能。而在分布式环境下,Session的共享,特别是登录状态的共享是难以逾越的一个“小”问题。
然而,这个“小”问题,却让我花费了大量的时间搞清楚了相互之间的协作关系,并成功实现了Docker+Nginx+Redis多种组件相结合的解决方案。
环境:ASP.NET Core 2.0
一、ASP.NET CORE Identity
为了实现Session共享,需要在Cookie中存储Session的ID信息以及用户信息,从而实现在多个应用之间的信息共享。有关ASP.NET CORE Identity的介绍,这里不在赘述。
ASP.NET CORE Identity主要包括UserManager和SignInManager两个主要的管理类,从名称可以看出来SignInManager实现的是登陆的管理,因为涉及到登录状态以及登录用户信息的共享,所以我们需要实现自定义的SignInManager类,重写其中最为重要的登录和登出方法。
1 public override Task<SignInResult> PasswordSignInAsync(ApplicationUser user, string password, bool isPersistent, bool lockoutOnFailure) 2 { 3 return base.PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure) 4 .ContinueWith<SignInResult>(task => 5 { 6 if (task.Result == SignInResult.Success) 7 { 8 LoginSucceeded(user); 9 } 10 11 return task.Result; 12 }); 13 } 14 15 public override Task<SignInResult> TwoFactorAuthenticatorSignInAsync(string code, bool isPersistent, bool rememberClient) 16 { 17 ApplicationUser au = this.GetTwoFactorAuthenticationUserAsync().Result; 18 return base.TwoFactorAuthenticatorSignInAsync(code, isPersistent, rememberClient) 19 .ContinueWith<SignInResult>(task => 20 { 21 if (task.Result == SignInResult.Success && au != null) 22 { 23 LoginSucceeded(au); 24 } 25 26 return task.Result; 27 }); 28 } 29 30 public override Task SignOutAsync() 31 { 32 return base.SignOutAsync() 33 .ContinueWith(task => 34 { 35 LogoutSucceeded(Context.Request.Cookies["sessionId"]); 36 }); ; 37 } 38 39 public override bool IsSignedIn(ClaimsPrincipal principal) 40 { 41 if (!Context.User.Identity.IsAuthenticated) 42 { 43 if (Context.Request.Cookies.ContainsKey("sessionId")) 44 { 45 string userInfor = Context.Session.GetString(Context.Request.Cookies["sessionId"]); 46 if (!string.IsNullOrEmpty(userInfor)) 47 { 48 ApplicationUser user = JsonConvert.DeserializeObject<ApplicationUser>(userInfor); 49 if (user != null) 50 { 51 principal = Context.User = this.ClaimsFactory.CreateAsync(user).Result; 52 } 53 } 54 } 55 } 56 57 var flag = base.IsSignedIn(principal); 58 59 return flag; 60 } 61 62 private void LoginSucceeded(ApplicationUser user) 63 { 64 try 65 { 66 string sessionId = Guid.NewGuid().ToString(); 67 string userInfor = JsonConvert.SerializeObject(user); 68 Context.Session.SetString(sessionId, userInfor); 69 Context.Response.Cookies.Delete("sessionId"); 70 Context.Response.Cookies.Append("sessionId", sessionId); 71 } 72 catch (Exception xcp) 73 { 74 MessageQueue.Enqueue(MessageFactory.CreateMessage(xcp)); 75 } 76 } 77 78 private void LogoutSucceeded(string sessionId) 79 { 80 try 81 { 82 if (!string.IsNullOrEmpty(sessionId)) 83 { 84 Context.Session.Remove(sessionId); 85 } 86 } 87 catch (Exception xcp) 88 { 89 MessageQueue.Enqueue(MessageFactory.CreateMessage(xcp)); 90 } 91 }
重写之后,需要在Startup.cs代码ConfigureServices方法中注册使用。
services.AddIdentity<ApplicationUser, IdentityRole>(o => { o.Password.RequireNonAlphanumeric = false; }) .AddEntityFrameworkStores<MyDbContext>() .AddSignInManager<MySignInManager>() .AddDefaultTokenProviders();
二、Docker
在实现自定义Identiy中的SignInManager类以后,将网站打包为Docker镜像(Image),然后根据需要运行多个容器(Container),这些容器的功能是相同的,其实是多个网站实例,跑在不同的端口上面,相当于实现了分布式部署。比如,运行三个容器的命令如下,分别跑在5000,5001和5002端口。
docker run --name webappdstr_0 -d -p 5000:80 -v /etc/localtime:/etc/localtime webapp:1.0 docker run --name webappdstr_1 -d -p 5001:80 -v /etc/localtime:/etc/localtime webapp:1.0 docker run --name webappdstr_2 -d -p 5002:80 -v /etc/localtime:/etc/localtime webapp:1.0
三、Nginx
在完成应用部署后,通过修改Nginx配置,实现负载均衡功能。主要配置如下:
1 # WebAppDistributed 2 server{ 3 listen 5443 ssl; 4 server_name www.webapp.com; 5 6 ssl_certificate ../cert/ssl.crt; 7 ssl_certificate_key ../cert/ssl.key; 8 9 location / { 10 proxy_pass http://webappserverd/; 11 proxy_redirect off; 12 proxy_set_header Host $host; 13 proxy_set_header X-Real-IP $remote_addr; 14 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 15 } 16 } 17 18 upstream webappserverd{ 19 server localhost:5000; 20 server localhost:5001; 21 server localhost:5002; 22 }
以上配置5000,5001和5002三个应用,即对应于Docker部署的三个网站应用。
四、Redis
Redis主要是实现Session的共享,通过Microsoft.Extensions.Caching.Redis.Core组件(通过Nuget获取),在Startup.cs代码ConfigureServices方法中添加Redis中间件服务。
// Redis services.AddDistributedRedisCache(option => { //redis 数据库连接字符串 option.Configuration = Configuration.GetConnectionString("RedisConnection"); //redis 实例名 option.InstanceName = "master"; });
Redis的地址获取的是appsettings.json配置中的配置项。
{ "ConnectionStrings": { ..., "RedisConnection": "192.168.1.16:6379" }, "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Warning" } } }
五、重点难点
主要是总结下碰到的各种坑以及解决方案。
1、Session共享的需要DataProtection以及相关的配置支持
ASP.NET CORE对Session进行了加密,为了能够在多个分布式应用中实现共享,则需要使用相同的加密Key。实现共享的方式有多种,这里采用自定义XmlRepository来实现。
1 public class CustomXmlRepository : IXmlRepository 2 { 3 private readonly string keyContent = @""; //使用前,插入key内容 4 5 public virtual IReadOnlyCollection<XElement> GetAllElements() 6 { 7 return GetAllElementsCore().ToList().AsReadOnly(); 8 } 9 10 private IEnumerable<XElement> GetAllElementsCore() 11 { 12 yield return XElement.Parse(keyContent); 13 } 14 public virtual void StoreElement(XElement element, string friendlyName) 15 { 16 if (element == null) 17 { 18 throw new ArgumentNullException(nameof(element)); 19 } 20 StoreElementCore(element, friendlyName); 21 } 22 23 private void StoreElementCore(XElement element, string filename) 24 { 25 } 26 }
之后,在Startup.cs中启用DataProtection中间件,并进行配置。
1 // Set data protection. 2 services.AddDataProtection(configure => 3 { 4 configure.ApplicationDiscriminator = "WebApplication"; 5 }) 6 .SetApplicationName("WebApplication") 7 .AddKeyManagementOptions(options => 8 { 9 //配置自定义XmlRepository 10 options.XmlRepository = new CustomXmlRepository(); 11 }) 12 .ProtectKeysWithCertificate(new System.Security.Cryptography.X509Certificates.X509Certificate2("webapp.crt"));