ASP.NET Core微服务之基于Ocelot+IdentityServer实现统一验证与授权

释放双眼,带上耳机,听听看~!

Tip: 此篇已加入.NET Core微服务基础系列文章索引

一、案例结构总览

这里,假设我们有两个客户端(一个Web网站,一个移动App),他们要使用系统,需要通过API网关(这里API网关始终作为客户端的统一入口)先向IdentityService进行Login以进行验证并获取Token,在IdentityService的验证过程中会访问数据库以验证。然后再带上Token通过API网关去访问具体的API Service。这里我们的IdentityService基于IdentityServer4开发,它具有统一登录验证和授权的功能。

二、改写API Gateway

这里主要基于前两篇已经搭好的API Gateway进行改写,如不熟悉,可以先浏览前两篇文章:Part 1和Part 2。

2.1 配置文件的改动


1
2
3
4
5
6
7
8
9
10
11
12
1  ......  
2  "AuthenticationOptions": {
3    "AuthenticationProviderKey": "ClientServiceKey",
4    "AllowedScopes": []
5  }
6  ......  
7  "AuthenticationOptions": {
8    "AuthenticationProviderKey": "ProductServiceKey",
9    "AllowedScopes": []
10  }
11  ......  
12

上面分别为两个示例API Service增加Authentication的选项,为其设置ProviderKey。下面会对不同的路由规则设置的ProviderKey设置具体的验证方式。

2.2 改写StartUp类


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1    public void ConfigureServices(IServiceCollection services)
2    {
3        // IdentityServer
4        #region IdentityServerAuthenticationOptions => need to refactor
5        Action<IdentityServerAuthenticationOptions> isaOptClient = option =>
6            {
7                option.Authority = Configuration["IdentityService:Uri"];
8                option.ApiName = "clientservice";
9                option.RequireHttpsMetadata = Convert.ToBoolean(Configuration["IdentityService:UseHttps"]);
10                option.SupportedTokens = SupportedTokens.Both;
11                option.ApiSecret = Configuration["IdentityService:ApiSecrets:clientservice"];
12            };
13
14        Action<IdentityServerAuthenticationOptions> isaOptProduct = option =>
15        {
16            option.Authority = Configuration["IdentityService:Uri"];
17            option.ApiName = "productservice";
18            option.RequireHttpsMetadata = Convert.ToBoolean(Configuration["IdentityService:UseHttps"]);
19            option.SupportedTokens = SupportedTokens.Both;
20            option.ApiSecret = Configuration["IdentityService:ApiSecrets:productservice"];
21        };
22        #endregion
23
24        services.AddAuthentication()
25            .AddIdentityServerAuthentication("ClientServiceKey", isaOptClient)
26            .AddIdentityServerAuthentication("ProductServiceKey", isaOptProduct);
27        // Ocelot
28        services.AddOcelot(Configuration);
29        ......      
30    }
31

这里的ApiName主要对应于IdentityService中的ApiResource中定义的ApiName。这里用到的配置文件定义如下:


1
2
3
4
5
6
7
8
9
1  "IdentityService": {
2    "Uri": "http://localhost:5100",
3    "UseHttps": false,
4    "ApiSecrets": {
5      "clientservice": "clientsecret",
6      "productservice": "productsecret"
7    }
8  }
9

这里的定义方式,我暂时还没想好怎么重构,不过肯定是需要重构的,不然这样一个一个写比较繁琐,且不利于配置。

三、新增IdentityService

这里我们会基于之前基于IdentityServer的两篇文章,新增一个IdentityService,不熟悉的朋友可以先浏览一下Part 1和Part 2。

3.1 准备工作

新建一个ASP.NET Core Web API项目,绑定端口5100,NuGet安装IdentityServer4。配置好证书,并设置其为“较新则复制”,以便能够在生成目录中读取到。

