第六節:IdentityServer4設備流授權模式和掃碼登錄(應用於IOT)


一. 模式探究

1.背景

  在一些輸入受限的設備上,要完成用戶名和口令的輸入是非常困難的,設備授權模式,可讓用戶登錄到智能電視、 IoT 物聯網設備或打印機等輸入受限的設備。 若要啟用此流,設備會讓用戶在另一台設備上的瀏覽器中訪問一個網頁,以進行登錄。 用戶登錄后,設備可以獲取所需的訪問令牌和刷新令牌。

2.運行流程

圖一:

圖二:

 

大致流程:

1. 客戶端通過攜帶ClientId、ClientSecret,請求IDS4服務器,請求成功,返回:DeviceCode、VerificationUriComplete

2. 客戶端將url寫入二維碼,或者用瀏覽器直接打開,進入授權頁面。這期間,客戶端攜帶ClientId、ClientSecret、DeviceCode不斷輪詢請求IDS4服務器,看是否已經授權。

3. 上面的授權頁面,用戶輸入賬號、密碼,確認授權。

4. 客戶端通過輪詢得知已經授權,且拿到返回值 accessToken。

5. 客戶端攜帶accessToken,請求資源服務器。

 

參考:

  微軟OAuth 2.0 設備代碼流:https://docs.microsoft.com/zh-cn/azure/active-directory/develop/v2-oauth2-device-code

  百度Device授權模式:https://developer.baidu.com/wiki/index.php?title=docs/oauth/device

  IDS4代碼參考:https://damienbod.com/2019/02/20/asp-net-core-oauth-device-flow-client-with-identityserver4/

 

二. 代碼實操與剖析

1. 項目准備

 (1). IDS4_Server2: 授權認證服務器

 (2). ResourceServer: 資源服務器

 (3). WinformClient1:基於winform的客戶端 (.Net下的,非Core下)

2.搭建步驟

(一).IDS4_Server2

 (1).通過Nuget安裝【IdentityServer4    4.0.2】程序集

 (2).集成IDS4官方的UI頁面

  進入ID4_Server2的根目錄,cdm模式下依次輸入下面指令,集成IDS4相關的UI頁面,發現新增或改變了【Quickstart】【Views】【wwwroot】三個文件夾

  A.【dotnet new -i identityserver4.templates】

  B.【dotnet new is4ui --force】 其中--force代表覆蓋的意思, 空項目可以直接輸入:【dotnet new is4ui】,不需要覆蓋。

 PS. 有時候正值版本更新期間,上述指令下載下來的文件可能不是最新的,這個時候只需要手動去下載,然后把上述三個文件夾copy到項目里即可

 (下載地址:https://github.com/IdentityServer/IdentityServer4.Quickstart.UI)

 (3).創建配置類 Config1

  A.配置api的范圍集合:ApiScope, 在4.x版本中必須配置

  B.配置需要保護Api資源:ApiResource,每個resource后面都需要配置對應的Scope

  C.配置可以訪問的客戶端資源:Client。重點配置設備流模式:GrantTypes.DeviceFlow。

  D.配置可以訪問的用戶資源:TestUser

代碼分享:

 public class Config1
    {

        /// <summary>
        /// 聲明api的Scope(范圍)集合
        /// IDS4 4.x版本必須寫的
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<ApiScope> GetApiScopes()
        {
            List<ApiScope> scopeList = new List<ApiScope>();
            scopeList.Add(new ApiScope("ResourceServer"));
            return scopeList;
        }

        /// <summary>
        /// 定義需要保護的Api資源
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<ApiResource> GetApiResources()
        {
            List<ApiResource> resources = new List<ApiResource>();
            //ApiResource第一個參數是ServiceName,第二個參數是描述
            resources.Add(new ApiResource("ResourceServer", "ResourceServer服務需要保護哦") { Scopes = { "ResourceServer" } });
            return resources;
        }

        /// <summary>
        /// 定義可以使用ID4 Server 客戶端資源
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<Client> GetClients()
        {
            List<Client> clients = new List<Client>() {
                new Client
                {
                    ClientId = "client1",//客戶端ID                             
                    AllowedGrantTypes = GrantTypes.DeviceFlow, //驗證類型:設備流模式
                    RequireConsent = true,          //手動確認授權
                    ClientSecrets ={ new Secret("0001".Sha256())},    //密鑰和加密方式
                    AllowedScopes = { "ResourceServer" },        //允許訪問的api服務
                    AlwaysIncludeUserClaimsInIdToken=true
                }
            };
            return clients;
        }

        /// <summary>
        /// 定義可以使用ID4的用戶資源
        /// </summary>
        /// <returns></returns>
        public static List<TestUser> GetUsers()
        {
            var address = new
            {
                street_address = "One Hacker Way",
                locality = "Heidelberg",
                postal_code = 69118,
                country = "Germany"
            };
            return new List<TestUser>()
            {
                new TestUser
                {
                        SubjectId = "001",
                        Username = "ypf1",    //賬號
                        Password = "123456",  //密碼
                        Claims =
                        {
                            new Claim(JwtClaimTypes.Name, "Alice Smith"),
                            new Claim(JwtClaimTypes.GivenName, "Alice"),
                            new Claim(JwtClaimTypes.FamilyName, "Smith"),
                            new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"),
                            new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                            new Claim(JwtClaimTypes.WebSite, "http://alice.com"),
                            new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json)
                        }
                 },
                 new TestUser
                 {
                        SubjectId = "002",
                        Username = "ypf2",
                        Password = "123456",
                        Claims =
                        {
                            new Claim(JwtClaimTypes.Name, "Bob Smith"),
                            new Claim(JwtClaimTypes.GivenName, "Bob"),
                            new Claim(JwtClaimTypes.FamilyName, "Smith"),
                            new Claim(JwtClaimTypes.Email, "BobSmith@email.com"),
                            new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                            new Claim(JwtClaimTypes.WebSite, "http://bob.com"),
                            //這是新的序列化模式哦
                            new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json)
                        }
                  }
            };
        }

    }
