当前位置:   article > 正文

(精华)2020年9月25日 微服务 身份验证、授权(Ocelot+IdentityServer4)_ocelot oauth1

ocelot oauth1

身份验证、授权

什么是身份认证

身份认证是指当客户端访问服务端资源时,验证客户端是否合法的一种机制

什么是授权

授权就是指当客户端经过身份认证后,能够有限的访问服务端资源的一种机制

为什么要使用身份验证和授权

为了保证服务端资源的安全,我们要理解必须从真实项目中去理解

身份认证和授权方式有哪些

​ 1、Base认证

​ Base64编号认证 == https

​ 2、Digest认证

​ MD5消息摘要认证 == https

​ 3、Bearer认证

​ 就是基于token(电子身份证)认证,讲用户信息等其他信息转换成为token,然后以token进行认证

​ token认证是一种无状态的认证方式,能够无限扩展,特别适合做单点登录

​ 3.1 OAuth 2.0 ==== 授权方式的认证

​ 3.2 JWT的也是一种身份认证

​ token 分两种类型

​ 引用类型token == OAuth 2.0

​ 无用户相关信息

​ 自包含token

​ 有用户相关信息 JWT === 地址,电话,id,等

​ 在实际分布式项目中,大部分都是使用Bearer来进行身份认证和身份授权

​ 在分布式项目或者微服务项目中,为了保证系统统一登录(SSO登录),

​ 使用OpenID协议标准来规范身份认证功能 === 规范

​ 同时使用OAuth 2.0协议标准来规范权限访问 === 规范

​ 为了将身份认证(单点登录)和授权结合起来,所以出现了OpenID Connect协议标准 === 接口

​ OpenID Connect = OpenID + OAuth 2.0

​ SSO+OAuth 2.0可以实现单点登录和授权

​ IdentityServer4 也可以实现单点登录和授权

​ 然后综合实现了这些框架就是今天要讲的IdentityServer4 身份认证服务器

如何在项目中使用IdentityServer4

IdentityServer4 是什么

​ IdentityServer是基于OpenID Connect协议标准的身份认证和授权程序,它实现了OpenID Connect 和 OAuth 2.0 协议。

IdentityServer4 功能

保护你的资源
使用本地帐户或通过外部身份提供程序对用户进行身份验证
提供会话管理和单点登录
管理和验证客户机
向客户发出标识和访问令牌
验证令牌

IdentityServer4 内部概念

下面对以下几个转用术语进行解释

用户(User)
用户是使用已注册的客户端(指在id4中已经注册)访问资源的人。

客户端(Client)

客户端就是从identityserver请求令牌的软件(你可以理解为一个app即可),既可以通过身份认证令牌来验证识别用户身份,又可以通过授权令牌来访问服务端的资源。但是客户端首先必须在申请令牌前已经在identityserver服务中注册过。

实际客户端不仅可以是Web应用程序,app或桌面应用程序(你就理解为pc端的软件即可),SPA,服务器进程等。

资源(Resources)

资源就是你想用identityserver保护的东东,可以是用户的身份数据或者api资源。
每一个资源都有一个唯一的名称,客户端使用这个唯一的名称来确定想访问哪一个资源(在访问之前,实际identityserver服务端已经配置好了哪个客户端可以访问哪个资源,所以你不必理解为客户端只要指定名称他们就可以随便访问任何一个资源)。

用户的身份信息实际由一组claim组成,例如姓名或者邮件都会包含在身份信息中(将来通过identityserver校验后都会返回给被调用的客户端)。

API资源就是客户端想要调用的功能(通常以json或xml的格式返回给客户端,例如webapi,wcf,webservice),通常通过webapi来建立模型,但是不一定是webapi,我刚才已经强调可以使其他类型的格式,这个要看具体的使用场景了。

身份令牌(顾名思义用于做身份认证,例如sso其实主要就是用于身份认证) id_token jwt
一个身份令牌指的就是对认证过程的描述。它至少要标识某个用户(Called the sub aka subject claim)的主身份信息,和该用户的认证时间和认证方式。但是身份令牌可以包含额外的身份数据,具体开发者可以自行设定,但是一般情况为了确保数据传输的效率,开发者一般不做过多额外的设置,大家也可以根据使用场景自行决定。

访问令牌(用于做客户端访问授权) access_token oauth 2.0
访问令牌允许客户端访问某个 API 资源。客户端请求到访问令牌,然后使用这个令牌来访问 API资源。访问令牌包含了客户端和用户(如果有的话,这取决于业务是否需要,但通常不必要)的相关信息,API通过这些令牌信息来授予客户端的数据访问权限。

IdentityServer4 角色逻辑关系图

见图部分

微服务中使用IdentityServer4

IdentityServer4 官网地址

中文地址:http://www.identityserver.com.cn/Home/Detail/Zhengtizongshu

英文地址:https://identityserver4.readthedocs.io/en/3.1.0/

客户端认证模式

条件

1、IdentityServer4 授权服务器(IdentityServer)

2、客户端(Client)

3、微服务系统(Resources)

4、用户

步骤

1、NutGet下载IdentityServer4 在资源服务器中

1.1、在IOC容器中配置IdentityServer4

 // 1、ioc容器中添加IdentityServer4
        services.AddIdentityServer()
            .AddDeveloperSigningCredential()// 1、用户登录配置
            .AddInMemoryApiResources(Config.GetApiResources()) // 2、存储Api资源
            .AddInMemoryClients(Config.GetClients()) // 3、存储客户端(模式)
  • 1
  • 2
  • 3
  • 4
  • 5

1.2、使用IdentityServer4

// 1、使用IdentityServe4
      app.UseIdentityServer();	
      //2、添加静态资源访问
            app.UseStaticFiles();
  • 1
  • 2
  • 3
  • 4

1.3、配置测试用户Config类

   /// <summary>
    /// 1、Identity测试使用
    /// </summary>
    public class Config
    {
        /// <summary>
        /// 1、微服务API资源
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource("TeamService", "TeamService api需要被保护")
            };
        }
   /// <summary>
    /// 2、客户端
    /// </summary>
    /// <returns></returns>
    public static IEnumerable<Client> GetClients()
    {
        return new List<Client>
        {
            new Client
            {
                ClientId = "client",

                // 没有交互性用户,使用 clientid/secret 实现认证。
                AllowedGrantTypes = GrantTypes.ClientCredentials,

                // 用于认证的密码
                ClientSecrets =
                {
                    new Secret("secret".Sha256())
                },
                // 客户端有权访问的范围(Scopes)
                AllowedScopes = { "TeamService" }
            }
        };
    }
}	
  • 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

