• 微服务系列之授权认证(二) identity server 4


    1.简介

      IdentityServer4 是为ASP.NET Core系列量身打造的一款基于 OpenID Connect 和 OAuth 2.0 认证授权框架。
        官方文档https://identityserver4.readthedocs.io/en/latest/

        框架源码:https://github.com/IdentityServer/IdentityServer4 

      IdentityServer主要使用场景:

      1)基于中台认证中心的saas系统/pass系统的单点登录或者做为统一认证授权入口(授权模式:授权码模式Authorization Code或者混合模式hybrid);

      2)用于API服务与API服务之间的固定token通讯,或者某业务系统服务群集与其他业务系统的服务群集之间通信,或者某业务系统群集服务与中台服务群集之间通信,所使用的授权模式为客户端模式Client Credential;

      3)用于移动客户端与API服务之间通信,授权码模式为自定义授权码。

      4)用于给第三方客户端授权使用平台数据资源,类似微信、支付宝等用户授权给。主要授权模式为权码模式Authorization Code

    2.Identity Server入门demo

     新建.net core 3.1项目,nuget安装IdentityServer4,我这里是3.14版本

      

    正常来说,商业业务,Api资源、Client客户端、Identity资源、User等数据存储在数据库,token可以存储在数据库也可以存储到redis,这里为了入门演示,使用内存模式,快速搭建。

    定义一个类,创建API资源,客户端client,我们这里只使用客户端模式授权,篇幅问题,其他授权方式就不一一写了,基本都差不多

    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
    public class TestConfig
        {
            ///
            /// Api资源
            ///
            ///
            public static IEnumerable GetApiResources()
            {
                return new List
                {
                    new ApiResource(){
                        Name = "myapi",
                        ApiSecrets= new List(){
                            new Secret(){
                                Description = "secret",
                                Value = "secret".Sha256()
                            }
                        },
                        Scopes = new List(){
                            new Scope(){
                                Name = "apim"
                            }
                        }
                    },
                };
            }
     
            ///
            /// client
            ///
            ///
            public static IEnumerable GetClients()
            {
     
                return new List
                {
                   new Client()
                   {
                       ClientId="client",//定义客户端ID
                  
                       //AllowedGrantTypes = new List()
                       //{
                       //    GrantTypes.ResourceOwnerPassword.FirstOrDefault(),
                       //    GrantType.ClientCredentials,
                       //    GrantType.Hybrid
                       //},
                       //必须是单个指定授权类型,可能是内存模式问题。
                       AllowedGrantTypes = GrantTypes.ClientCredentials,
                       // 用于认证的密码
                        ClientSecrets =
                        {
                            new Secret("secret".Sha256())
                        },
                       AllowedScopes= {"apim"},
                       AccessTokenLifetime = 360000000
                   },
                 };
            }
     
            public static List GetTestUsers()
            {
                return new List
                {
                    new TestUser()
                    {
                         SubjectId = "1",
                         Username = "test",
                         Password = "123456"
                    }
                };
            }
        }  

    在启动类注入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public void ConfigureServices(IServiceCollection services)
          {
              services.AddRazorPages();
              #region 内存方式
              services.AddIdentityServer()
                  .AddDeveloperSigningCredential()//添加证书加密方式,执行该方法,会先判断tempkey.rsa证书文件是否存在,如果不存在的话,就创建一个新的tempkey.rsa证书文件,如果存在的话,就使用此证书文件。
                  .AddInMemoryApiResources(TestConfig.GetApiResources())//把受保护的Api资源添加到内存中
                  .AddInMemoryClients(TestConfig.GetClients())//客户端配置添加到内存中
                  .AddTestUsers(TestConfig.GetTestUsers())//测试的用户添加进来
              .AddDeveloperSigningCredential();
              #endregion
          }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseExceptionHandler("/Error");
                }
           
                app.UseStaticFiles();
     
                app.UseRouting();
            
    //添加中间件
             //这个必须在UseRouting和UseEndpoints中间。如果IdentityServer服务端和API端要写在一起,
                //那么这个必须在UseAuthorization和UseAuthentication的上面。
                app.UseIdentityServer();
                app.UseAuthorization();
     
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapRazorPages();
                });
            }

     然后启动服务,使用postman访问identity server 4默认的endpoint地址发现文档:

    使用identityServer4的发现文档中的token_endpoint获取token

    token已经获取了,可以使用发现文档里的introspection_endpoint来验证token

    上图可见,我们已经为client客户端,创建了一个拥有访问scope为apim权限的token

    接下来,创建一个受保护的api服务,同样创建一个.net core 3.1服务,并nuget包安装Microsoft.AspNetCore.Authentication.JwtBearer,选择3.14版本,根据.net core版本来

    在启动类中,配置认证和授权DI,和添加认证授权中间件:

    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
    public class Startup
        {
            // This method gets called by the runtime. Use this method to add services to the container.
            // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
            readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
            public void ConfigureServices(IServiceCollection services)
            {
                //配置认证
                services.AddAuthentication("Bearer")
                    .AddJwtBearer(options =>
                    {
                        options.Authority = "http://localhost:5000";//刚才启动的授权认证服务
                        options.RequireHttpsMetadata = false;
                        options.TokenValidationParameters = new TokenValidationParameters //不验证jwt的aud信息
                        {
                            ValidateAudience = false
                        };
     
                    });
                // 配置授权策略
                services.AddAuthorization(options =>
                {
                    //定义授权策略,这个名字可以随便起
                    options.AddPolicy("ApiScope", policy =>
                    {
                        policy.RequireAuthenticatedUser();
                        //
                        policy.RequireClaim("scope", "apim");//策略需要scope有apim
                    });
                    options.AddPolicy("ApiScope2", policy =>
                    {
                        policy.RequireAuthenticatedUser();
                        //
                        policy.RequireClaim("scope", "apim2");
                    });
                });
                services.AddSingleton();
                services
                .AddCors(options =>
                {
                    options.AddPolicy(MyAllowSpecificOrigins,
                    builder => builder.AllowAnyOrigin()
                    .WithMethods("GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS")
                    );
                }).AddMvc();
                services.AddControllers();
            }
     
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                app.Use((context, next) =>
                {
                    context.Request.EnableBuffering();
                    return next();
                });
                app.UseRouting();
                //跨域设置
                app.UseCors(MyAllowSpecificOrigins);
     
                //身份验证中间件 (身份验证必须在授权的前面)
                app.UseAuthentication();
     
                //授权验证中间件
                app.UseAuthorization();
     
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapControllers();
                });
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapGet("/", async context =>
                    {
                        await context.Response.WriteAsync("Hello World!");
                    });
                });
            }
        }

      写2个接口

    注意,Authorize标签可生效于类或者方法上,,根据不同的授权策略来合理安排需要保护的资源。最后,可以用刚才的token来访问这个API,,如果token错误会401,如果根据授权策略的不同,比如mytwo接口受到apiScope2策略保护,apiScope2策略需要apim2这个scope权限,因为刚才我们获取的token只包含apim这个scope权限,所以访问会返回403权限不足,大家可以去试试,我试过了就不贴图。

    至此demo结束,大家可以去试试其他模式的获取token方式

    3.IdentityServer4的数据存储

      商业级项目,授权资源是需要持久化存储的,官方已经提供了基于ef core的来维护我们授权资源和token的管理模型、上下文、仓储接口等,具体我就不写了,推荐参考这篇文章.net core 3.1 Identity Server4 (EntityFramework Core 配置) - 尘叶心繁的专栏 - TNBLOG。下面贴几张基于Identity Server4 EFCore包管理的APIResource、Client、Identity资源、Token的相关代码简介

    复制代码
    services.AddIdentityServer()
                        .AddConfigurationStore(options =>  //注入idenity相关资源上下文
                        {
                            options.ResolveDbContextOptions = (provider, builder) =>
                            {
                                builder.UseSqlServer(Configuration.GetSection("Database:ConnectString").Value,
                                    sql => sql.MigrationsAssembly(migrationsAssembly));
                            };
                        })
                        .AddOperationalStore(options =>  //注入Token管理上下文
                        {
                            options.ConfigureDbContext = builder =>
                                builder.UseSqlServer(Configuration.GetSection("Database:ConnectString").Value,
                                    sql => sql.MigrationsAssembly(migrationsAssembly));
                            options.EnableTokenCleanup = true;
                            options.TokenCleanupInterval = 3600;
                        })
                       .AddDeveloperSigningCredential();
    复制代码
    复制代码
       private ConfigurationDbContext _dbContext;
            private PersistedGrantDbContext _grantdbContext; //这个就是identity资源上下文
            private IOptions _identityOption; //这个就是token上下文
            private IMediator _mediator;
            public ClientManager(ConfigurationDbContext dbContext, IOptions identityOption, PersistedGrantDbContext grantdbContext, IMediator mediator)
            {
                _dbContext = dbContext;
                _identityOption = identityOption;
                _grantdbContext = grantdbContext;
                _mediator = mediator;
            }
    复制代码

    下面是部分Client客户端管理代码

    public async Task CreateClient(ClientEntity clientEntity)
            {
                if (_dbContext.Clients.Any(m => m.ClientName == clientEntity.ClientName))
                    throw new Exception("clientName Duplicate");
                if (_dbContext.Clients.Any(m => m.ClientId == clientEntity.ClientId))
                    throw new Exception("clientId Duplicate");
                IdentityServer4.EntityFramework.Entities.Client client = new IdentityServer4.EntityFramework.Entities.Client()
                {
                    ClientId = clientEntity.ClientId,
                    ClientSecrets = new List()
                            {
                                new IdentityServer4.EntityFramework.Entities.ClientSecret(){
                                     Value=clientEntity.Sha256Secret,
                                     Description=clientEntity.Secret
                                }
                            },
                    ClientName = clientEntity.ClientName,
                    // ClientUri = clientEntity.ClientUri,
                    Description = clientEntity.Description,
                    AccessTokenType = 1,
                    RequireConsent = clientEntity.RequireConsent,
                    AccessTokenLifetime = clientEntity.AccessTokenLifetime,
                    AllowOfflineAccess = true,
                    RedirectUris = new List(),
                    PostLogoutRedirectUris = new List(),
                    AllowedGrantTypes = new List(),
                    Claims = new List()
                };
    
                if (clientEntity.RedirectUris.Count > 0)
                {
                    foreach (var url in clientEntity.RedirectUris)
                    {
                        client.RedirectUris.Add(new IdentityServer4.EntityFramework.Entities.ClientRedirectUri()
                        {
                            RedirectUri = url
                        });
                    }
    
                }
                if (clientEntity.PostLogoutRedirectUris.Count > 0)
                {
                    foreach (var url in clientEntity.PostLogoutRedirectUris)
                    {
                        client.PostLogoutRedirectUris.Add(new IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri()
                        {
                            PostLogoutRedirectUri = url
                        });
                    }
    
                }
    
                //平台默认开放这三个类型
                var typeList = new List<string>() { "hybrid", "client_credentials", "delegation" };
                typeList.ForEach(type =>
                {
                    client.AllowedGrantTypes.Add(new IdentityServer4.EntityFramework.Entities.ClientGrantType()
                    {
                        GrantType = type
                    });
                });
    
                var res = await _dbContext.Clients.AddAsync(client);
                await _dbContext.SaveChangesAsync();
                return res.Entity;
            }
    
            public async Task UpdateClient(ClientEntity clientEntity)
            {
                if (_dbContext.Clients.Any(m => m.ClientName == clientEntity.ClientName && m.Id != clientEntity.Id))
                    throw new Exception("clientName Duplicate");
                var client = await _dbContext.Clients
                                                .Include(x => x.AllowedGrantTypes)
                                                .Include(x => x.RedirectUris)
                                                .Include(x => x.PostLogoutRedirectUris)
                                                .Include(x => x.AllowedScopes)
                                                .Include(x => x.ClientSecrets)
                                                .Include(x => x.Claims)
                                                .Include(x => x.IdentityProviderRestrictions)
                                                .Include(x => x.AllowedCorsOrigins)
                                                .Include(x => x.Properties)
                                                .FirstOrDefaultAsync(x => x.Id == clientEntity.Id);
                if (client == null)
                    throw new Exception("Client Not Exists!");
                client.ClientName = clientEntity.ClientName;
                client.Description = clientEntity.Description;
                client.AccessTokenLifetime = clientEntity.AccessTokenLifetime;
                client.RequireConsent = clientEntity.RequireConsent;
                client.Enabled = clientEntity.Enabled;
                client.RedirectUris = new List();
                client.PostLogoutRedirectUris = new List();
                client.AllowedScopes = new List();
                client.AllowedGrantTypes = new List();
    
                if (clientEntity.RedirectUris.Count > 0)
                {
                    foreach (var url in clientEntity.RedirectUris)
                    {
                        client.RedirectUris.Add(new IdentityServer4.EntityFramework.Entities.ClientRedirectUri { RedirectUri = url });
                    }
                }
    
                if (clientEntity.PostLogoutRedirectUris.Count > 0)
                {
                    foreach (var url in clientEntity.PostLogoutRedirectUris)
                    {
                        client.PostLogoutRedirectUris.Add(new IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri { PostLogoutRedirectUri = url });
                    }
    
                }
    
                foreach (string scope in clientEntity.AllowedScopes)
                {
                    client.AllowedScopes.Add(new IdentityServer4.EntityFramework.Entities.ClientScope { Scope = scope });
                }
    
                foreach (string key in clientEntity.AllowedGrantTypes)
                {
                    client.AllowedGrantTypes.Add(new IdentityServer4.EntityFramework.Entities.ClientGrantType { GrantType = key });
                }
    
                var res = _dbContext.Clients.Update(client);
                await _dbContext.SaveChangesAsync();
                return res.Entity;
            }
    View Code

     下面以部分token管理代码

     public async Task<string> GenerateToken(int id, string nickName, string projectGroup, string contact, string useReason)
            {
                var client = await _dbContext.Clients
                                                .Include(x => x.AllowedGrantTypes)
                                                .Include(x => x.RedirectUris)
                                                .Include(x => x.PostLogoutRedirectUris)
                                                .Include(x => x.AllowedScopes)
                                                .Include(x => x.ClientSecrets)
                                                .Include(x => x.Claims)
                                                .Include(x => x.IdentityProviderRestrictions)
                                                .Include(x => x.AllowedCorsOrigins)
                                                .Include(x => x.Properties)
                                                .FirstOrDefaultAsync(x => x.Id == id);
                if (client == null)
                    throw new Exception("Client Not Exists!");
                //初始化连接IdentityServer客户端,这也是人家封装好的http请求
                var discoveryClient = new DiscoveryClient(_identityOption.Value.Host)
                {
                    Policy = new DiscoveryPolicy { RequireHttps = _identityOption.Value.Https, ValidateIssuerName = false }
                };
                //获取endpint
                var discoveryResponse = await discoveryClient.GetAsync();
                //连接获取token那个endpoint
                var tokenClient = new TokenClient(discoveryResponse.TokenEndpoint,
                                                      client.ClientId,
                                                      client.ClientSecrets.FirstOrDefault().Description);
                #region 计算当前client的拥有的API资源SCOPE
                var allScopes = client.AllowedScopes.Select(p => p.Scope).ToList();
                var apiResourceScopes = new List<string>();
                _dbContext.ApiResources.Include("Scopes").ToList().ForEach(api =>
                {
                    if (api.Scopes.Count > 0)
                        apiResourceScopes.AddRange(api.Scopes.Select(p => p.Name).ToList());
                });
                var inScopes = apiResourceScopes.Intersect(allScopes);
                #endregion
                var scope = string.Join(" ", inScopes);
                //请求生成客户端模式的token
                var tokenResponse = await tokenClient.RequestCustomGrantAsync("client_credentials", scope);
    
                if (tokenResponse.IsError)
                {
                    throw new Exception(tokenResponse.Error);
                }
                else
                {
                    //发送事件
                    var _key = string.Format("{0}:reference_token", tokenResponse.AccessToken).ToSha256();
                    var tokenEntity = new TokenEntity(_key, nickName, projectGroup, contact, useReason, client.ClientId, tokenResponse.AccessToken);
                    await _mediator.Publish(new GenerateTokenEvent(tokenEntity));
                    return tokenResponse.AccessToken;
                }
            }
    View Code

    下面还有一段关于修改token过期时间的代码

     public async Task<bool> AddExpiration(string token, DateTime date)
            {
                if (!_dbContext.TokenEntities.Any(p => p.Token == token))
                    throw new Exception("manage token not exsits");
                //这个key是经过一定格式后进行sha256加密后,作为数据库表PersistedGrants一个唯一标识
                var _key = string.Format("{0}:reference_token", token).ToSha256();
                var persistedGrant = _grantdbContext.PersistedGrants.FirstOrDefault(p => p.Key == _key);
                if (persistedGrant == null)
                    throw new Exception("token不存在或者TOKEN已过期");
                var data = JObject.Parse(persistedGrant.Data);
                var creation = data.Value("CreationTime");
                var lifetime = data.Value<int>("Lifetime");
    
                data["Lifetime"] = (int)((date - creation).TotalSeconds);
    
                persistedGrant.Expiration = date;
                persistedGrant.Data = data.ToString(Newtonsoft.Json.Formatting.None);
    
                _grantdbContext.PersistedGrants.Update(persistedGrant);
                await _grantdbContext.SaveChangesAsync();
                return true;
            }
    View Code

    再来一段自定义授权模式代码

    public class DelegationGrantValidator : IExtensionGrantValidator//需要继承一下类型验证扩展接口
        {
            private readonly ITokenValidator _validator;//identity框架已实现的token验证服务,直接注入使用
    
            public DelegationGrantValidator(ITokenValidator validator)
            {
                _validator = validator;
            }
    
            public string GrantType => "delegation";//自定义的授权类型,我这实现的是一个token交换token的类型
    
            public async Task ValidateAsync(ExtensionGrantValidationContext context)
            {
                var userToken = context.Request.Raw.Get("token");//获取被交换token
    
                if (string.IsNullOrEmpty(userToken))
                {
                    context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
                    return;
                }
    
                var result = await _validator.ValidateAccessTokenAsync(userToken);//直接使用人家实现好的token验证服务验证传来的token
                if (result.IsError)
                {
                    context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
                    return;
                }
    
                //声明获取用户,如果有用户,说明要换取用户token
                var sub = result.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
                if (sub != null)
                {
                    context.Result = new GrantValidationResult(sub, GrantType);//换取用户token,
                    return;
                }
    
                // 声明中获取客户端ID,如果有clientId,说明换取客户端token
                var client_id = result.Claims.FirstOrDefault(c => c.Type == "client_id")?.Value;
                if (client_id != null)
                {
                    //context.Result = new GrantValidationResult(client_id, GrantType);
                    context.Result = new GrantValidationResult();//换取客户端token
                    return;
                }
    
                context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest);
            }
        }
    
    builder.AddExtensionGrantValidator();注入DI。
    View Code

    IdentityServer4还可以扩展endpoint,但是扩展完后,在发现文档不显示,但是可以作为http使用,以下代码截图供参考

    4.结尾

      identityServer4要写的东西实在太多,整体的把握理解还是有一定的复杂性的,我之前公司一个pass平台项目,是基于认证中心,其他业务系统实现快速集成,我当时负责的就是授权资源、token管理,还有对IDP的授权类型、endpoint一些扩展,现在总结成博客,写的不是很细,希望对后来者带来一些帮助和参考意义。

  • 相关阅读:
    【无标题】
    【VeighNa】开始量化交易——第二章:PostgreSQL数据库配置
    nodeJS--axios和fetch
    38. 多态中的静态联编和动态联编
    properties文件
    1015: 【C1】【循环】【for】整数序列的元素最大跨度值
    Linux流量监控
    Gartner:2024 年十大战略技术趋势
    SpringBoot使用spring.config.import多种方式导入配置文件
    iPortal如何灵活设置用户名及密码的安全规则
  • 原文地址:https://www.cnblogs.com/saltlight-wangchao/p/16708648.html