零、背景介绍web
在学习ASP.NET CORE开发的过程当中,身份认证是必须考虑的一项必要的组件。ASP.NET CORE Identity是由微软官方开发的一整套身份认证组件,兼具完整性和自由度。Docker做为目前虚拟化的主流解决方案,能够很快捷地实现应用的打包和部署。Nginx做为反向代理,结合Docker多环境部署,能够实现负载均衡功能。而在分布式环境下,Session的共享,特别是登陆状态的共享是难以逾越的一个“小”问题。redis
然而,这个“小”问题,却让我花费了大量的时间搞清楚了相互之间的协做关系,并成功实现了Docker+Nginx+Redis多种组件相结合的解决方案。docker
环境:ASP.NET Core 2.0数据库
1、ASP.NET CORE Identityjson
为了实现Session共享,须要在Cookie中存储Session的ID信息以及用户信息,从而实如今多个应用之间的信息共享。有关ASP.NET CORE Identity的介绍,这里不在赘述。session
ASP.NET CORE Identity主要包括UserManager和SignInManager两个主要的管理类,从名称能够看出来SignInManager实现的是登录的管理,由于涉及到登陆状态以及登陆用户信息的共享,因此咱们须要实现自定义的SignInManager类,重写其中最为重要的登陆和登出方法。app
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();
2、Dockerwebapp
在实现自定义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
3、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部署的三个网站应用。
4、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" } } }
5、重点难点
主要是总结下碰到的各类坑以及解决方案。
一、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"));