​ 2、微服务配置

​ 2.1 Nuget下载IdentityServer4.AccessTokenValidation

​ 2.2 配置身份和授权认证(配置认证资源,让资源服务器进行保护)

 // 6、校验AccessToken,从身份校验中心进行校验
            services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
                    .AddIdentityServerAuthentication(options => {
                        options.Authority = "http://localhost:5005"; // 1、授权中心地址
                        options.ApiName = "TeamService"; // 2、api名称(项目具体名称)
                        options.RequireHttpsMetadata = false; // 3、https元数据,不需要
                    });	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

​ 2.3 开启身份认证

app.UseAuthentication(); // 1、使用身份验证	
  • 1

​ 3、客户端配置

​ 3.1 Nuget下载IdentityModel

​ 3.2 调用代码即可

​ 以上默认客户端认证模式

客户端密码认证模式

条件

1、用户Test

步骤

1、在Config中添加新客户端

new Client
{
	ClientId = "client-password",
	// 使用用户名密码交互式验证
	AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

	// 用于认证的密码
	ClientSecrets =
	{
		new Secret("secret".Sha256())
	},
	// 客户端有权访问的范围(Scopes)
	AllowedScopes = { "TeamService" }
},
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

2、在config中添加test用户

/// <summary>
        /// 客户端下面的用户
        /// </summary>
        /// <returns></returns>
        public static List<TestUser> GetUsers()
        {
            return new List<TestUser>()
            {
                new TestUser
                {
                    SubjectId="1",
                    Username="tony",
                    Password="123456"
                }
            };
        }	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

3、在startup中添加配置

	// 1、ioc容器中添加IdentityServer4
            services.AddIdentityServer()
                .AddDeveloperSigningCredential()// 1、用户登录配置
                .AddInMemoryApiResources(Config.GetApiResources()) // 2、存储Api资源
                .AddInMemoryClients(Config.GetClients()) // 3、存储客户端(模式)
                .AddTestUsers(Config.GetUsers())// 4、添加登录用户(模式)	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

4、 开始调用

​ 在代码中已经写好调用过程

授权码模式认证(通过code获取assess_token)

条件

1、OpenIdConnect

步骤

1、Nuget下载Microsoft.AspNetCore.Authentication.OpenIdConnect

2、认证服务器配置

​ 2.1 在config中添加openid身份资源声明

public static IEnumerable<IdentityResource> Ids => new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile()
        };	
  • 1
  • 2
  • 3
  • 4
  • 5

​ 2.2 在config中添加客户端

// openid客户端
new Client
{
ClientId="client-code",
ClientSecrets={new Secret("secret".Sha256())},
AllowedGrantTypes=GrantTypes.Code,
RequireConsent=false,
RequirePkce=true,

RedirectUris={ "https://localhost:5006/signin-oidc"},

PostLogoutRedirectUris={ "https://localhost:5006/signout-callback-oidc"},

AllowedScopes=new List<string>{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"TeamService" // 启用对刷新令牌的支持
},

// 增加授权访问
AllowOfflineAccess=true
}	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

​ 2.3 添加openidtest用户

	// openid 身份验证
                new TestUser{SubjectId = "818727", Username = "张三", Password = "123456",
                    Claims =
                    {
                        new Claim(JwtClaimTypes.Name, "张三"),
                        new Claim(JwtClaimTypes.GivenName, "三"),
                        new Claim(JwtClaimTypes.FamilyName, "张"),
                        new Claim(JwtClaimTypes.Email, "zhangsan@email.com"),
                        new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                        new Claim(JwtClaimTypes.WebSite, "http://zhangsan.com"),
                      //  new Claim(JwtClaimTypes.Address, @"{ '城市': '杭州', '邮政编码': '310000' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json)
                    }
                }	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

​ 2.4、在startup中添加配置

 // 1、ioc容器中添加IdentityServer4
            services.AddIdentityServer()
                .AddDeveloperSigningCredential()// 1、用户登录配置
                .AddInMemoryApiResources(Config.GetApiResources()) // 2、存储Api资源
                .AddInMemoryClients(Config.GetClients()) // 3、存储客户端(模式)
                .AddTestUsers(Config.GetUsers())// 4、添加登录用户(模式)
                .AddInMemoryIdentityResources(Config.Ids); // 4、使用openid模式	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

​ 2.5 准备登陆ui页面

​ 代码中已经准备好

3、客户端配置

​ 3.1 startup.cs文件中配置

            // 1、添加身份验证
            // 我们使用cookie来本地登录用户(通过“Cookies”作为DefaultScheme),并且将DefaultChallengeScheme设置为oidc。因为当我们需要用户登录时,我们将使用OpenID Connect协议。
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
            // 添加可以处理cookie的处理程序
            .AddCookie("Cookies")
            // 用于配置执行OpenID Connect协议的处理程序
            .AddOpenIdConnect("oidc", options =>
            {
                options.Authority = "http://localhost:5005";    // 受信任令牌服务地址
                options.RequireHttpsMetadata = false;
            options.ClientId = "client-code";
            options.ClientSecret = "secret";
            options.ResponseType = "code";
            options.SaveTokens = true;  // 用于将来自IdentityServer的令牌保留在cookie中

            // 1、添加授权访问api的支持
            options.Scope.Add("TeamService");
            options.Scope.Add("offline_access");
        });	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

​ 3.2 使用配置

	// 1、使用静态页面
      app.UseStaticFiles();

// 2、开启身份验证
    app.UseAuthentication();
  • 1
  • 2
  • 3
  • 4
  • 5
IdentityServer4持久化

主要是将资源数据,用户数据,身份数据全部进行放在数据库进行存储。

条件

1、ConfigurationDbContext

​ 用于配置数据,如clients,resources和scopes。

2、PersistedGrantDbContext

​ 用于临时操作数据,如授权代码和刷新令牌

步骤

1、Nuge安装IdentityServer4.EntityFramework

​ Microsoft.EntityFrameworkCore.SqlServer

