Consul+Ocelot搭建微服务实践--IdentityServer集成
文章目录
本篇幅所涉及的内容比较多,请园友有耐心的品味下去。
1、IdentityServer介绍
IdentityServer4 是asp.netcore 的OpenID Connect和OAuth 2.0框架。官方文档:http://docs.identityserver.io/en/latest/
我主要进行对自己所做的进行总结,不会介绍很多的理论类容。
2、建立IdentityServer
2.1 安装IdentityServer4
可以使用PS进行命令安装: Install-Package IdentityServer4。也可以在Nuget管理中心进行搜索安装。
2.2 定义配置中心
网上很多都是使用的TestUser进行的测试,我这个案例中使用sqlserver持久化进行验证,对于用户这块我后面详细介绍。(要是觉得麻烦那么也可以使用TestUser和)
2.2.1 定义Client
可以查看 官方文档–Client 进行了解Client相关知识。
public class InMemoryConfiguration
{
public static IEnumerable<Client> GetAllClients()
{
return new[]
{
//采用密码模式
new Client
{
ClientId = "api.service1",
ClientSecrets = new [] { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
AllowedScopes = new [] {
"service1",
},
//支持刷新token
AllowOfflineAccess=true,
//toke过期时间。默认一小时
//AccessTokenLifetime=60,
//滑动刷新token的时间。默认一天。
//SlidingRefreshTokenLifetime=60,
//刷新token的模式,固定刷新时间和滑动刷新时间
//RefreshTokenExpiration=TokenExpiration.Sliding,
//详情文档:https://identityserver4-zh-cn.readthedocs.io/zh_CN/release/topics/refresh_tokens.html
},
//采用客户端模式
new Client
{
ClientId = "api.service2",
ClientSecrets = new [] { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = new [] {
"service2",
},
}
};
}
}
2.2.2 定义ApiResource
可以查看 官方文档–ApiResouce 进行了解ApiResouce相关知识。
public class InMemoryConfiguration
{
public static IEnumerable<ApiResource> GetAllResources()
{
return new[]
{
//向Cliam中添加Role、Email。这样在获取HttpContext.User.Cliam时才能获取到role、email
new ApiResource("service1", "微服务架构设计,Service1",new List<string>{
JwtClaimTypes.Role,
JwtClaimTypes.Email,
}),
new ApiResource("service2", "微服务架构设计,Service2",new List<string>{
JwtClaimTypes.Role,
JwtClaimTypes.Email,
})
};
}
}
2.2.3定义IdentityResource
可以查看 官方文档–IdentityResource 进行了解IdentityResource相关知识。
public class InMemoryConfiguration
{
public static IEnumerable<IdentityResource> GetIdentityResources()
{
//定义支持的资源类型。SuportScope的类型
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email()
};
}
}
使用UseIdentityServer先考虑几个问题。
1) 那些API可以使用AuthorizationServer
2) 那些Client可以使用AuthorizationServer
3) 那些User可以使用AuthorizationServer
既然定义了这些资源内容那么就要添加到IdentityServer中去。
在ConfigureServices中添加内容:
#region identityserver4
//添加中间件
services.AddIdentityServer()
.AddSigningCredential(new X509Certificate2(
Path.Combine(Directory.GetCurrentDirectory(),Configuration["Credential:Path"]),
Configuration["Credential:Password"],
X509KeyStorageFlags.MachineKeySet))
.AddInMemoryClients(InMemoryConfiguration.GetAllClients())
.AddInMemoryApiResources(InMemoryConfiguration.GetAllResources())
.AddResourceOwnerValidator<CustomResourceOwnerPasswordValidator>()
.AddProfileService<CustomProfileService>()
.AddCorsPolicyService<CorsPolicyService>();
#endregion
在Configure中使用IdentityServer
app.UseIdentityServer();
对以上添加的内容进行解释
-
在实际的项目中肯定会使用自己的证书的,在这里使用AddSigningCredential添加证书。IdentityServer也友好的提供了开发者使用证书,使用AddDeveloperSigningCredential进行添加。
使用openssl工具生成证书。
下载地址通过我的百度云盘进行下载,因为本地直接下载可能很慢甚至可能下载不了。
链接:https://pan.baidu.com/s/1-kf3T7iOO3PuzFRag_atZQ
提取码:q79u1.1. 使用openssl命令进行证书下载
openssl req -newkey rsa:2048 -nodes -keyout auth.center.key -x509 -days 365 -out auth.center.cer
下面将生成的证书和Key封装成一个文件,以便IdentityServer可以使用它们去正确地签名tokens
openssl pkcs12 -export -in auth.center.cer -inkey cas.clientservice.key -out auth.center.pfx最终生成的目录结构:
1.2 然后将生成的文件拷贝到项目中去,并在appsetting.json文件中配置路径和密码(上面生成证书的时候输入的密码)。{ "DBConnStr": "Server=.;DataBase=Study.Microservices.Information;User=sa;Password=123456", "Credential": { "Path": "cert\\IDS4.pfx", "Password": "cy" }, "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*" }
最后就将所生成的证书配置到AddSigningCredential中去,详情请看上面贴的配置。
-
使用AddResourceOwnerValidator添加自定义验证。
2.1 继承IResourceOwnerPasswordValidator实现Task ValidateAsync(ResourceOwnerPasswordValidationContext context); 方法进行自定义验证。
public class CustomResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { private readonly ISystemClock _clock; private readonly IUserApplicationService _users; public CustomResourceOwnerPasswordValidator(ISystemClock clock, IUserApplicationService users) { _clock = clock; _users = users; } public Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { if (_users.ValidateCredentials(context.UserName, context.Password)) { var user = _users.FindUserByAccount(context.UserName); //验证通过返回结果 //subjectId 为用户唯一标识 一般为用户id //authenticationMethod 描述自定义授权类型的认证方法 //authTime 授权时间 //claims 需要返回的用户身份信息单元 此处应该根据我们从数据库读取到的用户信息 //添加Claims 如果是从数据库中读取角色信息,那么我们应该在此处添加 此处只返回必要的Claim context.Result = new GrantValidationResult( user.Id ?? throw new ArgumentException("Subject ID not set", nameof(user.Id)), OidcConstants.AuthenticationMethods.Password, _clock.UtcNow.UtcDateTime, claims: GetClaims(user)); } else { //验证失败 context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "invalid custom credential"); } return Task.CompletedTask; } /// <summary> /// 添加自定义Claim /// </summary> /// <param name="claims"></param> /// <returns></returns> private ICollection<Claim> GetClaims(UserInfo user) { return new Claim[]{ new Claim(JwtClaimTypes.Id,user.Id), new Claim(JwtClaimTypes.Name,user.Account) }; } }
2.2 用户相关操作接口IUserApplicationService
public interface IUserApplicationService { /// <summary> /// 通过用户名查到用户信息 /// </summary> /// <param name="userName">用户名</param> /// <returns></returns> UserInfo FindUserByAccount(string userName); /// <summary> /// 判断用户是否可以登录 /// </summary> /// <param name="userName">用户名</param> /// <param name="password">密码</param> /// <returns></returns> bool ValidateCredentials(string userName, string password); /// <summary> /// 通过用户id查找用户信息 /// </summary> /// <param name="id">用户id</param> /// <returns></returns> UserInfo FindBySubjectId(string id); }
2.3 用户实体类
[Table("User")] public class UserInfo { [Key] public string Id { get; set; } public string Account { get; set; } public bool IsActive { get; set; } public string Password { get; internal set; } }
2.3 用户相关操作实现UserApplicationService
public class UserApplicationService : IUserApplicationService { public UserInfo FindBySubjectId(string id) { using (var db=new UserDbContext()) { return db.Users.FirstOrDefault(u => u.Id == id & u.IsActive); } } public UserInfo FindUserByAccount(string userName) { using (var db = new UserDbContext()) { return db.Users.FirstOrDefault(u => u.Account == userName & u.IsActive); } } public bool ValidateCredentials(string userName, string password) { using (var db = new UserDbContext()) { return db.Users.Where(u => u.Account == userName & u.Password==password & u.IsActive).Count()>0; } } }
2.4 从上面的实现类中可以看出采用的是EF,那么下面我将简单快速的将Migration的使用以及EF Core的使用进行介绍。
1) 安装Microsoft.EntityFrameworkCore.SqlServer包
2)添加UserDbContextpublic class UserDbContext : DbContext { public UserDbContext() { } public DbSet<UserInfo> Users { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); var build = new ConfigurationBuilder() .SetBasePath(System.IO.Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", false, true) .Build(); optionsBuilder.UseSqlServer(build["DBConnStr"]); } }
DBConnStr 是配置文件中配置的,配置文件件贴在了证书介绍区域。
3)使用Migration将User添加到数据库中去。
执行 Add-Migration 初始化用户 命令添加相关修改到Migrations中去。
最终会生成一个Migrations目录结构,后面对模型进行调整添加修改等等操作都会记录到这个文件中去。
执行 update-database –verbose 更新数据库
最终生成数据库结构:
为了测试我将User表中添加了一条admin/admin的数据,为了后面进行测试。 -
使用AddProfileService添加自定义Profile装载Claim信息
采用自定义验证都要结合AddProfileService来使用,通过AddProfileService来装载前面定义的CustomResourceOwnerPasswordValidatorClaim信息。然后在使用的时候直接调用User.Claims就能看到配置的自定义Claim信息了。
/// <summary> /// 采用自定义profile来装载Claim信息 /// </summary> public class CustomProfileService : IProfileService { /// <summary> /// The logger /// </summary> protected readonly ILogger Logger; private readonly IUserApplicationService _users; /// <summary> /// Initializes a new instance of the <see cref="CustomProfileService"/> class. /// </summary> /// <param name="logger">The logger.</param> public CustomProfileService(ILogger<CustomProfileService> logger,IUserApplicationService users) { Logger = logger; _users = users; } /// <summary> /// 只要有关用户的身份信息单元被请求(例如在令牌创建期间或通过用户信息终点),就会调用此方法 /// </summary> /// <param name="context">The context.</param> /// <returns></returns> public virtual Task GetProfileDataAsync(ProfileDataRequestContext context) { context.LogProfileRequest(Logger); //判断是否有请求Claim信息 if (context.RequestedClaimTypes.Any()) { //根据用户唯一标识查找用户信息 var user = _users.FindBySubjectId(context.Subject.GetSubjectId()); if (user != null) { //调用此方法以后内部会进行过滤,只将用户请求的Claim加入到 context.IssuedClaims 集合中 context.IssuedClaims = context.Subject.Claims.ToList(); } } context.LogIssuedClaims(Logger); return Task.CompletedTask; } /// <summary> /// 验证用户是否有效 例如:token创建或者验证 /// </summary> /// <param name="context">The context.</param> /// <returns></returns> public virtual Task IsActiveAsync(IsActiveContext context) { Logger.LogDebug("IsActive called from: {caller}", context.Caller); var user = _users.FindBySubjectId(context.Subject.GetSubjectId()); context.IsActive = user?.IsActive == true; return Task.CompletedTask; } }
-
使用AddCorsPolicyService来配置跨域
IdentityServer提供了DefaultCorsPolicyService默认跨域类。我这里使用自定义的CorsPolicyService来处理跨域,使用自定的跨域处理类只需要实现ICorsPolicyService中的Task IsOriginAllowedAsync(string origin) 即可。
为了测试我这里支架返回true允许所有,在实际中肯定是不行的,需要业务场景去配置相关origin。
private class CorsPolicyService : ICorsPolicyService { public Task<bool> IsOriginAllowedAsync(string origin) { return Task.FromResult(true); } }
3、配置IdentityServer到Ocelot中去
上面啰里啰嗦的说了那么多,也是怕园友们会学的糊里糊涂,因为我看文章都是这么过来的,学个东西要看很多文章甚至要看很多遍才能够理解清楚。
3.1 添加配置文件
不明白的可以看前面两篇文章
Consul+Ocelot搭建微服务实践–初探路由
Consul+Ocelot搭建微服务实践–负载均衡
{
"ReRoutes": [
//identityserver4 token
{
"DownstreamPathTemplate": "/connect/token",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": "5001"
}
],
"UpstreamPathTemplate": "/auth/token",
"UpstreamHttpMethod": [ "Post" ]
}
]
}
3.2 进行测试
终于可以看到你们心中怀念已久的token了。
设置Body的内容,设置的内容也是前面InMemoryConfiguration所配置的内容。
修改Body 中的值进行测试:
特意将secret的值进行修改进行测试。
IdentiyServer的日志分析:
4、总结
大部分类容都是围绕着IdentityServer的展开的,这里会为下文做一个铺垫,后面使用Ocelot集中授权认证的时候会用到。
真心的发现总结一块内容是多么的花时间,写这点东西不知不觉花了一个多小时,大多数都是记录的我自己的测试中所运用到的东西,还没有很啰里啰嗦。咋一看时间不知不觉已经十二点过了,重庆最近的雨水是真心多,从周五晚上到现在一直没有停过,并且还下的很大,敲着代码听着雨滴敲打在我家雨棚上面的声音真好听,至少还有它们陪着我。
慢慢自己将前面学的东西总结下来想想还是蛮有成就感的,心中有那么一丁点的欣慰。能够得到园子里面的园友们支持我会很高兴!
5 、附录
本系列其他文档: