IdentityServer4 (1) 客户端授权模式(Client Credentials)

写在前面

1、源码(.Net Core 2.2)

  git地址:https://github.com/yizhaoxian/CoreIdentityServer4Demo.git

2、相关章节

  2.1、《IdentityServer4 (1) 客户端授权模式(Client Credentials)
  2.2、《IdentityServer4 (2) 密码授权(Resource Owner Password)
  2.3、《IdentityServer4 (3) 授权码模式(Authorization Code)
  2.4、《IdentityServer4 (4) 静默刷新(Implicit)
  2.5、《IdentityServer4 (5) 混合模式(Hybrid)

3、参考资料

  IdentityServer4 中文文档 http://www.identityserver.com.cn/
  IdentityServer4 英文文档 https://identityserver4.readthedocs.io/en/latest/

4、流程图

  客户端授权模式是最基本的使用场景,我们需要做一个API(受保护的资源),一个客户端(访问的应用),一个IdentityServer(用来授权)

  

一、创建IdentityServer

1、用VS创建一个Web 项目

  

2、添加引用 IdentityServer4 包,下图是我已经安装好了的截图

3、添加一个配置文件(这里也可以使用json文件)

    public class IdpConfig
    {
        /// <summary>
        /// 用户认证信息
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<IdentityResource> GetApiResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Address(),
                new IdentityResources.Email(),
                new IdentityResources.Phone()
            };
        }
        /// <summary>
        /// API 资源
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<ApiResource> GetApis()
        {
            return new List<ApiResource>
            {
                new ApiResource("api1", "My API") 
            };
        }

        /// <summary>
        /// 客户端应用
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    // 客户端ID 这个很重要
                    ClientId = "client",
                    //AccessToken 过期时间,默认3600秒,注意这里直接设置5秒过期是不管用的,解决方案继续看下面 API资源添加JWT
                    //AccessTokenLifetime=5, 
                    // 没有交互性用户,使用 clientid/secret 实现认证。
                    AllowedGrantTypes = GrantTypes.ClientCredentials, 
                    // 用于认证的密码
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },
                    // 客户端有权访问的范围(Scopes)
                    AllowedScopes = { "api1" }
                }
            };
        }
    }

4、在StartUp.cs 里注册 IdentityServer4 

  ConfigureServices()

    services.AddIdentityServer(options =>
    {
        options.Events.RaiseErrorEvents = true;
        options.Events.RaiseInformationEvents = true;
        options.Events.RaiseFailureEvents = true;
        options.Events.RaiseSuccessEvents = true;
    })
      .AddDeveloperSigningCredential()//解决Keyset is missing 错误
      //.AddTestUsers(TestUsers.Users)
      //.AddInMemoryIdentityResources(IdpConfig.GetApiResources())
      .AddInMemoryApiResources(IdpConfig.GetApis())
      .AddInMemoryClients(IdpConfig.GetClients());

  Configure()方法添加使用 IdentityServer4 中间件

app.UseIdentityServer();

5、配置完成

  启动项目,访问 http://localhost:5002/.well-known/openid-configuration (我的端口号是5002) ,可以浏览 发现文档,参考下图,说明已经配置成功。

  后面客户端会使用里面的数据进行请求toke

  项目第一次启动根目录也会生成一个文件 tempkey.rsa

   

二、客户端

1、新建一个.Net Core Web 项目

  这里可以使用其他建立客户端 。例如:控制台程序、wpf 等等。需要添加 NuGet 包 IdentityModel

  

2、新建一个 Controller 用来测试访问上面的IdentityServer

  获取token,访问 http://localhost:5003/Idp/token ,提示访问成功

    public class IdpController : Controller
    { 
        private static readonly string _idpBaseUrl = "http://localhost:5002"; 
        public async Task<IActionResult> Token()
        {
            var client = new HttpClient();
            var disco = await client.GetDiscoveryDocumentAsync(_idpBaseUrl);
            if (disco.IsError)
            {
                return Content("获取发现文档失败。error:" + disco.Error);
            }
            var token = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest()
            {
                Address = disco.TokenEndpoint,
                //ClientId、ClientSecret、Scope 这里要和 API 里定义的Client一模一样
                ClientId = "client",
                ClientSecret = "secret",
                Scope = "api1"
            });
            if (token.IsError)
            {
                return Content("获取 AccessToken 失败。error:" + disco.Error);
            } 
            return Content("获取 AccessToken 成功。Token:" + token.AccessToken);
        }  
    }

三、添加API资源