​ Microsoft.EntityFrameworkCore.Tools

2、重新配置Startup.cs文件

// 2、资源客户端持久化操作
            var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
            var connectionString = Configuration.GetConnectionString("DefaultConnection");
            services.AddIdentityServer()
                .AddConfigurationStore(options =>
                {
                    options.ConfigureDbContext = builder =>
                    {
                        builder.UseSqlServer(connectionString, options =>
                             options.MigrationsAssembly(migrationsAssembly));
                    };
                })
                .AddOperationalStore(options =>
                {
                    options.ConfigureDbContext = builder =>
                    {
                        builder.UseSqlServer(connectionString, options =>
                  options.MigrationsAssembly(migrationsAssembly));
                    };
                })
                .AddTestUsers(Config.GetUsers())
                .AddDeveloperSigningCredential();	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

3、appsettings.json中的配置

{
"ConnectionStrings": {
    "DefaultConnection": "Data Source=.;Initial Catalog=IndentityServer4;Persist Security Info=True;User ID=sa;Password=tony"
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5

4、执行迁移脚本

dotnet ef migrations add PersistedGrantDbMigration -c PersistedGrantDbContext -o Data/Migrations/IdentityServer/PersistedGrantDb
dotnet ef migrations add ConfigurationDbMigration -c ConfigurationDbContext -o Data/Migrations/IdentityServer/ConfigurationDb
  • 1
  • 2

5、迁移脚本生成数据库和表

dotnet ef database update -c PersistedGrantDbContext 
dotnet ef database update -c ConfigurationDbContext 
  • 1
  • 2

6、示例数据存储

           // 1、将config中数据存储起来
        private void InitializeDatabase(IApplicationBuilder app)
        {
            using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                serviceScope.ServiceProvider.GetService<PersistedGrantDbContext>().Database.Migrate();
           var context = serviceScope.ServiceProvider.GetService<ConfigurationDbContext>();
            context.Database.Migrate();
            if (!context.Clients.Any())
            {
                foreach (var client in Config.GetClients())
                {
                    context.Clients.Add(client.ToEntity());
                }
                context.SaveChanges();
            }

            if (!context.IdentityResources.Any())
            {
                foreach (var resource in Config.Ids)
                {
                    context.IdentityResources.Add(resource.ToEntity());
                }
                context.SaveChanges();
            }

            if (!context.ApiResources.Any())
            {
                foreach (var resource in Config.GetApiResources())
                {
                    context.ApiResources.Add(resource.ToEntity());
                }
                context.SaveChanges();
            }
        }
    }	
  • 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

6、客户端进行访问

IdentityServer4 用户进行持久化(数据存储)

条件

1、Identity

步骤

1、Nuget安装Microsoft.AspNetCore.Identity.EntityFrameworkCore

2、添加上下文类

   public class IdentityServerUserDbContext : IdentityDbContext<IdentityUser>
    {
        public IdentityServerUserDbContext(DbContextOptions<IdentityServerUserDbContext> options) : base(options)
        {
        }
   protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
    }

}	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

3、startup.cs文件配置

 services.AddDbContext<IdentityServerUserDbContext>(options =>
            {
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
            });
    // 1.1 添加用户
        services.AddIdentity<IdentityUser, IdentityRole>(options => {
            // 1.2 密码复杂度配置
            options.Password.RequireDigit = true;
            options.Password.RequiredLength = 6;
            options.Password.RequiredUniqueChars = 1;
            options.Password.RequireLowercase = false;
            options.Password.RequireNonAlphanumeric = false;
            options.Password.RequireUppercase = false;
        })
        .AddEntityFrameworkStores<IdentityServerUserDbContext>()
        .AddDefaultTokenProviders();	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

4、执行迁移脚本

	dotnet ef migrations add IdentityServerUserDbMigration -c IdentityServerUserDbContext -o Data/Migrations/IdentityServer/IdentityServerUserDb
  • 1

5、迁移脚本生成数据库和表

dotnet ef database update -c IdentityServerUserDbContext
  • 1

6、示例数据存储

// 2、将用户中数据存储起来
        private void InitializeUserDatabase(IApplicationBuilder app)
        {
            using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetService<IdentityServerUserDbContext>();
                context.Database.Migrate();
var userManager = serviceScope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();
            var idnetityUser = userManager.FindByNameAsync("tony").Result;
            if (idnetityUser == null)
            {
                idnetityUser = new IdentityUser
                {
                    UserName = "zhangsan",
                    Email = "zhangsan@email.com"
                };
                var result = userManager.CreateAsync(idnetityUser, "123456").Result;
                if (!result.Succeeded)
                {
                    throw new Exception(result.Errors.First().Description);
                }
                result = userManager.AddClaimsAsync(idnetityUser, new Claim[] {
                    new Claim(JwtClaimTypes.Name, "tony"),
                    new Claim(JwtClaimTypes.GivenName, "tony"),
                    new Claim(JwtClaimTypes.FamilyName, "tony"),
                    new Claim(JwtClaimTypes.Email, "tony@email.com"),
                    new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                    new Claim(JwtClaimTypes.WebSite, "http://tony.com")
                }).Result;

                if (!result.Succeeded)
                {
                    throw new Exception(result.Errors.First().Description);
                }
            }
        }
    }	
  • 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

6、AccountController修改Login代码

public class AccountController : Controller
{
    private UserManager<ApplicationUser> _userManager;
    private SignInManager<ApplicationUser> _signInManager;
    private IIdentityServerInteractionService _interaction;
public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager, IIdentityServerInteractionService interaction)
{
    _userManager = userManager;
    _signInManager = signInManager;
    _interaction = interaction;
}
public IActionResult Login(string returnUrl)
{
    ViewData["ReturnUrl"] = returnUrl;
    return View();
}
        [HttpPost]
