Identity Server4 笔记




一.角色

1.身份认证服务器

简而言之,他就是一个向客户端发送安全令牌的软件。

身份认证服务器的主要功能

  • 保护您的资源
  • 使用本地帐户存储或通过外部身份提供商对用户进行身份验证(例如微信登录)
  • 提供会话管理和单点登录
  • 管理和认证客户
  • 向客户端发布身份和访问令牌
  • 验证令牌

主要支持的协议

  • OpenID Connect
  • OAuth 2.0

2.用户

使用客户端的人

3.客户端

客户端是一种从IdentityServer请求令牌的软件-用于认证用户(请求身份令牌)或访问资源(请求访问令牌)。客户端必须先向授权服务器注册,然后才能请求令牌。

客户端可以是 web 应用程序,或者桌面应用程序,或移动程序


4. 资源

ID4 要保护的 API 或 身份数据 (因为我主要是webapi,所以后边主要使用 webapi 举例子)
每一个 API 都有一个独一无二的名字,客户端使用这个名字来指定他要访问的是哪个 API。


5.Identity Token(身份令牌)

表示身份认证完成后的结果

他至少包括用户的身份信息,什么时候和怎么样去使用权限。

6.Access Token(访问令牌)

Access Token 是用来获取 API 的。客户端接受 access token,并将将她专送给 受保护的 API。

Access Token 里面包含客户端和使用者的信息(如果有的话),

如何理解 Identity token 和 Access Token 的关系?
可以把 Identity token 想象成在门卫那里领到的,证明你身份的条,而 Access Token是你可以 要进入 API 的钥匙 。(这是我自己的理解,不一定准确)


id4官方给的例子 https://demo.identityserver.io/

网址 https://gitee.com/rush_peng/identity-server4-example.git




二.客户端凭据的认证方式

没有用户参与的程序。使用的是 Client Credentials(客户端证书)这种授权方式。

里面用到的三个项目的 nuget 包是不一样的。

  • IdentityServer : IdentityServer4
  • API :JwtBearer (解析jwt文件)
  • Client : IdentityModel (方便寻找 identityserver 的 api 接口的)

1.建立 IdentityServer4