3.2 定义一个InMemoryConfiguration用于测试


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
1    /// <summary>
2    /// One In-Memory Configuration for IdentityServer => Just for Demo Use
3    /// </summary>
4    public class InMemoryConfiguration
5    {
6        public static IConfiguration Configuration { get; set; }
7        /// <summary>
8        /// Define which APIs will use this IdentityServer
9        /// </summary>
10        /// <returns></returns>
11        public static IEnumerable<ApiResource> GetApiResources()
12        {
13            return new[]
14            {
15                new ApiResource("clientservice", "CAS Client Service"),
16                new ApiResource("productservice", "CAS Product Service"),
17                new ApiResource("agentservice", "CAS Agent Service")
18            };
19        }
20
21        /// <summary>
22        /// Define which Apps will use thie IdentityServer
23        /// </summary>
24        /// <returns></returns>
25        public static IEnumerable<Client> GetClients()
26        {
27            return new[]
28            {
29                new Client
30                {
31                    ClientId = "cas.sg.web.nb",
32                    ClientName = "CAS NB System MPA Client",
33                    ClientSecrets = new [] { new Secret("websecret".Sha256()) },
34                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
35                    AllowedScopes = new [] { "clientservice", "productservice",
36                        IdentityServerConstants.StandardScopes.OpenId,
37                        IdentityServerConstants.StandardScopes.Profile }
38                },
39                new Client
40                {
41                    ClientId = "cas.sg.mobile.nb",
42                    ClientName = "CAS NB System Mobile App Client",
43                    ClientSecrets = new [] { new Secret("mobilesecret".Sha256()) },
44                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
45                    AllowedScopes = new [] { "productservice",
46                        IdentityServerConstants.StandardScopes.OpenId,
47                        IdentityServerConstants.StandardScopes.Profile }
48                },
49                new Client
50                {
51                    ClientId = "cas.sg.spa.nb",
52                    ClientName = "CAS NB System SPA Client",
53                    ClientSecrets = new [] { new Secret("spasecret".Sha256()) },
54                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
55                    AllowedScopes = new [] { "agentservice", "clientservice", "productservice",
56                        IdentityServerConstants.StandardScopes.OpenId,
57                        IdentityServerConstants.StandardScopes.Profile }
58                },
59                new Client
60                {
61                    ClientId = "cas.sg.mvc.nb.implicit",
62                    ClientName = "CAS NB System MVC App Client",
63                    AllowedGrantTypes = GrantTypes.Implicit,
64                    RedirectUris = { Configuration["Clients:MvcClient:RedirectUri"] },
65                    PostLogoutRedirectUris = { Configuration["Clients:MvcClient:PostLogoutRedirectUri"] },
66                    AllowedScopes = new [] {
67                        IdentityServerConstants.StandardScopes.OpenId,
68                        IdentityServerConstants.StandardScopes.Profile,
69                        "agentservice", "clientservice", "productservice"
70                    },
71                    //AccessTokenLifetime = 3600, // one hour
72                    AllowAccessTokensViaBrowser = true // can return access_token to this client
73                }
74            };
75        }
76
77        /// <summary>
78        /// Define which IdentityResources will use this IdentityServer
79        /// </summary>
80        /// <returns></returns>
81        public static IEnumerable<IdentityResource> GetIdentityResources()
82        {
83            return new List<IdentityResource>
84            {
85                new IdentityResources.OpenId(),
86                new IdentityResources.Profile(),
87            };
88        }
89    }
90

这里使用了上一篇的内容,不再解释。实际环境中,则应该考虑从NoSQL或数据库中读取。

3.3 定义一个ResourceOwnerPasswordValidator

在IdentityServer中,要实现自定义的验证用户名和密码,需要实现一个接口:IResourceOwnerPasswordValidator


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
1    public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
2    {
3        private ILoginUserService loginUserService;
4
5        public ResourceOwnerPasswordValidator(ILoginUserService _loginUserService)
6        {
7            this.loginUserService = _loginUserService;
8        }
9
10        public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
11        {
12            LoginUser loginUser = null;
13            bool isAuthenticated = loginUserService.Authenticate(context.UserName, context.Password, out loginUser);
14            if (!isAuthenticated)
15            {
16                context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Invalid client credential");
17            }
18            else
19            {
20                context.Result = new GrantValidationResult(
21                    subject : context.UserName,
22                    authenticationMethod : "custom",
23                    claims : new Claim[] {
24                        new Claim("Name", context.UserName),
25                        new Claim("Id", loginUser.Id.ToString()),
26                        new Claim("RealName", loginUser.RealName),
27                        new Claim("Email", loginUser.Email)
28                    }
29                );
30            }
31
32            return Task.CompletedTask;
33        }
34    }
35

这里的ValidateAsync方法中(你也可以把它写成异步的方式,这里使用的是同步的方式),会调用EF去访问数据库进行验证,数据库的定义如下(密码应该做加密,这里只做demo,没用弄):