public async Task<IActionResult> Login(LoginViewModel loginViewModel, string returnUrl)
{
   // check if we are in the context of an authorization request
            var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);

            // the user clicked the "cancel" button
            if (button != "login")
            {
                if (context != null)
                {
                    // if the user cancels, send a result back into IdentityServer as if they 
                    // denied the consent (even if this client does not require consent).
                    // this will send back an access denied OIDC error response to the client.
                    await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);

                    // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
                    if (await _clientStore.IsPkceClientAsync(context.ClientId))
                    {
                        // if the client is PKCE then we assume it's native, so this change in how to
                        // return the response is for better UX for the end user.
                        return this.LoadingPage("Redirect", model.ReturnUrl);
                    }

                    return Redirect(model.ReturnUrl);
                }
                else
                {
                    // since we don't have a valid context, then we just go back to the home page
                    return Redirect("~/");
                }
            }

            if (ModelState.IsValid)
            {
                var user = await _userManager.FindByNameAsync(model.Username);
                if (user == null)
                {
                    ModelState.AddModelError(nameof(model.Username), $"用户名 {model.Username} not exists");
                }
                else
                {
                    if (await _userManager.CheckPasswordAsync(user, model.Password))
                    {
                        AuthenticationProperties props = null;
                        if (model.RememberLogin)
                        {
                            props = new AuthenticationProperties
                            {
                                IsPersistent = true,
                                ExpiresUtc = DateTimeOffset.UtcNow.Add(TimeSpan.FromMinutes(30))
                            };
                        }

                        var isuser = new IdentityServerUser(user.Id)
                        {
                            DisplayName = user.UserName
                        };

                        await HttpContext.SignInAsync(isuser, props);

                        if (_interaction.IsValidReturnUrl(model.ReturnUrl))
                        {
                            return Redirect(model.ReturnUrl);
                        }
                        return Redirect("~/");
                    }
                    ModelState.AddModelError(nameof(model.Password), "密码错误");
                }
            }

            // something went wrong, show form with error
            var vm = await BuildLoginViewModelAsync(model);
            return View(vm);
}
}
  • 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
  • 91
  • 92
ocelot集成 IdentityServer4

条件

1、IdentityServer4

步骤

1、Nuget安装IdentityServer4.AccessTokenValidation

2、在startup中文件配置IndentityServer4

// 1、配置IdentityServer
var authenticationProviderKey = "OcelotKey";
            services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
                    .AddIdentityServerAuthentication(options => {
                        options.Authority = "http://localhost:5005"; // 1、授权中心地址
                        options.ApiName = "TeamService"; // 2、api名称(项目具体名称)
                        options.RequireHttpsMetadata = false; // 3、https元数据,不需要
                        options.SupportedTokens = SupportedTokens.Both;
                    });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

3、 在ocelot.json中配置身份验证

"ReRoutes": [
{
"AuthenticationOptions": {
		"AuthenticationProviderKey": "OcelotKey",
		"AllowedScopes": []
	}
}
]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
ocelot集成 IdentityServer4 配置封装

条件

1、IdentityServerOptions类

步骤

1、创建IdentityServerOptions类