View Code

 (4).在Startup類中注冊、啟用、修改路由

   A.在ConfigureService中進行IDS4的注冊.

   B.在Configure中啟用IDS4 app.UseIdentityServer();

   C.路由,這里需要注意,不要和原Controllers里沖突即可,該項目中沒有Controllers文件夾,不要特別配置。

代碼分享:

 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)
        {
            services.AddControllersWithViews();
            services.AddIdentityServer()
               .AddDeveloperSigningCredential()    //生成Token簽名需要的公鑰和私鑰,存儲在bin下tempkey.rsa(生產場景要用真實證書,此處改為AddSigningCredential)
               .AddInMemoryApiScopes(Config1.GetApiScopes())       //存儲所有的scopes
               .AddInMemoryApiResources(Config1.GetApiResources())  //存儲需要保護api資源
                .AddTestUsers(Config1.GetUsers())          //存儲用戶信息
               .AddInMemoryClients(Config1.GetClients()); //存儲客戶端模式(即哪些客戶端可以用)
        }

        // 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");
            }
            app.UseStaticFiles();
            app.UseRouting();


            //啟用IDS4
            app.UseIdentityServer();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
View Code

 (5).配置啟動端口,直接設置默認值: webBuilder.UseStartup<Startup>().UseUrls("http://127.0.0.1:7000");

 (6).修改屬性方便調試:項目屬性→ 調試→應用URL(p),改為:http://127.0.0.1:7000 (把IISExpress和控制台啟動的方式都改了,方便調試)

圖:

 

(二).ResourceServer

 (1).通過Nuget安裝 【IdentityServer4.AccessTokenValidation 3.0.1】

 (2).在ConfigureService通過AddIdentityServerAuthentication連接ID4服務器,進行校驗,使用的是Bear認證方式。這里ApiName中的“ResourceServer”必須是ID4中GetApiResources中添加的。

特別注意:這個Authority要用127.0.0.1, 不用Localhost,因為我們獲取token的時候,使用的地址也是127.0.0.1,必須對應起來.

 (3).在Config中添加認證中間件 app.UseAuthentication();

代碼分享:

  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)
        {
            services.AddControllers();

            //校驗AccessToken,從身份校驗中心(IDS4_Server2)進行校驗
            services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)  //Bear模式
                    .AddIdentityServerAuthentication(options =>
                    {
                        options.Authority = "http://127.0.0.1:7000"; // 1、授權中心地址
                        options.ApiName = "ResourceServer"; // 2、api名稱(項目具體名稱)
                        options.RequireHttpsMetadata = false; // 3、https元數據,不需要
                    });
        }

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

            //認證中間件(服務於上ID4校驗,一定要放在UseAuthorization之前)
            app.UseAuthentication();
            app.UseAuthorization();

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

 (4).新建一個GetMsg接口,並加上特性[Authorize]。