至于EF部分,则是一个典型的简单的Service调用Repository的逻辑,下面只贴Repository部分:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1    public class LoginUserRepository : RepositoryBase<LoginUser, IdentityDbContext>, ILoginUserRepository
2    {
3        public LoginUserRepository(IdentityDbContext dbContext) : base(dbContext)
4        {
5        }
6
7        public LoginUser Authenticate(string _userName, string _userPassword)
8        {
9            var entity = DbContext.LoginUsers.FirstOrDefault(p => p.UserName == _userName &&
10                p.Password == _userPassword);
11
12            return entity;
13        }
14    }
15

其他具体逻辑请参考示例代码。

3.4 改写StarUp类


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1    public void ConfigureServices(IServiceCollection services)
2    {
3        // IoC - DbContext
4        services.AddDbContextPool<IdentityDbContext>(
5            options => options.UseSqlServer(Configuration["DB:Dev"]));
6        // IoC - Service & Repository
7        services.AddScoped<ILoginUserService, LoginUserService>();
8        services.AddScoped<ILoginUserRepository, LoginUserRepository>();
9        // IdentityServer4
10        string basePath = PlatformServices.Default.Application.ApplicationBasePath;
11        InMemoryConfiguration.Configuration = this.Configuration;
12        services.AddIdentityServer()
13            .AddSigningCredential(new X509Certificate2(Path.Combine(basePath,
14                Configuration["Certificates:CerPath"]),
15                Configuration["Certificates:Password"]))
16            //.AddTestUsers(InMemoryConfiguration.GetTestUsers().ToList())
17            .AddInMemoryIdentityResources(InMemoryConfiguration.GetIdentityResources())
18            .AddInMemoryApiResources(InMemoryConfiguration.GetApiResources())
19            .AddInMemoryClients(InMemoryConfiguration.GetClients())
20            .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
21            .AddProfileService<ProfileService>();
22        ......
23    }
24

这里高亮的是新增的部分,为了实现自定义验证。关于ProfileService的定义如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
1    public class ProfileService : IProfileService
2    {
3        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
4        {
5            var claims = context.Subject.Claims.ToList();
6            context.IssuedClaims = claims.ToList();
7        }
8
9        public async Task IsActiveAsync(IsActiveContext context)
10        {
11            context.IsActive = true;
12        }
13    }
14

3.5 新增统一Login入口

这里新增一个LoginController:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
1    [Produces("application/json")]
2    [Route("api/Login")]
3    public class LoginController : Controller
4    {
5        private IConfiguration configuration;
6        public LoginController(IConfiguration _configuration)
7        {
8            configuration = _configuration;
9        }
10
11        [HttpPost]
12        public async Task<ActionResult> RequestToken([FromBody]LoginRequestParam model)
13        {
14            Dictionary<string, string> dict = new Dictionary<string, string>();
15            dict["client_id"] = model.ClientId;
16            dict["client_secret"] = configuration[$"IdentityClients:{model.ClientId}:ClientSecret"];
17            dict["grant_type"] = configuration[$"IdentityClients:{model.ClientId}:GrantType"];
18            dict["username"] = model.UserName;
19            dict["password"] = model.Password;
20
21            using (HttpClient http = new HttpClient())
22            using (var content = new FormUrlEncodedContent(dict))
23            {
24                var msg = await http.PostAsync(configuration["IdentityService:TokenUri"], content);
25                if (!msg.IsSuccessStatusCode)
26                {
27                    return StatusCode(Convert.ToInt32(msg.StatusCode));
28                }
29
30                string result = await msg.Content.ReadAsStringAsync();
31                return Content(result, "application/json");
32            }
33        }
34    }
35

这里假设客户端会传递用户名,密码以及客户端ID(ClientId,比如上面InMemoryConfiguration中的cas.sg.web.nb或cas.sg.mobile.nb)。然后构造参数再调用connect/token接口进行身份验证和获取token。这里将client_secret等机密信息封装到了服务器端,无须客户端传递(对于机密信息一般也不会让客户端知道):


1
2
3
4
5
6
7
8
9
10
11
1  "IdentityClients": {
2    "cas.sg.web.nb": {
3      "ClientSecret": "websecret",
4      "GrantType": "password"
5    },
6    "cas.sg.mobile.nb": {
7      "ClientSecret": "mobilesecret",
8      "GrantType": "password"
9    }
10  }
11