/// <summary>
    /// IdentityServer的配置选项
    /// </summary>
    public class IdentityServerOptions
    {
        /// <summary>
        /// 授权服务器的地址
        /// </summary>
        public int AuthorityAddress { get; set; }
        /// <summary>
        /// access_token的类型,获取access_token的时候返回参数中的token_type一致
        /// </summary>
        public string IdentityScheme { get; set; }
        /// <summary>
        /// 资源名称,认证服务注册的资源列表名称一致,
        /// </summary>
        public string ResourceName { get; set; }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

2、配置IdentityServerOptions

// 1、配置IdentityServer 增加配置选项
var identityServerOptions = new IdentityServerOptions();
Configuration.Bind("IdentityServerOptions", identityServerOptions);
var authenticationProviderKey = "OcelotKey";
            services.AddAuthentication(identityServerOptions.IdentityScheme)
                    .AddIdentityServerAuthentication(options => {
                        options.Authority = identityServerOptions.AuthorityAddress; // 1、授权中心地址
                        options.ApiName = identityServerOptions.ResourceName; // 2、api名称(项目具体名称)
                        options.RequireHttpsMetadata = false; // 3、https元数据,不需要
                        options.SupportedTokens = SupportedTokens.Both;
                    });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
ocelot动态路由

条件

1、consul

步骤

1、动态路由配置

{
  "ReRoutes": [],
  "Aggregates": [],
  "GlobalConfiguration": {
    "RequestIdKey": null,
    "ServiceDiscoveryProvider": {
      "Host": "localhost",
      "Port": 8500,
      "Type": "Consul",
      "Token": null,
      "ConfigurationKey": null
    },
    "RateLimitOptions": {
      "ClientIdHeader": "ClientId",
      "QuotaExceededMessage": null,
      "RateLimitCounterPrefix": "ocelot",
      "DisableRateLimitHeaders": false,
      "HttpStatusCode": 429
    },
    "QoSOptions": {
      "ExceptionsAllowedBeforeBreaking": 0,
      "DurationOfBreak": 0,
      "TimeoutValue": 0
    },
    "BaseUrl": null,
    "LoadBalancerOptions": {
      "Type": "LeastConnection",
      "Key": null,
      "Expiry": 0
    },
    "DownstreamScheme": "https",
    "HttpHandlerOptions": {
      "AllowAutoRedirect": false,
      "UseCookieContainer": false,
      "UseTracing": false
    }
  }
}
  • 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

2、动态路由服务限流配置

{
  "DynamicReRoutes": [
    {
      "ServiceName": "TeamService",
      "RateLimitRule": {
        "ClientWhitelist": [],
        "EnableRateLimiting": true,
        "Period": "1s",
        "PeriodTimespan": 3000,
        "Limit": 3
      }
    }
  ],
  "GlobalConfiguration": {
    "RequestIdKey": null,
    "ServiceDiscoveryProvider": {
      "Host": "localhost",
      "Port": 8500,
      "Type": "Consul"
    },
    "RateLimitOptions": {
      "ClientIdHeader": "ClientId",
      "QuotaExceededMessage": "",
      "RateLimitCounterPrefix": "",
      "DisableRateLimitHeaders": false,
      "HttpStatusCode": 428
    },
    "DownstreamScheme": "http"
  }
}
  • 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

具体代码如下:

IdentityServer4认证中心

/// <summary>
    /// Identity测试使用
    /// </summary>
    public class Config
    {
        /// <summary>
        /// 1、微服务API资源
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource("TeamService", "TeamService api需要被保护",new List<string> {"role","admin" })
            };
        }

        /// <summary>
        /// 2、客户端
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientId = "client",

                    // 没有交互性用户,使用 clientid/secret 实现认证。
                    // 1、client认证模式
                    // 2、client用户密码认证模式
                    // 3、授权码认证模式(code)
                    // 4、简单认证模式(js)
                    AllowedGrantTypes = GrantTypes.ClientCredentials,

                    // 用于认证的密码
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },
                    // 客户端有权访问的范围(Scopes)
                    AllowedScopes = { "TeamService","MemberService" }
                },
                new Client
                {
                    ClientId = "client-password",
	                // 使用用户名密码交互式验证
	                AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

	                // 用于认证的密码
	                ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },
	                // 客户端有权访问的范围(Scopes)
	                AllowedScopes = { "TeamService" }
                },
                // openid客户端
                new Client
                {
                    ClientId="client-code",
                    ClientSecrets={new Secret("secret".Sha256())},
                    AllowedGrantTypes=GrantTypes.Code,
                    RequireConsent=false,
                    RequirePkce=true,

                    RedirectUris={ "https://localhost:5006/signin-oidc"}, // 1、客户端地址

                    PostLogoutRedirectUris={ "https://localhost:5006/signout-callback-oidc"},// 2、登录退出地址

                    AllowedScopes=new List<string>{
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    "TeamService" // 启用服务授权支持
                    },

                    // 增加授权访问
                    AllowOfflineAccess=true
                }
            };
        }

        /// <summary>
        /// 2.1 openid身份资源
        /// </summary>
        public static IEnumerable<IdentityResource> Ids => new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile()
        };

        /// <summary>
        /// 3、测试用户
        /// </summary>
        /// <returns></returns>
        public static List<TestUser> GetUsers()
        {
            return new List<TestUser>()
            {
                new TestUser
                {
                    SubjectId="1",
                    Username="tony",
                    Password="123456"
                },
                // openid 身份验证
                new TestUser{SubjectId = "818727", Username = "tony-1", Password = "123456",
                    Claims =
                    {
                        new Claim(JwtClaimTypes.Name, "tony-1"),
                        new Claim(JwtClaimTypes.GivenName, "tony-1"),
                        new Claim(JwtClaimTypes.FamilyName, "tony-1"),
                        new Claim(JwtClaimTypes.Email, "tony-1@email.com"),
                        new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                        new Claim(JwtClaimTypes.WebSite, "http://tony-1.com"),
                      //  new Claim(JwtClaimTypes.Address, @"{ '城市': '杭州', '邮政编码': '310000' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json)
                    }
                }
            };
        }

    }
  • 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
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // 1、ioc容器中添加IdentityServer4
           /* services.AddIdentityServer()
                .AddDeveloperSigningCredential()// 1、用户登录配置
                .AddInMemoryApiResources(Config.GetApiResources()) // 2、存储Api资源
                .AddInMemoryClients(Config.GetClients())// 3、存储客户端(模式)
                .AddTestUsers(Config.GetUsers())// 4、客户端用户
                .AddInMemoryIdentityResources(Config.Ids); // 5、openid身份*/


            // 2、如何将Config配置的数据持久化到Sqlserver
            // 2、资源客户端持久化操作
            var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
            var connectionString = Configuration.GetConnectionString("DefaultConnection");
            services.AddIdentityServer()
                .AddConfigurationStore(options =>
                {
                    options.ConfigureDbContext = builder =>
                    {
                        builder.UseSqlServer(connectionString, options =>
                             options.MigrationsAssembly(migrationsAssembly));
                    };
                })
                .AddOperationalStore(options =>
                {
                    options.ConfigureDbContext = builder =>
                    {
                        builder.UseSqlServer(connectionString, options =>
                  options.MigrationsAssembly(migrationsAssembly));
                    };
                })
                .AddDeveloperSigningCredential();


            // 2、用户相关的配置
            services.AddDbContext<IdentityServerUserDbContext>(options =>
            {
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
            });
            // 1.1 添加用户
            services.AddIdentity<IdentityUser, IdentityRole>(options => {
                // 1.2 密码复杂度配置
                options.Password.RequireDigit = true;
                options.Password.RequiredLength = 6;
                options.Password.RequiredUniqueChars = 1;
                options.Password.RequireLowercase = false;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireUppercase = false;
            })
            .AddEntityFrameworkStores<IdentityServerUserDbContext>()
            .AddDefaultTokenProviders();

            services.AddControllersWithViews();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // 1、初始化数据
            InitializeDatabase(app);

            // 2、初始化用户数据
            InitializeUserDatabase(app);

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
            app.UseHttpsRedirection();
            app.UseStaticFiles();

            // 1、使用IdentityServe4
            app.UseIdentityServer();

            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }


        // 1、将config中数据存储起来
        private void InitializeDatabase(IApplicationBuilder app)
        {
            using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                serviceScope.ServiceProvider.GetService<PersistedGrantDbContext>().Database.Migrate();

                var context = serviceScope.ServiceProvider.GetService<ConfigurationDbContext>();
                context.Database.Migrate();
                if (!context.Clients.Any())
                {
                    foreach (var client in Config.GetClients())
                    {
                        context.Clients.Add(client.ToEntity());
                    }
                    context.SaveChanges();
                }

                if (!context.IdentityResources.Any())
                {
                    foreach (var resource in Config.Ids)
                    {
                        context.IdentityResources.Add(resource.ToEntity());
                    }
                    context.SaveChanges();
                }

                if (!context.ApiResources.Any())
                {
                    foreach (var resource in Config.GetApiResources())
                    {
                        context.ApiResources.Add(resource.ToEntity());
                    }
                    context.SaveChanges();
                }
            }
        }

        // 2、将用户中数据存储起来
        private void InitializeUserDatabase(IApplicationBuilder app)
        {
            using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetService<IdentityServerUserDbContext>();
                context.Database.Migrate();

                var userManager = serviceScope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();
                var idnetityUser = userManager.FindByNameAsync("tony").Result;
                if (idnetityUser == null)
                {
                    idnetityUser = new IdentityUser
                    {
                        UserName = "tony",
                        Email = "tony@email.com"
                    };
                    var result = userManager.CreateAsync(idnetityUser, "123456").Result;
                    if (!result.Succeeded)
                    {
                        throw new Exception(result.Errors.First().Description);
                    }
                    result = userManager.AddClaimsAsync(idnetityUser, new Claim[] {
                        new Claim(JwtClaimTypes.Name, "tony"),
                        new Claim(JwtClaimTypes.GivenName, "tony"),
                        new Claim(JwtClaimTypes.FamilyName, "tony"),
                        new Claim(JwtClaimTypes.Email, "tony@email.com"),
                        new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                        new Claim(JwtClaimTypes.WebSite, "http://tony.com")
                    }).Result;

                    if (!result.Succeeded)
                    {
                        throw new Exception(result.Errors.First().Description);
                    }
                }
            }
        }
    }
  • 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
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181