代碼分享:

 [Route("api/[controller]/[action]")]
    [ApiController]
    public class HomeController : ControllerBase
    {

        /// <summary>
        /// 資源服務器的api
        /// </summary>
        /// <returns></returns>
        [Authorize]
        [HttpGet]
        public string GetMsg()
        {
            //快速獲取token的方式
            string token = HttpContext.GetTokenAsync("access_token").Result;

            return $"ypf";
        }
    }
View Code

 (5).配置啟動端口,直接設置默認值: webBuilder.UseStartup<Startup>().UseUrls("http://127.0.0.1:7001");

 (6).修改屬性方便調試:項目屬性→ 調試→應用URL(p),改為:http://127.0.0.1:7001 (把IISExpress和控制台啟動的方式都改了,方便調試)

圖:

(三).WinformClient1

 (1).通過Nuget安裝【QRCoder 1.3.9】 【IdentityModel 4.3.0】

 (2).請求IDS4服務器,拿到一個url,寫入二維碼,並顯示二維碼;客戶端此時在輪詢請求IDS4,看是否已經授權成功。

 (3).正常應該用手機掃描二維碼,進行授權,這里為了方便演示, 用瀏覽器直接打開這個地址,代替手機掃描

 eg:Process.Start(new ProcessStartInfo(deviceResponse.VerificationUriComplete) { UseShellExecute = true });

 (4).授權成功后,客戶端拿着返回的accessToken,繼續請求api資源服務器,請求成功

代碼分享:

   }
        private async void Form1_Load(object sender, EventArgs e)
        {
            var client = new HttpClient();
            var disco = await client.GetDiscoveryDocumentAsync("http://127.0.0.1:7000");    //IDS4服務器
            var deviceResponse = await client.RequestDeviceAuthorizationAsync(new DeviceAuthorizationRequest
            {
                Address = disco.DeviceAuthorizationEndpoint,
                ClientId = "client1",
                ClientSecret = "0001"
            });
            //生成二維碼
            CreateQrCode(deviceResponse.VerificationUriComplete);
            //通過瀏覽器打開地址
            Process.Start(new ProcessStartInfo(deviceResponse.VerificationUriComplete) { UseShellExecute = true });
            //輪詢請求
            string accessToken;
            while (true)
            {
                // request token
                var tokenResponse = await client.RequestDeviceTokenAsync(new DeviceTokenRequest
                {
                    Address = disco.TokenEndpoint,
                    ClientId = "client1",
                    ClientSecret = "0001",
                    DeviceCode = deviceResponse.DeviceCode
                });
                if (!tokenResponse.IsError)
                {
                    accessToken = tokenResponse.AccessToken;                 
                    break;
                }
                await Task.Delay(TimeSpan.FromSeconds(deviceResponse.Interval));
                //await Task.Delay(TimeSpan.FromSeconds());
            }
            await CallApiAsync(accessToken);
        }



        /// <summary>
        /// 請求api資源
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        private async Task CallApiAsync(string token)
        {
            // call api
            var apiClient = new HttpClient();
            apiClient.SetBearerToken(token);
            var response = await apiClient.GetAsync("http://127.0.0.1:7001/api/Home/GetMsg");
            if (!response.IsSuccessStatusCode)
            {
                var msg= response.Content.ReadAsStringAsync().Result;
                this.pictureBox1.Visible = false;   //隱藏二維碼
                this.label2.Text = msg;  //顯示返回結果
                //MessageBox.Show($"api返回值為:{msg}");
            }
            else
            {
                var msg = response.Content.ReadAsStringAsync().Result;
                this.pictureBox1.Visible = false;   //隱藏二維碼
                this.label2.Text = msg;   //顯示返回結果
                //MessageBox.Show($"api返回值為:{msg}");
            }
        }



        /// <summary>
        /// 生成二維碼
        /// </summary>
        /// <param name="verificationUriComplete"></param>
        public void CreateQrCode(string verificationUriComplete)
        {
            QRCodeGenerator qrGenerator = new QRCodeGenerator();
            QRCodeData qrCodeData = qrGenerator.CreateQrCode(verificationUriComplete, QRCodeGenerator.ECCLevel.Q);
            QRCode qrCode = new QRCode(qrCodeData);
            Bitmap qrCodeImage = qrCode.GetGraphic(6);
            this.pictureBox1.Image = Image.FromHbitmap(qrCodeImage.GetHbitmap());
        }
View Code

窗體:

PS:詳細的流程剖析詳見下面的剖析測試

3.剖析測試

測試過程如下:

(PS:不知為啥fiddler捕捉不到winfrom發送的http請求,這里只能通過截圖來說明了)

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鵬飛)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 聲     明1 : 如有錯誤,歡迎討論,請勿謾罵^_^。
  • 聲     明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM