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         }
MySignInManager

   重写之后,需要在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    }
Nginx config

     以上配置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     }
CustomXmlRepository

    之后,在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"));
Startup.cs
原文地址:https://www.cnblogs.com/junier/p/11091102.html