网关层

public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                    webBuilder.ConfigureAppConfiguration((hostingContext, config) =>
                    {
                        // 1、加载ocelot配置文件
                        // config.AddJsonFile("ocelot.json");
                        config
                             .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath)
                             .AddJsonFile("appsettings.json", true, true)
                            .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true)
                            .AddJsonFile("ocelot.json", true, true) // 动态路由配置
                           // .AddOcelot(hostingContext.HostingEnvironment)
                            .AddEnvironmentVariables();
                    });
                });
                
    }
  • 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
public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // 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
        public void ConfigureServices(IServiceCollection services)
        {
            // 1、集成ids4
            var identityServerOptions = new IdentityServerOptions();
            Configuration.Bind("IdentityServerOptions", identityServerOptions);
            services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
                    .AddIdentityServerAuthentication(identityServerOptions.IdentityScheme, options =>
                    {
                        options.Authority = identityServerOptions.AuthorityAddress; // 1、授权中心地址
                        options.ApiName = identityServerOptions.ResourceName; // 2、api名称(项目具体名称)
                        options.RequireHttpsMetadata = false; // 3、https元数据,不需要
                    });

            // 1、添加网关Ocelot到ioc容器
            services.AddOcelot().AddConsul().AddPolly();
        }

        // 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();
            }
            // 2、使用网关
            app.UseOcelot((build, config) =>
            {
                build.BuildCustomeOcelotPipeline(config); // 2.1 自定义ocelot中间件完成
            }).Wait();

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });
        }
    }
  • 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
{
  "ReRoutes": [],
  "Aggregates": [],
  "DynamicReRoutes": [
    {
      "ServiceName": "TeamService",
      "RateLimitRule": {
        "ClientWhitelist": [],
        "EnableRateLimiting": true,
        "Period": "1s",
        "PeriodTimespan": 3000,
        "Limit": 3
      }
    }
  ],
  "GlobalConfiguration": {
    "RequestIdKey": null,
    "ServiceDiscoveryProvider": {
      "Host": "localhost",
      "Port": 8500,
      "Type": "Consul",
      "Token": null,
      "ConfigurationKey": null
    },
    "RateLimitOptions": {
      "ClientIdHeader": "ClientId",
      "QuotaExceededMessage": "3123123",
      "RateLimitCounterPrefix": "ocelot",
      "DisableRateLimitHeaders": false,
      "HttpStatusCode": 429
    },
    "QoSOptions": {
      "ExceptionsAllowedBeforeBreaking": 0,
      "DurationOfBreak": 0,
      "TimeoutValue": 0
    },
    "BaseUrl": null,
    "LoadBalancerOptions": {
      "Type": "LeastConnection",
      "Key": null,
      "Expiry": 0
    },
    "DownstreamScheme": "https",
    "HttpHandlerOptions": {
      "AllowAutoRedirect": false,
      "UseCookieContainer": false,
      "UseTracing": false
    },
    "AuthenticationOptions": {
      "AuthenticationProviderKey": "OcelotKey",
      "AllowedScopes": ["memberservice","teamserver"]
    }
  }
}
  • 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
