.NET Core2.0應用IdentityServer4

IdentityServer4能解決什麼問題

假設咱們開發了一套【微博程序】,主要擁有兩個功能:【登錄驗證】、【數據獲取】html

隨後咱們又開發了【簡書程序】、【知乎程序】,它們的主要功能也是:【登錄驗證】、【數據獲取】web

這時候咱們就會想一個問題,每一個應用程序的【數據獲取】可能各不相同。可是【登錄驗證】可否作成單點登錄?因而有了以下的結構json

注意:因爲【微博程序】、【簡書程序】、【知乎程序】,都是咱們本身開發的程序,因此咱們能夠將全部登錄都彙總到【微博登錄中心】,用戶信息在每一個程序之間的傳輸(哪些用戶信息能夠在程序間共享,哪些用戶信息不能在程序間共享),咱們是方便控制的。api

用戶還會使用不少第三方程序。對於用戶而言,確定不想每用到一個第三方程序都要從新去維護本身的我的信息。有沒有一種方式既能夠實現用戶信息在多個第三方程序間共享(用戶就不用每使用一個第三方系統都須要維護我的信息),同時又能保證我的信息安全?跨域

注意:【微博登錄中心】【第三方程序】所使用的是【雙向箭頭】,說明【第三方程序】【微博登錄中心】中獲取到了【用戶信息】。而如何保證【用戶信息】的安全,就是用【IdentityServer4】來實現的。瀏覽器

IdentityServer4如何保證用戶信息安全

從上文最後一個場景中,咱們瞭解到容許【第三方程序】訪問【用戶信息】能夠爲用戶提供不少便利。可是提供便利的同時如何能保證【信息安全】又是一個不得不解決的問題。要保證用戶信息的安全,至少要知足如下3點。安全

(A)  用戶贊成【第三方程序】訪問本身的【用戶信息】,或者說用戶必須告訴【登錄中心】:我贊成當前這個【第三方程序】訪問個人【用戶信息】

(B)【第三方程序】必須是在【登錄中心】登記過,即【登錄中心】認證【第三方程序】的身份是否真實可靠

(C)【登錄中心】將【用戶信息】劃分爲【可共享信息】與【不可共享信息】,【登錄中心】受權【第三方程序】訪問【可共享信息】
服務器

固然這裏只是拋磚引玉,數據傳輸間的加密協議、【第三方程序】訪問【登錄中心】的次數限制。。。都沒有列出來。總之最好有一套成熟(公認)的協議來保證【數據共享】與【信息安全】,因此這裏就引出了:OpenID Connect,IentityServer4就是依靠OpenID Connect來保證用戶信息的共享與安全。mvc

何爲OpenID Connect

 OpenID的定義

OpenID 是一個以用戶爲中心的數字身份識別框架,它具備開放、分散性。OpenID 的建立基於這樣一個概念:咱們能夠經過 URI (又叫 URL 或網站地址)來認證一個網站的惟一身份,同理,咱們也能夠經過這種方式來做爲用戶的身份認證app

 總結爲:OpenId用於身份認證(Authentication)

OpenID Connect的定義