1、新建一个API项目

  我把API项目和IdentityServer 放到同一个解决方案,这个自己定,无所谓的

  API资源指的是IdentityServer IdpConfig.GetApis() 里面添加的 api1(这个api1名称随便起,但是要注意一定要保持一致)

  添加认证之后就可以测试用 AccessToken 请求资源了

2、添加JWT 认证

  StartUp.ConfigureServices()

       services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
       .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
       {
           // IdentityServer 地址
           options.Authority = "http://localhost:5002";
           //不需要https
           options.RequireHttpsMetadata = false;
           //这里要和 IdentityServer 定义的 api1 保持一致
           options.Audience = "api1";
           //token 默认容忍5分钟过期时间偏移,这里设置为0,
           //这里就是为什么定义客户端设置了过期时间为5秒,过期后仍可以访问数据
           options.TokenValidationParameters.ClockSkew = TimeSpan.Zero;
           options.Events = new JwtBearerEvents
           {
               //AccessToken 验证失败
               OnChallenge = op =>
               {
                   //跳过所有默认操作
                   op.HandleResponse();
                   //下面是自定义返回消息
                   //op.Response.Headers.Add("token", "401");
                   op.Response.ContentType = "application/json";
                   op.Response.StatusCode = StatusCodes.Status401Unauthorized;
                   op.Response.WriteAsync(JsonConvert.SerializeObject(new
                   {
                       status = StatusCodes.Status401Unauthorized,
                       msg = "token无效"
                   }));
                   return Task.CompletedTask;
               }
           };
       });

3、添加认证中间件

//这里注意 一定要在 UseMvc前面,顺序不可改变
app.UseAuthentication();

4、Controller 添加特性认证 [Authorize]

    [Route("api/[controller]")]
    [Authorize] 
    public class SuiBianController : Controller
    {
        [HttpGet]
        public string Get()
        {
            var roles = User.Claims.Where(l => l.Type == ClaimTypes.Role);
            return "访问成功,当前用户角色 " + string.Join(',', roles.Select(l => l.Value));
        }
    }

5、测试

  访问 http://localhost:5001/api/suibian ,提示 token 无效,证明我们增加认证成功

  

四、客户端测试

1、修改 IdpController, 添加一个action 访问 API资源 /api/suibian

    public class IdpController : Controller
    {
        //内存缓存 需要提前注册  services.AddMemoryCache();
        private IMemoryCache _memoryCache;
        private static readonly string _idpBaseUrl = "http://localhost:5002";
        private static readonly string _apiBaseUrl = "http://localhost:5001";
        public IdpController(IMemoryCache memoryCache)
        {
            _memoryCache = memoryCache;
        }
        public async Task<IActionResult> Token()
        {
            var client = new HttpClient();
            var disco = await client.GetDiscoveryDocumentAsync(_idpBaseUrl);
            if (disco.IsError)
            {
                return Content("获取发现文档失败。error:" + disco.Error);
            }
            var token = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest()
            {
                Address = disco.TokenEndpoint,
                ClientId = "client",
                ClientSecret = "secret",
                Scope = "api1"
            });
            if (token.IsError)
            {
                return Content("获取 AccessToken 失败。error:" + disco.Error);
            }
            //将token 临时存储到 缓存中
            _memoryCache.Set("AccessToken", token.AccessToken);
            return Content("获取 AccessToken 成功。Token:" + token.AccessToken);
        }

        public async Task<IActionResult> SuiBian()
        {
            string token, apiurl = GetApiUrl("suibian");
            _memoryCache.TryGetValue("AccessToken", out token);
            if (string.IsNullOrEmpty(token))
            {
                return Content("token is null");
            }
            var client = new HttpClient();
            client.SetBearerToken(token);
            var response = await client.GetAsync(apiurl);
            var result = await response.Content.ReadAsStringAsync();
            if (!response.IsSuccessStatusCode)
            {
                _memoryCache.Remove("AccessToken");
                return Content($"获取 {apiurl} 失败。StatusCode:{response.StatusCode} \r\n Token:{token} \r\n result:{result}");
            }
            return Json(new
            {
                code = response.StatusCode,
                data = result
            });
        }

        private string GetApiUrl(string address)
        {
            return _apiBaseUrl + "/api/" + address;
        }
    }

2、请求 AccessToken

  http://localhost:5003/Idp/token ,请求成功后会将 token 存储到 cache 中

     

3、请求 API 资源

  http://localhost:5003/Idp/suibian ,token是直接在缓存里面取出来的

五、项目目录

 

 

posted @ 2020-08-03 17:32  朝闲  阅读(2538)  评论(0编辑  收藏  举报