/// <summary>
    /// 注册ocelot中间件
    /// </summary>
    public static class DemoOcelotExtension
    {
        public static IOcelotPipelineBuilder UseDemoResponseMiddleware(this IOcelotPipelineBuilder builder)
        {
            return builder.UseMiddleware<DemoResponseMiddleware>();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
/// <summary>
    /// 自定义ocelot中间件
    /// </summary>
    public class DemoResponseMiddleware : OcelotMiddleware
    {
        private readonly OcelotRequestDelegate _next;
        public DemoResponseMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory)
            : base(loggerFactory.CreateLogger<DemoResponseMiddleware>())
        {
            _next = next;
        }


        public async Task Invoke(DownstreamContext context)
        {
            if (!context.IsError && context.HttpContext.Request.Method.ToUpper() != "OPTIONS")
            {
                Console.WriteLine("自定义中间件");
                Console.WriteLine("自定义业务逻辑处理");
                // 1、处理统一结果
                // resutList resultMap 
                // 2、统一日志记录
                // 3、做链路监控
                // 4、性能监控
                // 5、流量统计

            }
            else
            {
                await _next.Invoke(context);
            }
        }
    }
  • 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
public static class OcelotPipelineExtensions
    {
        public static OcelotRequestDelegate BuildCustomeOcelotPipeline(this IOcelotPipelineBuilder builder,
            OcelotPipelineConfiguration pipelineConfiguration)
        {
            // This is registered to catch any global exceptions that are not handled
            // It also sets the Request Id if anything is set globally
            builder.UseExceptionHandlerMiddleware();

            // If the request is for websockets upgrade we fork into a different pipeline
            builder.MapWhen(context => context.HttpContext.WebSockets.IsWebSocketRequest,
                app =>
                {
                    app.UseDownstreamRouteFinderMiddleware();
                    app.UseDownstreamRequestInitialiser();
                    app.UseLoadBalancingMiddleware();
                    app.UseDownstreamUrlCreatorMiddleware();
                    app.UseWebSocketsProxyMiddleware();
                });

            // Allow the user to respond with absolutely anything they want.
            builder.UseIfNotNull(pipelineConfiguration.PreErrorResponderMiddleware);

            // This is registered first so it can catch any errors and issue an appropriate response
            builder.UseResponderMiddleware();

            // Then we get the downstream route information
            builder.UseDownstreamRouteFinderMiddleware();

            // This security module, IP whitelist blacklist, extended security mechanism
            builder.UseSecurityMiddleware();

            //Expand other branch pipes
            if (pipelineConfiguration.MapWhenOcelotPipeline != null)
            {
                foreach (var pipeline in pipelineConfiguration.MapWhenOcelotPipeline)
                {
                    builder.MapWhen(pipeline);
                }
            }

            // Now we have the ds route we can transform headers and stuff?
            builder.UseHttpHeadersTransformationMiddleware();

            // Initialises downstream request
            builder.UseDownstreamRequestInitialiser();

            // We check whether the request is ratelimit, and if there is no continue processing
            builder.UseRateLimiting();

            // This adds or updates the request id (initally we try and set this based on global config in the error handling middleware)
            // If anything was set at global level and we have a different setting at re route level the global stuff will be overwritten
            // This means you can get a scenario where you have a different request id from the first piece of middleware to the request id middleware.
            builder.UseRequestIdMiddleware();

            // Allow pre authentication logic. The idea being people might want to run something custom before what is built in.
            builder.UseIfNotNull(pipelineConfiguration.PreAuthenticationMiddleware);

            // Now we know where the client is going to go we can authenticate them.
            // We allow the ocelot middleware to be overriden by whatever the
            // user wants
            if (pipelineConfiguration.AuthenticationMiddleware == null)
            {
                builder.UseAuthenticationMiddleware();
            }
            else
            {
                builder.Use(pipelineConfiguration.AuthenticationMiddleware);
            }

            // The next thing we do is look at any claims transforms in case this is important for authorisation
            builder.UseClaimsToClaimsMiddleware();

            // Allow pre authorisation logic. The idea being people might want to run something custom before what is built in.
            builder.UseIfNotNull(pipelineConfiguration.PreAuthorisationMiddleware);

            // Now we have authenticated and done any claims transformation we
            // can authorise the request
            // We allow the ocelot middleware to be overriden by whatever the
            // user wants
            if (pipelineConfiguration.AuthorisationMiddleware == null)
            {
                builder.UseAuthorisationMiddleware();
            }
            else
            {
                builder.Use(pipelineConfiguration.AuthorisationMiddleware);
            }

            // Now we can run the claims to headers transformation middleware
            builder.UseClaimsToHeadersMiddleware();

            // Allow the user to implement their own query string manipulation logic
            builder.UseIfNotNull(pipelineConfiguration.PreQueryStringBuilderMiddleware);

            // Now we can run any claims to query string transformation middleware
            builder.UseClaimsToQueryStringMiddleware();

            // Get the load balancer for this request
            builder.UseLoadBalancingMiddleware();

            // This takes the downstream route we retrieved earlier and replaces any placeholders with the variables that should be used
            builder.UseDownstreamUrlCreatorMiddleware();

            // Not sure if this is the best place for this but we use the downstream url
            // as the basis for our cache key.
            builder.UseOutputCacheMiddleware();

            //We fire off the request and set the response on the scoped data repo
            builder.UseHttpRequesterMiddleware();

            //添加自定义测试中间件
            builder.UseDemoResponseMiddleware();

            return builder.Build();
        }

        private static void UseIfNotNull(this IOcelotPipelineBuilder builder,
            Func<DownstreamContext, Func<Task>, Task> middleware)
        {
            if (middleware != null)
            {
                builder.Use(middleware);
            }
        }
    }
  • 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
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
/// <summary>
    /// IdentityServer的配置选项
    /// </summary>
    public class IdentityServerOptions
    {
       /// <summary>
        /// 授权服务器的地址
        /// </summary>
        public string AuthorityAddress { get; set; }
        /// <summary>
        /// access_token的类型,获取access_token的时候返回参数中的token_type一致
        /// </summary>
        public string IdentityScheme  { get; set; }
        /// <summary>
        /// 资源名称,认证服务注册的资源列表名称一致,
        /// </summary>
        public string ResourceName { get; set; }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

客服端

public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
             // 1、添加身份验证
            // 我们使用cookie来本地登录用户(通过“Cookies”作为DefaultScheme),并且将DefaultChallengeScheme设置为oidc。因为当我们需要用户登录时,我们将使用OpenID Connect协议。
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc"; // openid connect
            })
            // 添加可以处理cookie的处理程序
            .AddCookie("Cookies")
            // 用于配置执行OpenID Connect协议的处理程序
            .AddOpenIdConnect("oidc", options =>
            {
                // 1、生成id_token
                options.Authority = "http://localhost:5005";    // 受信任令牌服务地址
                options.RequireHttpsMetadata = false;
                options.ClientId = "client-code";
                options.ClientSecret = "secret";
                options.ResponseType = "code";
                options.SaveTokens = true;  // 用于将来自IdentityServer的令牌保留在cookie中
               
                // 1、添加授权访问api的支持(access_token)
                options.Scope.Add("TeamService");
                options.Scope.Add("offline_access");
            });

            services.AddControllersWithViews();
        }

        // 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();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
            app.UseHttpsRedirection();

            // 1、增加样式
            app.UseStaticFiles();
            
            app.UseRouting();
            app.UseAuthentication(); // 1、添加身份认证
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}").RequireAuthorization();
            });
        }
    }
  • 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