OpenID Connect 1.0 是基於OAuth 2.0協議之上的簡單身份層,它容許客戶端根據受權服務器的認證結果最終確認終端用戶的身份,以及獲取基本的用戶信息;它支持包括Web、移動、JavaScript在內的全部客戶端類型去請求和接收終端用戶信息和身份認證會話信息;它是可擴展的協議,容許你使用某些可選功能,如身份數據加密、OpenID提供商發現、會話管理等。

 總結爲:OpenID Connect = OIDC = OpenID(Authentication)+ OAuth2.0(Authorization

 因爲OpenID Connect的受權是基於OAuth2.0協議的,因此下面須要着重介紹一下OAuth2.0

OAuth2.0定義

OAuth 2.0是行業標準的受權協議。 OAuth 2.0取代了2006年建立的原始OAuth協議所作的工做。OAuth 2.0專一於客戶端開發人員的簡單性,同時爲Web應用程序,桌面應用程序,移動電話和客廳設備提供特定的【受權流程】

上面這段話摘自OAuth2.0官網,總結:爲各類端(web應用、桌面應用、移動設備。。。)提供【受權流程】。

OAuth2.0受權流程圖



(A)用戶打開客戶端之後,客戶端要求用戶給予受權。

(B)用戶贊成給予客戶端受權。

(C)客戶端使用上一步得到的受權,向認證服務器申請令牌。

(D)認證服務器對客戶端進行認證之後,確認無誤,贊成發放令牌。

(E)客戶端使用令牌,向資源服務器申請獲取資源。

(F)資源服務器確認令牌無誤,贊成向客戶端開放資源。

 流程圖與解釋均摘自OAuth2.0 RFC 6749,流程總結爲:用戶受權-》申請令牌-》獲取資源

 OAuth2.0四種受權模式

客戶端必須獲得用戶的受權(authorization grant),才能得到令牌(access token)。OAuth 2.0定義了四種受權方式。

(1)受權碼模式(authorization code):功能最完整、流程最嚴密的受權模式。它的特色就是經過客戶端的後臺服務器,與"服務提供商"的認證服務器進行互動。(用戶受權->客戶端請求受權碼(Authorization Code)->客戶端獲取受權碼->客戶端請求令牌(Access Token)->客戶端獲取令牌->客戶端請求資源)

(2)簡化模式(implicit):不經過第三方應用程序的服務器,直接在瀏覽器中向認證服務器申請令牌,跳過了"受權碼"這個步驟,所以得名。全部步驟在瀏覽器中完成,令牌對訪問者是可見的,且客戶端不須要認證。(用戶受權->客戶端請求令牌(Access Token)->客戶端獲取令牌->客戶端請求資源)

(3)密碼模式(resource owner password credentials):用戶向客戶端提供本身的用戶名和密碼。客戶端使用這些信息,向"服務商提供商"索要受權。(用戶將用戶名&密碼提供給客戶端->客戶端請求令牌(Access Token)->客戶端獲取令牌->客戶端請求資源)

(4)客戶端模式(client credentials):客戶端以本身的名義,而不是以用戶的名義,向"服務提供商"進行認證。(客戶端請求令牌(Access Token)->客戶端獲取令牌->客戶端請求資源)

每種模式的流程圖、關鍵字,在這裏就不作過多的贅述,你們能夠參閱阮一峯的文章:理解OAuth2.0

OpenID Connect(OIDC)流程概述

OAuth2提供了Access Token來解決受權第三方客戶端訪問受保護資源的問題;OIDC在這個基礎上提供了ID Token來解決第三方客戶端標識用戶身份認證的問題。OIDC的核心在於在OAuth2的受權流程中,一併提供用戶的身份認證信息(ID Token)給到第三方客戶端,ID Token使用JWT格式來包裝,得益於JWT(JSON Web Token)的自包含性,緊湊性以及防篡改機制,使得ID Token能夠安全的傳遞給第三方客戶端程序而且容易被驗證。此外還提供了UserInfo的接口,用戶獲取用戶的更完整的信息

 總結爲:OIDC新增了一個【ID Token】,【ID Token】主要用來給【第三方平臺】標識(認證)用戶(經過sub與subid),同時也能夠將【用戶信息】存儲其中。

OpenID Connect(OIDC)認證受權流程圖

 名詞介紹

(A)AuthN:認證

(B)AuthZ:受權

(C)EU:End User:用戶

(D)RP:Relying Party :用來代指OAuth2中的受信任的客戶端,身份認證和受權信息的消費方

(E)OP:OpenID Provider,服務端(好比OAuth2中的受權服務),用來爲客戶端提供用戶的身份認證信息

(F)ID Token:JWT格式的數據,包含用戶身份認證的信息(還可包含用戶其它信息)

(G)Access Token:受權碼,服務點受權成功後,返回給客戶端。用於請求用戶接口信息

(H)UserInfo Endpoint:用戶信息接口(受OAuth2保護),當客戶端使用Access Token訪問時,返回用戶的信息,此接口必須使用HTTPS

流程圖

(A)RP(客戶端)向OpenID提供商(OP)發送請求

(B)OP對用戶進行身份驗證並得到受權

(C)OP以Id Token、Access Token響應

(D)RP可使用Access Token向UserInfo端點發送請求

(E)UserInfo端點返回有關用戶的信息

OpenID Connect(OIDC)三種受權模式

受權碼模式:客戶端請求用戶信息->用戶受權->受權服務返回受權碼(Authorization Code)->客戶端獲取受權碼(Authorization Code)->客戶端請求令牌(Access Token & Id Token)->客戶端獲取令牌->客戶端請求資源

(A)RP發送一個認證請求給OP,請求中必須包含Client ID

(B)OP驗證用戶信息,同時獲取用戶贊成/受權

(C)OP將受權碼(Code)返回給RP

(D)RP向Token Endpoint申請Id Token與Access Token,請求中必須包含Code

(E)RP向UserInfo Endpoint申請用戶信息(Claims),請求中必須包含Access Token

簡化模式(implicit):客戶端請求用戶信息->用戶受權->受權服務返回令牌(Id Token & Access Token)  

(A)RP發送一個認證請求給OP(附帶client_id)

(B)OP驗證用戶信息,同時獲取用戶贊成/受權

(C)OP將Id Token與Access Token返回給客戶端,Id Token中能夠包含用戶信息(Claims)

混合模式:客戶端請求用戶信息->用戶受權->受權服務返回受權碼(Authorization Code)與一些其它參數->客戶端獲取受權碼(Authorization Code)->客戶端請求令牌(Access Token & Id Token)->客戶端獲取令牌->客戶端請求資源

(A)RP發送一個認證請求給OP,請求中必須包含Client ID

(B)OP驗證用戶信息,同時獲取用戶贊成/受權

(C)OP將受權碼(Code)返回給RP,可根據相應類型返回附加參數(官網給出的例子中並無給出附加參數的例子)

(D)RP向Token Endpoint申請Id Token與Access Token,請求中必須包含Code

(E)RP向UserInfo Endpoint申請用戶信息(Claims),請求中必須包含Access Token

IdentityServer4在ASP.NET Core中的運用

回到文章開頭的假設,咱們擁有【用戶信息】,第三方程序但願經過用戶受權來訪問【用戶信息】。這裏我準備用【簡化模式(implicit)】來演示ASP.NET Core中運用IdentityServer4來實現單點登陸功能。至於爲什麼選擇【簡化模式】,由於它的實現最簡單,從簡單的入手,方便快速瞭解框架實現套路。

One:客戶端搭建

1.新建一個ASP.NET Core MVC項目

2.注入認證中間件,同時啓動認證

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

            //清空默認綁定的用戶信息
            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

            //添加認證服務
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";                   //默認使用Cookies方案進行認證
                options.DefaultChallengeScheme = "oidc";        //默認認證失敗時啓用oidc方案
            })
            .AddCookie("Cookies")   //添加Cookies認證方案

            //添加oidc方案
            .AddOpenIdConnect("oidc", options =>
            {
                options.SignInScheme = "Cookies";       //身份驗證成功後使用Cookies方案來保存信息

                options.Authority = "http://localhost:16584";    //受權服務地址
                options.RequireHttpsMetadata = false;

                options.ClientId = "mvc_implicit";
                options.SaveTokens = true;
            });
        }

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

            //啓用認證
            app.UseAuthentication();

            app.UseStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
View Code

3.資源添加受權保護,即:HomeController中添加[Authorize]

    [Authorize]
    public class HomeController : Controller
    {
         //...       
    }
View Code

4.About試圖展現用戶信息與Token

@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"]</h2>
<h3>@ViewData["Message"]</h3>

@using Microsoft.AspNetCore.Authentication

<p>
    <dl>
        @foreach (var claim in User.Claims)
        {
            <dt>@claim.Type</dt>
            <dd>@claim.Value</dd>
        }

        <dt>Access Token</dt>
        <dd>@await ViewContext.HttpContext.GetTokenAsync("access_token")</dd>

        <dt>Refresh Token</dt>
        <dd>@await ViewContext.HttpContext.GetTokenAsync("refresh_token")</dd>

        <dt>Id Token</dt>
        <dd>@await ViewContext.HttpContext.GetTokenAsync("id_token")</dd>
    </dl>
</p>
View Code

Two:受權服務搭建

1.新建一個ASP.NET Core MVC項目

2.引入nuget包,IdentityServer4 v2.2.0

3.註冊ApiResource,即受權後可訪問的Api(PS:ApiResource對應的是OAuth2.0中的Scope)

        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource("api","My Api")
            };
        }
View Code

4.註冊IdentityResource,即受權後客戶端可訪問的用戶信息(PS:IdentityResource對應的是OpenId Connect中的Scope

        public static IEnumerable<IdentityResource> GetIdentityResources() => new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
            new IdentityResources.Email()
        };
View Code

5.註冊客戶端,便可被受權的Client

        public static IEnumerable<Client> GetClients() => new List<Client>
        {
            new Client
            {
                ClientId = "mvc_implicit",
                ClientName = "MVC Client",
                AllowedGrantTypes = GrantTypes.Implicit,                //簡化模式
                RequireConsent = false,     //Consent是受權頁面,這裏咱們不進行受權

                RedirectUris = { "http://localhost:1798/signin-oidc" },
                PostLogoutRedirectUris = { "http://localhost:1798/signout-callback-oidc" },

                //受權後能夠訪問的用戶信息(OpenId Connect Scope)與Api(OAuth2.0 Scope)
                AllowedScopes = new List<string>
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    IdentityServerConstants.StandardScopes.Email,
                    "api"
                }
            }
        };
View Code

RedirectUris:客戶端oidc自帶的地址(功能),用於處理登錄成功後處理受權服務返回的response,同時保存配置
PostLogoutRedirectUris:
客戶端oidc自帶的地址(功能),退出登陸後跳轉到受權服務,將受權服務也推出登錄


6.註冊用戶,這裏就用IdentityServer4提供的TestUser進行測試

        public static List<TestUser> GetTestUsers() => new List<TestUser>
        {
            new TestUser()
            {
                SubjectId="1",
                Username="test",
                Password="123456"
            }
        };
View Code

7.在.NET Core中注入IdentityServer4,同時把上面的(3)(4)(5)(6)注入到.NET Core中,同時啓動IdentityServer4

        public void ConfigureServices(IServiceCollection services)
        {
            //注入IdentityServer4
            services.AddIdentityServer(c => {
                //登錄地址
                c.UserInteraction.LoginUrl = "/account/login";
            })
            .AddDeveloperSigningCredential()

            //下面是注入資源信息
            .AddInMemoryApiResources(Config.GetApiResources())
            .AddInMemoryClients(Config.GetClients())
            .AddInMemoryIdentityResources(Config.GetIdentityResources())
            .AddTestUsers(Config.GetTestUsers());

            services.AddMvc();
        }

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

            //啓動IdentityServer4
            app.UseIdentityServer();

            app.UseStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
View Code

8.添加登錄Controller,AccountController

    public class AccountController : Controller
    {
        private readonly TestUserStore _users;

        public AccountController(TestUserStore users)
        {
            _users = users;
        }

        public IActionResult Index()
        {
            return View("Login");
        }

        public IActionResult Login(string returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;
            return View();
        }

        [HttpPost]
        public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
        {
            if (ModelState.IsValid)
            {
                ViewData["ReturnUrl"] = returnUrl;

                var user = _users.FindByUsername(model.UserName);

                if (user == null)
                {
                    ModelState.AddModelError(nameof(model.UserName), "Username not exists");
                }
                else
                {
                    if (user.Password.Equals(model.Password))
                    {
                        //(保存)認證信息字典
                        AuthenticationProperties props = new AuthenticationProperties
                        {
                            IsPersistent = true,    //認證信息是否跨域有效
                            ExpiresUtc = DateTimeOffset.UtcNow.Add(TimeSpan.FromMinutes(30))    //憑據有效時間
                        };

                        await Microsoft.AspNetCore.Http.AuthenticationManagerExtensions.SignInAsync(
                            HttpContext, user.SubjectId, user.Username, props);

                        return RedirectToLoacl(returnUrl);
                    }

                    ModelState.AddModelError(nameof(model.Password), "Password Error");
                }
            }

            return View(model);
        }

        public async Task<IActionResult> Logout()
        {
            await HttpContext.SignOutAsync();
            return RedirectToAction("Index", "Home");
        }

        private IActionResult RedirectToLoacl(string returnUrl)
        {
            if (Url.IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            }

            return RedirectToAction(nameof(HomeController.Index), "Home");
        }
    }
