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();

对以上添加的内容进行解释

  1. 在实际的项目中肯定会使用自己的证书的,在这里使用AddSigningCredential添加证书。IdentityServer也友好的提供了开发者使用证书,使用AddDeveloperSigningCredential进行添加。

    使用openssl工具生成证书。

    下载地址通过我的百度云盘进行下载,因为本地直接下载可能很慢甚至可能下载不了。
    链接:https://pan.baidu.com/s/1-kf3T7iOO3PuzFRag_atZQ
    提取码:q79u

    1.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中去,详情请看上面贴的配置。

  2. 使用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)添加UserDbContext

    public 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的数据,为了后面进行测试。

  3. 使用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;
        }
    }
    
  4. 使用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 、附录

本系列其他文档:

原文地址:https://www.cnblogs.com/cqxhl/p/12993296.html