1.用类的方式,声明配置文佳

 public static class Config
    {
        //定义API
        public static IEnumerable<ApiScope> ApiScopes =>
            new List<ApiScope>
            {
                  new ApiScope("api1", "My API")
            };


        //定义客户端

        // 定义访问 API 的客户端应用
        // 这种情况下,客户端没有交互式的用户,只能通过客户端密钥进行身份验证。

        // ClientId 和 Client Secret 可以视为登录名和密码。让身份认证服务器知道是哪个用户。
        public static IEnumerable<Client> Clients => new List<Client>
         {
        new Client
        {
            // 定义一个客户端
            ClientId = "client",

            // 认证类型。
            //使用客户端密钥的方式进行验证。

            AllowedGrantTypes = GrantTypes.ClientCredentials,

            // 认证的密码
            ClientSecrets =
            {
                new Secret("secret".Sha256())
            },
            // 这个客户端端可以访问的 API
            AllowedScopes = { "api1" }
        }
    };

2.在 Startup 里面做调用设置

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

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
              services.AddControllers();
            var builder = services.AddIdentityServer()
                  .AddInMemoryApiScopes(Config.ApiScopes) // 那个api可以使用
                  .AddInMemoryClients(Config.Clients)// 哪个client可以使用
                  .AddDeveloperSigningCredential() ;//解决连接不上的问题,实际中,签名需要一对公钥和私钥,他会帮我们将公钥和私钥存储到硬盘上
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseRouting();
            app.UseIdentityServer();//使用 Identity 4

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

重生成,而使用命令行运行一下

dotnet … --urls http://*:5001

2.建立受保护的API

1.建立要访问受保护的 Action


    [ApiController]
    public class IdentityController : ControllerBase
    {
        [HttpGet("identity")]
        [Authorize]
        public IActionResult Get()
        {
            return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
        }

        [Authorize]
        [HttpGet("test")]
        public string Getst()
        {
            return "受保护的 API 访问成功";
        }
    }

2.在startup 里面进行配置

  public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            //接受授服务器发送的任何访问令牌。
            services.AddAuthentication("Bearer")
           .AddJwtBearer("Bearer", options =>
           {
               options.Authority = "https://localhost:5001";

               options.TokenValidationParameters = new TokenValidationParameters
               {
                   ValidateAudience = false
               };
           });

           // 允许检测客户端请求的令牌中是否存在作用域
            services.AddAuthorization(options =>
            {
                options.AddPolicy("ApiScope", policy =>
                {
                    policy.RequireAuthenticatedUser();
                    policy.RequireClaim("scope", "api1");
                });
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();
            app.UseAuthentication();//每次都要执行身份验证.
            app.UseAuthorization(); //授权,确保匿名客户端无法访问我们的 api 端点

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

3.建立用来访问的客户端

  public class Program
    {
  
        // IdentityModel包括一个与发现端点一起使用的客户端库。这样,您只需要知道IdentityServer的基地址-可以从元数据中读取实际的端点地址:
      
        //找到授权的服务器
        public static async System.Threading.Tasks.Task Main(string[] args)
        {

            //找到授权服务器
            var client = new HttpClient();
            var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5001");  //异步的方法名前边必须声明 async
            if (disco.IsError)
            {
                Console.WriteLine($"连接失败****{disco.Error}");
                return;
            }


            //向授权服务器请求 token
            var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
            {
                Address = disco.TokenEndpoint,

                ClientId = "client",
                ClientSecret = "secret",
                Scope = "api1"
            });
            if (tokenResponse.IsError)
            {
                Console.WriteLine("token返回错误", tokenResponse.Error);
                return;
            }
            Console.WriteLine(tokenResponse.Json);


            //将访问令牌发送给 API
            var apiClient = new HttpClient();
            apiClient.SetBearerToken(tokenResponse.AccessToken);

            var response = await apiClient.GetAsync("http://localhost:6001/test");
            if (!response.IsSuccessStatusCode)
            {
                Console.WriteLine(response.StatusCode);
                Console.WriteLine("访问失败");
            }
            else
            {
                var content = await response.Content.ReadAsStringAsync();
                Console.WriteLine("访问成功");
                // Console.WriteLine(JArray.Parse(content));
                Console.WriteLine(content);
            }

            Console.ReadLine();
        }

具体查看gitee代码:

4.保护WPF客户端

这种是有用户参与的,使用的是 Password Grant (用名名密码)这种授权方式。

1.流程

运行的顺序和字母的顺序是一致的:

  • 浏览器要将请求传到用户代理
  • 用户代理再连接 身份认证服务器,并将返回信息返回给用户代理,由用户决定要不要授权。
  • 如果用户点了同意,那么,用户代理 就会再跳回浏览器,并带有 Authorization Code 。
  • 然后浏览器带着 Authorization code 到 身份认证服务器。请求 access token。

所以在这个过程之中,即对用户进行身份认证,也对浏览器进行身份认证。

在这里插入图片描述

具体操作看代码吧。我也没用过MVC



2.为MVC刷新 token

使用 Refresh Token 刷新 Access Token。

1.在MVC 客户端进行设置
 new Client
                {
                    ClientId = "mvc client",
                    ClientName = "ASP.NET Core MVC Client", // 随便写

                    AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,//允许两种授权方式.
                    ClientSecrets = { new Secret("mvc secret".Sha256()) },//密码.


                    //下边都是固定的地址。
                    RedirectUris = { "http://localhost:5002/signin-oidc" },
                    FrontChannelLogoutUri = "http://localhost:5002/signout-oidc",
                    PostLogoutRedirectUris = 
                    { "http://localhost:5002/signout-callback-oidc" },
                    
                    AlwaysIncludeUserClaimsInIdToken = true,

                    AllowOfflineAccess = true, // offline_access

                    //默认是一个小时。现在改成 60s 。
                    AccessTokenLifetime = 60, // 60 seconds

                    AllowedScopes =
                    {
                        "api1",
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Email,
                        IdentityServerConstants.StandardScopes.Address,
                        IdentityServerConstants.StandardScopes.Phone,
                        IdentityServerConstants.StandardScopes.Profile
                    }
                },

但是仅仅这样设置,过了一分钟以后,进入 MVC ,依然还能得到 API 的相关信息 。这是因为我们的 API 没有设置对于 对于token检验。


2.对受保护的 API进行设置
        services.AddAuthentication("Bearer")//bearer 的授权方式。
                .AddJwtBearer("Bearer", options =>
                {
                    options.Authority = "https://localhost:5000";
                    
                    options.RequireHttpsMetadata = false;//不需要https

                    options.TokenValidationParameters = new         TokenValidationParameters
                    {
                        ValidateAudience = false
                    };

                    //每隔一分钟,验证一次这个token
                    options.TokenValidationParameters.RequireExpirationTime = true;
                    
                    //必须要有超时时间这个参数。
                    options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(1);

                });

3.刷新 Token

在 MVC 的控制器里面,再添加一个 action ,用来刷新 Token。

   //刷新 token
        private async Task<string> RenewTokensAsync()
        {
            //发现文档
            var client = new HttpClient();
            var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
            if (disco.IsError)
            {
                throw new Exception(disco.Error);
            }


            //获取 refreshToken
            var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);

            //请求token
            var tokenResponse = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
            {
                Address = disco.TokenEndpoint,
                ClientId = "mvc client",
                ClientSecret = "mvc secret",
                Scope = "api1 openid profile email phone address",
                GrantType = OpenIdConnectGrantTypes.RefreshToken,
                RefreshToken = refreshToken
            });

            if (tokenResponse.IsError)
            {
                throw new Exception(tokenResponse.Error);
            }

            //超时时间,使用的是 UTC 时间。
            var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn);

            // 把 token 数组全部集中起来,做成一个数组。
            var tokens = new[]
            {
                new AuthenticationToken
                {
                    Name = OpenIdConnectParameterNames.IdToken,
                    Value = tokenResponse.IdentityToken
                },
                new AuthenticationToken
                {
                    Name = OpenIdConnectParameterNames.AccessToken,
                    Value = tokenResponse.AccessToken
                },
                new AuthenticationToken
                {
                    Name = OpenIdConnectParameterNames.RefreshToken,
                    Value = tokenResponse.RefreshToken
                },
                new AuthenticationToken
                {
                    Name = "expires_at",
                    Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
                }
            };

            // 获取身份认证的结果,包含当前的pricipal和properties
            var currentAuthenticateResult =
                await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);

            // 把获得的token再更新一遍。
            currentAuthenticateResult.Properties.StoreTokens(tokens);

            // 再进行以下登录动作.
            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
                currentAuthenticateResult.Principal, currentAuthenticateResult.Properties);
            // 再把 access token 返回过去.
            return tokenResponse.AccessToken;
        }

在前边用到token的地方,再更新一下这个方法:

 var response = await client.GetAsync("http://localhost:5001/identity");
            if (!response.IsSuccessStatusCode)
            {
                if (response.StatusCode == HttpStatusCode.Unauthorized)
                {
                    await RenewTokensAsync();
                    return RedirectToAction();
                }

                throw new Exception(response.ReasonPhrase);
            }

5.Implicit Flow

  • 用于公共客户端 ,比如用 Angular 这种做的。CS 的文件。如果有些保密文件,可以很容易的获取。
  • 最好是客户端可以直接从浏览器 访问资源。
  • 没有显示的客户端身份认证。

6. Hybrid Flow

  • 访问被保护资源
  • 刷新 Access Token
  • 处理 Claims
  • 基于策略的权限

使用 Hybrid 保护 api 资源。

之前使用的 OpenId Connect 隐式流,所有的令牌都是通过浏览器来传输,这对于 ID token 来说当然没有问题,但是我们还想请求一个 access token,accesstoken 更加敏感,所以在没有必要的时候,我们不会想把他暴露给外部世界, OpenID Connect 包含了一个叫做 “混合流(Hybrid flowe)”,它为我们提供了两方面的优点,身份令牌通过浏览器频道来传输,这样客户端就能能够在做任何工作前验证他,如果验证成功,客户端就会打开一个后端通道,来链接令牌,以检索访问令牌。

1.客户端类型

  1. 机密客户端:这种客户端有能力维护其凭据的机密性。位于服务端,例如服务器端的 Web 应用,例如 MVC。
  2. 公开客户端:这种客户端无法维持其凭据的机密性,位于客户端设备,例如 JS 应用,移动应用,原生应用和软件等。

在这里插入图片描述


OAuth2.和相比OpenID 就是相当于多出来一个 hybirid Flow


在这里插入图片描述




在这里插入图片描述

2.返回类型

根据 respons_type 的不同,分为三种情况:

  • response_type = code Id_token
  • response_type = code token
  • response_type = code id_token token

在这里插入图片描述

3.角色和策略

  • 角色:规定几个角色,然后每个角色里面添加若干个用户。一个用户可以处于不同的角色。
  • 通过策略授予权限,可以将多个属性后者
3.1基于角色

在这里插入图片描述


在注册的用户里面直接声明 管理员角色

使用的时候,直接声明;

       [Authorize(Roles ="管理员,普通用户")]
        [HttpGet("test")]
        public string Getst()
        {
            return "受保护的 API 访问成功";
        }
3.2基于策略

比如:定义把名字为Smith,地址为Somewhere的作为条件。

总的来讲,基于策略的更加的灵活,我们可以自己定义:比如只让 firstname== “ 张 ”的人登录

在客户端声明:


       services.AddAuthorization(options =>
          {
             options.AddPolicy("SmithInSomewhere", builder =>
             {
               builder.RequireAuthenticatedUser();
               builder.RequireClaim(JwtClaimTypes.FamilyName, "Smith");
               builder.RequireClaim("location", "somewhere");
             });  

在API调用

  [Authorize(Policy ="sithInSomewhere")]
        public IActionResult Index()
        {
            var user = User.Identity;
            return View();
        }

4.使用混合流保护客户端


参考文献

[1] Identity4 官方文档
[2] identityserver4
[3] 杨旭视频
[4] identityServer4 中文文档

posted @ 2020-08-26 15:24  沧海一声笑rush  阅读(171)  评论(0)    收藏  举报