3.6 加入API网关中

在API网关的Ocelot配置文件中加入配置,配置如下(这里我是开发用,所以没有用服务发现,实际环境建议采用服务发现):


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
1    // --> Identity Service Part
2    {
3      "UseServiceDiscovery": false, // do not use Consul service discovery in DEV env
4      "DownstreamPathTemplate": "/api/{url}",
5      "DownstreamScheme": "http",
6      "DownstreamHostAndPorts": [
7        {
8          "Host": "localhost",
9          "Port": "5100"
10        }
11      ],
12      "ServiceName": "CAS.IdentityService",
13      "LoadBalancerOptions": {
14        "Type": "RoundRobin"
15      },
16      "UpstreamPathTemplate": "/api/identityservice/{url}",
17      "UpstreamHttpMethod": [ "Get", "Post" ],
18      "RateLimitOptions": {
19        "ClientWhitelist": [ "admin" ], // 白名单
20        "EnableRateLimiting": true, // 是否启用限流
21        "Period": "1m", // 统计时间段:1s, 5m, 1h, 1d
22        "PeriodTimespan": 15, // 多少秒之后客户端可以重试
23        "Limit": 10 // 在统计时间段内允许的最大请求数量
24      },
25      "QoSOptions": {
26        "ExceptionsAllowedBeforeBreaking": 2, // 允许多少个异常请求
27        "DurationOfBreak": 5000, // 熔断的时间,单位为秒
28        "TimeoutValue": 3000 // 如果下游请求的处理时间超过多少则视如该请求超时
29      },
30      "HttpHandlerOptions": {
31        "UseTracing": false // use butterfly to tracing request chain
32      },
33      "ReRoutesCaseSensitive": false // non case sensitive
34    }
35

四、改写业务API Service

4.1 ClientService

(1)安装IdentityServer4.AccessTokenValidation

NuGet>Install-Package IdentityServer4.AccessTokenValidation

(2)改写StartUp类


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1    public IServiceProvider ConfigureServices(IServiceCollection services)
2    {
3        ......
4
5        // IdentityServer
6        services.AddAuthentication(Configuration["IdentityService:DefaultScheme"])
7            .AddIdentityServerAuthentication(options =>
8            {
9                options.Authority = Configuration["IdentityService:Uri"];
10                options.RequireHttpsMetadata = Convert.ToBoolean(Configuration["IdentityService:UseHttps"]);
11            });
12
13        ......
14    }
15

这里配置文件的定义如下:


1
2
3
4
5
6
7
1  "IdentityService": {
2    "Uri": "http://localhost:5100",
3    "DefaultScheme":  "Bearer",
4    "UseHttps": false,
5    "ApiSecret": "clientsecret"
6  }
7

4.2 ProductService

与ClientService一致,请参考示例代码。

五、测试

5.1 测试Client: cas.sg.web.nb

(1)统一验证&获取token (by API网关)

(2)访问clientservice (by API网关)

(3)访问productservice(by API网关)

5.2 测试Client: cas.sg.mobile.nb

由于在IdentityService中我们定义了一个mobile的客户端,但是其访问权限只有productservice,所以我们来测试一下:

(1)统一验证&获取token

(2)访问ProductService(by API网关)

(3)访问ClientService(by API网关) => 401 Unauthorized

六、小结

本篇主要基于前面Ocelot和IdentityServer的文章的基础之上,将Ocelot和IdentityServer进行结合,通过建立IdentityService进行统一的身份验证和授权,最后演示了一个案例以说明如何实现。不过,本篇实现的Demo还存在诸多不足,比如需要重构的代码较多如网关中各个Api的验证选项的注册,没有对各个请求做用户角色和权限的验证等等,相信随着研究和深入的深入,这些都可以逐步解决。后续会探索一下数据一致性的基本知识以及框架使用,到时再做一些分享。

示例代码

Click Here => 点我进入GitHub

参考资料

杨中科,《.NET Core微服务介绍课程》

作者:周旭龙

出处:http://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

给TA打赏
共{{data.count}}人
人已打赏
安全网络

CDN安全市场到2022年价值76.3亿美元

2018-2-1 18:02:50

安全资讯

为什么这届年轻人越挣钱越穷?

2021-8-16 15:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索