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  阅读(120)  评论(0编辑  收藏  举报