View Code

9.添加登錄ViewModel,LoginViewModel

    public class LoginViewModel
    {
        public string UserName { get; set; }

        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
View Code

10.添加登錄View,Login.cshtml

@{
    ViewData["Title"] = "Login";
}

@model LoginViewModel;

<div class="row">
    <div class="col-md-4">
        <section>
            <form method="post" asp-controller="Account" asp-action="Login" asp-route-returnUrl="@ViewData["ReturnUrl"]">
                <h4>Use a local account to log in.</h4>
                <hr />

                <div class="form-group">
                    <label asp-for="UserName"></label>
                    <input asp-for="UserName" class="form-control" />
                    <span asp-validation-for="UserName" class="text-danger"></span>
                </div>
                <div class="form-group">
                    <label asp-for="Password"></label>
                    <input asp-for="Password" type="password" class="form-control" />
                    <span asp-validation-for="Password" class="text-danger"></span>
                </div>

                <div class="form-group">
                    <button type="submit" class="btn btn-default">Log in</button>
                </div>

            </form>
        </section>
    </div>

</div>

@section Scripts
    {
    @await Html.PartialAsync("_ValidationScriptsPartial")
}
View Code

PS:上面貼出的代碼中,你們須要注意的就是地址配置,我配置的都是我本機的地址,並且我並無配置固定地址,各位能夠配置成固定地址。

Three:顯示效果

1.先運行服務端

2.再運行客戶端,會直接跳轉到服務端的Login頁面。PS:URL後面跟着client_Id,登錄成功後的跳轉頁面等

3.登錄成功後跳轉回客戶端

4.點擊About,查看返回的用戶信息

5.遺留問題

1.簡化模式默認是不返回Access Token,若是須要返回,須要作以下配置

a.受權服務,客戶端註冊時,開啓返回Access Token

public static IEnumerable<Client> GetClients() => new List<Client>
        {
            new Client
            {
                ClientId = "mvc_implicit",
                ClientName = "MVC Client",
                AllowedGrantTypes = GrantTypes.Implicit,                //簡化模式
                RequireConsent = false,     //Consent是受權頁面,這裏咱們不進行受權

                RedirectUris = { "http://localhost:1798/signin-oidc" },
                PostLogoutRedirectUris = { "http://localhost:1798/signout-callback-oidc" },

                //受權後能夠訪問的用戶信息(OpenId Connect Scope)與Api(OAuth2.0 Scope)
                AllowedScopes = new List<string>
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    IdentityServerConstants.StandardScopes.Email,
                    "api"
                },

                //容許返回Access Token
                AllowAccessTokensViaBrowser = true
            }
        };
View Code

b.客戶端請求類型(response_type),須要包含Access Token

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

            //清空默認綁定的用戶信息
            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

            //添加認證服務
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";                   //默認使用Cookies方案進行認證
                options.DefaultChallengeScheme = "oidc";        //默認認證失敗時啓用oidc方案
            })
            .AddCookie("Cookies")   //添加Cookies認證方案

            //添加oidc方案
            .AddOpenIdConnect("oidc", options =>
            {
                options.SignInScheme = "Cookies";       //身份驗證成功後使用Cookies方案來保存信息

                options.Authority = "http://localhost:16584";    //受權服務地址
                options.RequireHttpsMetadata = false;

                options.ClientId = "mvc_implicit";
                options.ResponseType = "id_token token";    //默認只返回id_token 這裏添加上token(Access Token)
                options.SaveTokens = true;
            });
        }
View Code

2.咱們並無獲得用戶郵箱、profile等

若是要解決這個問題,須要添加ProfileService,請參考IdentityServer4

參考博文與實例代碼下載

http://www.cnblogs.com/cgzl/p/7793241.html

https://www.jianshu.com/p/be7cc032a4e9

示例代碼下載

相關文章
相關標籤/搜索