/// <summary>
    /// MVC客户端
    /// </summary>
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }

        public async Task<IActionResult> Index()
        {
            #region token模式 
            {
               /* // 1、生成AccessToken
                // 1.1 客户端模式
                // 1.2 客户端用户密码模式
                // 1.3 客户端code状态码模式
                string access_token = await GetAccessToken();

                // 2、使用AccessToken 进行资源访问
                string result = await UseAccessToken(access_token);

                // 3、响应结果到页面
                ViewData.Add("Json", result);*/
            }
            #endregion

            #region openid connect 协议
            {
                // 1、获取token(id_token , access_token ,refresh_token)
                var accessToken = await HttpContext.GetTokenAsync("access_token"); // ("id_token")
                Console.WriteLine($"accessToken:{accessToken}");
                // var refreshToken =await HttpContext.GetTokenAsync("refresh_token");
                var client = new HttpClient();
                client.SetBearerToken(accessToken);
                /*client.DefaultRequestHeaders.Authorization =
                    new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);*/

                // 2、使用token
                var result = await client.GetStringAsync("https://localhost:5004/TeamService/teams");// ==== https://localhost:5001/teams

                // 3、响应结果到页面
                ViewData.Add("Json", result);
            }
            #endregion
           
            return View();
        }

        /// <summary>
        /// 1、生成token
        /// </summary>
        /// <returns></returns>
        public static async Task<string> GetAccessToken()
        {
            //1、建立连接
            HttpClient client = new HttpClient();
            DiscoveryDocumentResponse disco = await client.GetDiscoveryDocumentAsync("http://localhost:5005");
            if (disco.IsError)
            {
                Console.WriteLine($"[DiscoveryDocumentResponse Error]: {disco.Error}");
            }
            // 1.1、通过客户端获取AccessToken
           /* TokenResponse tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
            {
                Address = disco.TokenEndpoint, // 1、生成AccessToken中心
                ClientId = "client", // 2、客户端编号
                ClientSecret = "secret",// 3、客户端密码
                Scope = "TeamService" // 4、客户端需要访问的API
            });*/

            // 1.2 通过客户端用户密码获取AccessToken
            TokenResponse tokenResponse = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
            {
                Address = disco.TokenEndpoint,
                ClientId = "client-password",
                ClientSecret = "secret",
                Scope = "TeamService",
                UserName = "tony",
                Password = "123456"
            });

            // 1.3 通过授权code获取AccessToken[需要进行登录]
            /* TokenResponse tokenResponse = await client.RequestAuthorizationCodeTokenAsync
                 (new AuthorizationCodeTokenRequest
                 {
                     Address = disco.TokenEndpoint,
                     ClientId = "client-code",
                     ClientSecret = "secret",
                     Code = "12",
                     RedirectUri = "http://localhost:5005"

                 });*/
            if (tokenResponse.IsError)
            {
                //ClientId 与 ClientSecret 错误,报错:invalid_client
                //Scope 错误,报错:invalid_scope
                //UserName 与 Password 错误,报错:invalid_grant
                string errorDesc = tokenResponse.ErrorDescription;
                if (string.IsNullOrEmpty(errorDesc)) errorDesc = "";
                if (errorDesc.Equals("invalid_username_or_password"))
                {
                    Console.WriteLine("用户名或密码错误,请重新输入!");
                }
                else
                {
                    Console.WriteLine($"[TokenResponse Error]: {tokenResponse.Error}, [TokenResponse Error Description]: {errorDesc}");
                }
            }
            else
            {
                Console.WriteLine($"Access Token: {tokenResponse.Json}");
                Console.WriteLine($"Access Token: {tokenResponse.RefreshToken}");
                Console.WriteLine($"Access Token: {tokenResponse.ExpiresIn}");
            }
            return tokenResponse.AccessToken;
        }

        /// <summary>
        /// 2、使用token
        /// </summary>
        public static async Task<string> UseAccessToken(string AccessToken)
        {
            HttpClient apiClient = new HttpClient();
            apiClient.SetBearerToken(AccessToken); // 1、设置token到请求头
            HttpResponseMessage response = await apiClient.GetAsync("https://localhost:5001/teams");
            if (!response.IsSuccessStatusCode)
            {
                Console.WriteLine($"API Request Error, StatusCode is : {response.StatusCode}");
            }
            else
            {
                string content = await response.Content.ReadAsStringAsync();
                Console.WriteLine("");
                Console.WriteLine($"Result: {JArray.Parse(content)}");

                // 3、输出结果到页面
                return JArray.Parse(content).ToString();
            }
            return "";
            
        }

        public IActionResult Privacy()
        {
            return View();
        }

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
  • 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
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157

服务端

public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // 1、注册上下文到IOC容器
            services.AddDbContext<TeamContext>(options => {
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
            });
            // 2、注册团队service
            services.AddScoped<ITeamService, TeamServiceImpl>();

            // 3、注册团队仓储
            services.AddScoped<ITeamRepository, TeamRepository>();

            // 4、添加映射
            //services.AddAutoMapper();

            // 5、添加服务注册条件
            services.AddConsulRegistry(Configuration);

            // 6、校验AccessToken,从身份校验中心进行校验
            /*services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
                    .AddIdentityServerAuthentication(options =>
                    {
                        options.Authority = "http://localhost:5005"; // 1、授权中心地址
                        options.ApiName = "TeamService"; // 2、api名称(项目具体名称)
                        options.RequireHttpsMetadata = false; // 3、https元数据,不需要
                    });*/

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

            // 1、consul服务注册
            app.UseConsulRegistry();

            app.UseRouting();

          //  app.UseAuthentication();// 1、开启身份验证
            app.UseAuthorization();// 2、使用授权

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
  • 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
/// <summary>
    /// 团队微服务api
    /// </summary>
    [Route("Teams")]
  //  [Authorize] // 1、保护起来
    [ApiController]
    public class TeamsController : ControllerBase
    {
        private readonly ITeamService teamService;

        public TeamsController(ITeamService teamService)
        {
            this.teamService = teamService;
        }

        // GET: api/Teams
        [HttpGet]
        public ActionResult<IEnumerable<Team>> GetTeams()
        {
            // Thread.Sleep(10000000);
            // 1、演示宕机
            Console.WriteLine($"查询团队信息");
            return teamService.GetTeams().ToList();
        }

        // GET: api/Teams/5
        [HttpGet("{id}")]
        public ActionResult<Team> GetTeam(int id)
        {
            Team team = teamService.GetTeamById(id);

            if (team == null)
            {
                return NotFound();
            }
            return team;
        }

        // PUT: api/Teams/5
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for
        // more details see https://aka.ms/RazorPagesCRUD.
        [HttpPut("{id}")]
        public IActionResult PutTeam(int id, Team team)
        {
            if (id != team.Id)
            {
                return BadRequest();
            }

            try
            {
                teamService.Update(team);
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!teamService.TeamExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        // POST: api/Teams
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for
        // more details see https://aka.ms/RazorPagesCRUD.
        [HttpPost]
        public ActionResult<Team> PostTeam(Team team)
        {
            teamService.Create(team);

            return CreatedAtAction("GetTeam", new { id = team.Id }, team);
        }

        // DELETE: api/Teams/5
        [HttpDelete("{id}")]
        public ActionResult<Team> DeleteTeam(int id)
        {
            var team = teamService.GetTeamById(id);
            if (team == null)
            {
                return NotFound();
            }

            teamService.Delete(team);
            return team;
        }
    }
  • 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
  • 91
  • 92
  • 93
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/224629
推荐阅读
相关标签
  

闽ICP备14008679号