IdentityServer4使用OpenIdConnect實現單點登陸

接上一篇:IdentityServer4實現OAuth2.0四種模式之受權碼模式html

前面寫的四種OAuth2.0實現模式只涉及到IdentityServer4的OAuth2.0特性,並無涉及到OenId方面的。OpenIdConnect是OAuth2.0與OpenId的結合,並加入了一個重要的概念:id_token。咱們以前所講的token是用於訪問受權的access_token,而id_token是用於身份驗證的,做用徹底不一樣,這一點要區分開來。access_token是OAth2.0特性,而id_token是OpenIdConnect方案爲改善OAuth2.0方案在身份驗證方面的薄弱而加入的特性。web

客戶端獲取Id_token與隱藏模式和受權碼模式同樣,都是經過redirect_url參數返回的,因此前面的四種模式中的客戶端模式與密碼模式不支持獲取id_token,而受權碼模式受限於流程,必需先取得Code才能取到token,因此不能直接支持獲取id_token,若是需求是使用受權碼模式,同時又須要id_token,OpenIdConnect支持第五種模式:混合模式(Hybrid),就是基於隱藏模式與受權碼模式的結合。sql

一,IdentityServer服務配置

  1)添加IdentityResouces

以前的OAuth2.0四種模式已經接觸過ApiResouces,ApiResources做用是用於標誌Api接口域,與Client配合決定了一個access_token所能訪問的api區間,而且容許隨access_token攜帶一些指定的用戶Claim。IdentityResources是用於決定了一個id_token能夠攜帶那些用戶的身份信息(Claim),其中,若是要從IdentityServer取得id_token,名爲"openid"的IdentityResource是必需的json

  • 使用IdentityServer4定義好的IdentityResource

IdentityServer.Config.GetIdentityResourcesapi

public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new IdentityResource[]
            {
                new IdentityResources.OpenId()
            };
        }

  IdentityResources.OpenId的實現源碼以下瀏覽器

public OpenId()
            {
                Name = IdentityServerConstants.StandardScopes.OpenId;
                DisplayName = "Your user identifier";
                Required = true;
                UserClaims.Add(JwtClaimTypes.Subject);
            }

  實際上new IdentityResources.OpenId()等效於:安全

new IdentityResource(IdentityServerConstants.StandardScopes.OpenId,"Your user identifier",new List<string>(){JwtClaimTypes.Subject})

 或者cookie

  new IdentityResource("openid","Your user identifier",new List<string>(){ "sub"})

 所用的重載方法說明最後一個參數List<string>決定了這個IdentityResource攜帶的Claim,「sub」是Client中定義的Subject屬性。session

        // 摘要:
        //     Initializes a new instance of the IdentityServer4.Models.IdentityResource class.
        //
        // 參數:
        //   name:
        //     The name.
        //
        //   displayName:
        //     The display name.
        //
        //   claimTypes:
        //     The claim types.
        //
        // 異常:
        //   T:System.ArgumentNullException:
        //     name
        //
        //   T:System.ArgumentException:
        //     Must provide at least one claim type - claimTypes
        public IdentityResource(string name, string displayName, IEnumerable<string> claimTypes);

  IdentityServer4預約義了OenId,Profile,Email,Phone,Addrss這5個IdentityResource,其中Profile是比較重要的,他默承認攜帶包括用戶的名字、生日、我的網站等信息。Profile映射的Claim的源碼以下app

 { IdentityServerConstants.StandardScopes.Profile, new[]
                            { 
                                JwtClaimTypes.Name,
                                JwtClaimTypes.FamilyName,
                                JwtClaimTypes.GivenName,
                                JwtClaimTypes.MiddleName,
                                JwtClaimTypes.NickName,
                                JwtClaimTypes.PreferredUserName,
                                JwtClaimTypes.Profile,
                                JwtClaimTypes.Picture,
                                JwtClaimTypes.WebSite,
                                JwtClaimTypes.Gender,
                                JwtClaimTypes.BirthDate,
                                JwtClaimTypes.ZoneInfo,
                                JwtClaimTypes.Locale,
                                JwtClaimTypes.UpdatedAt 
                            }},

  

  • 添加自定義的IdentityResource

儘管IdentityServer定義好了這麼多IdentityResource,但確定不能包含全部用戶信息。好比我須要在id_token中攜帶用戶手機型號和用戶手機價格二個Claim。能夠這樣自定義一個IdentityResource。

new IdentityResource("PhoneModel","User's phone Model",new List<string>(){ "phonemodel","phoneprise"})

  

  2)將IdentityResource添加到IdentityServer

IdentityServer.StartUp

  var builder = services.AddIdentityServer()
                //身份信息資源
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                
                //API受權資源
                .AddInMemoryApiResources(Config.GetApis())
                //客戶端
                .AddInMemoryClients(Config.GetClients())
                //添加用戶
                .AddTestUsers(Config.GetUsers());

  

  3)配置用戶的Claim信息

 new TestUser()
                {
                    //用戶名
                     Username="apiUser",
                     //密碼
                     Password="apiUserPassword",
                     //用戶Id
                     SubjectId="0",
                     
                     Claims=new List<Claim>(){
                         new Claim(ClaimTypes.Role,"admin"),
                         new Claim(ClaimTypes.Name,"apiUser"),
                         new Claim("prog","正式項目"),
                         new Claim("phonemodel","huawei"),
                         new Claim("phoneprise","5000元"),

                     }
                },

  

  4)配置隱藏模式客戶端容許訪問該IdentityResource

  在前邊的四種模式中只有隱藏模式支持直接獲取id_token

 new Client()
                {
                    //客戶端Id
                     ClientId="apiClientImpl",
                     ClientName="ApiClient for Implicit",
                     //客戶端受權類型,Implicit:隱藏模式
                     AllowedGrantTypes=GrantTypes.Implicit,
                     //容許登陸後重定向的地址列表,能夠有多個
                    RedirectUris = {"https://localhost:5002/auth.html"},
                     //容許訪問的資源
                     AllowedScopes={
                        "secretapi",
                        IdentityServerConstants.StandardScopes.OpenId,
                        "PhoneModel"
                    },
                     //容許將token經過瀏覽器傳遞
                     AllowAccessTokensViaBrowser=true,
                     //容許ID_TOKEN附帶Claims
                     AlwaysIncludeUserClaimsInIdToken=true
                }

 5)添加混合模式客戶端並配置AllowedSopes

 new Client()
                {
                     AlwaysIncludeUserClaimsInIdToken=true,
                    //客戶端Id
                     ClientId="apiClientHybrid",
                     ClientName="ApiClient for HyBrid",
                     //客戶端密碼
                     ClientSecrets={new Secret("apiSecret".Sha256()) },
                     //客戶端受權類型,Hybrid:混合模式
                     AllowedGrantTypes=GrantTypes.Hybrid,
                     //容許登陸後重定向的地址列表,能夠有多個
                    RedirectUris = {"https://localhost:5002/auth.html"},
                     //容許訪問的資源
                     //容許訪問的資源
                     AllowedScopes={
                        "secretapi",
                        IdentityServerConstants.StandardScopes.OpenId,
                        "PhoneModel"
                    },
                      AllowOfflineAccess = true,
                     AllowAccessTokensViaBrowser=true
                }

  

二,隱藏模式獲取id_token

先來回顧一下隱藏模式怎麼請求access_token的

根據OAuth2.0協議,隱藏模式獲取access_token須要傳的參數以下所示。

client_id:客戶端Id
redirect_uri=重定向Url,用戶登陸成功後跳回此地址
response_type=token,固定值,表示獲取token
scope=secretapi,此token須要訪問的api

接受參數的地址則是IdentityServer的Discover文檔中的authorization_endpoint節點。把參數和地址拼接成如下地址:http://localhost:5000/connect/authorize?client_id=apiClientImpl&redirect_uri=https://localhost:5002/auth.html&response_type=token&scope=secretapi,直接訪問,會跳轉到用戶登陸頁面, 確認後,瀏覽器將會自動跳轉到redirect_url。

獲取id_token也是一樣的方法,但要注意如下四點

  1. response_type:隱藏模式支持三種response_type,上面獲取access_token已經使用了一種,第二種是獲取id_token:id_token。第三種是同時獲取access_token和id_token:token id_token
  2. scope:上面的scope值"scretapi"是一個ApiResource,咱們要獲取Id_token,必需加入"openid",這是一個IdentityResource。其它的profile,email等按需添加。
  3. 除開上面的四個參數外,還須要添加一個參數:nonce。這個參數做用是協助你驗證這個id_token是否由你本身發出的,能夠是一個隨機值,也能夠是你本身的請求特徵加密字符串,會隨id_token一併返回供你驗證。
  4. 能夠選擇性添加一個參數:response_mode。這個參數的做用是指定id_token傳到redirect_Url的方法。支持三種方法:

    1,query,用於獲取受權碼,經過url的Query部份傳遞。如(http://redirect_url.com?code=)。支持受權碼模式客戶端

    2,fragment。和隱藏模式獲取access_token同樣,經過url的fragment部份傳遞,如(http://redirect_url.com#token=&id_token=)。支持隱藏模式和混合模式客戶端

    3,form_post模式,經過form表單(x-www-form-urlencoded)Post到指定url。支持混合模式客戶端

根據這四點注意事項,請求url就變成了這樣

http://localhost:5000/connect/authorize?client_id=apiClientImpl&redirect_uri=https://localhost:5002/auth.html&response_type=token%20id_token&scope=secretapi%20openid%20PhoneModel&nonce=123&response_model=fragment

使用以前建立apiUser登陸成功後出現以下受權界面

 

 三個紅色的方框表明請求的三個scope。

贊成受權後,將會跳轉回redirect_url,id_token和access_token都獲取到了

 

 在jwt.io中解析一下這個id_token

{
  "nbf": 1569059940,
  "exp": 1569060240,
  "iss": "http://localhost:5000",
  "aud": "apiClientImpl",
  "nonce": "123",
  "iat": 1569059940,
  "at_hash": "PJZyIPRkonv7BWTF42asJw",
  "sid": "4b2901045d883a8ba7cf6169b976a113",
  "sub": "0",
  "auth_time": 1569059940,
  "idp": "local",
  "phonemodel": "huawei",
  "phoneprise": "5000元",
  "amr": [
    "pwd"
  ]
}

  jwt.io中基本支持全部平臺對jwt格式的解析和驗證,詳見https://jwt.io/

三,混合模式獲取id_token

1,使用fragment方式

       混合模式獲取Id_token與隱藏模式獲取id_token大致相同,只有如下二點要注意

  1. 把client_id改爲第一步建立的混合模式客戶端id
  2. 隱藏模式支持三種response_type:token、id_token、token id_token,分別用於請求access_token,id_token以及同時請求兩者。而混合模式支持四種:code,code token,code id_token,code token id_token。可用於請求code,id_token,access_token以及同時請求三者。

根據這兩點,混合模式的請求url變成了

http://localhost:5000/connect/authorize?client_id=apiClientHybrid&redirect_uri=https://localhost:5002/auth.html&response_type=code token id_token&scope=secretapi openid PhoneModel&nonce=123&response_mode=fragment

用戶登陸並受權後重定向到redirect_url

 

 

 能夠看到code,id_token,access_code都返回了。拿到code後能夠根據code去獲取access_token用於訪問被保護的api,參考以前的文章:IdentityServer4 實現OAuth2.0受權碼模式。也能夠直接拿返回的acess_token用,直接返回的access_token因爲是和隱藏模式同樣以url參數帶過來的,爲安全考慮,這個access_code的有效時間很段,默認是一個小時。

2,使用form_post方式

先在IdentityMvc項目新建一個Mvc控制器,用於接收post數據請求。

TokenData類用於包裝從IdentityServer處Post回來的token數據

public class TokenData
    {
        public string code { get; set; }
        public string id_token { get; set; }
        public string access_token { get; set; }
        public string token_type { get; set; }
        public string expires_in { get; set; }
        public string scope { get; set; }
        public string session_state { get; set; }
    }

  HomeController.GetTokenData,用於identityserver的redirect_url

[HttpPost]
        public IActionResult GetTokenData(TokenData data)
        {
            return new JsonResult(data);
        }

建好控制器後,把該控制器的訪問路徑添加IdentityServer的混合模式客戶端的RedirectUris

 new Client()
                {
                     AlwaysIncludeUserClaimsInIdToken=true,
                    //客戶端Id
                     ClientId="apiClientHybrid",
                     ClientName="ApiClient for HyBrid",
                     //客戶端密碼
                     ClientSecrets={new Secret("apiSecret".Sha256()) },
                     //客戶端受權類型,Hybrid:混合模式
                     AllowedGrantTypes=GrantTypes.Hybrid,
                     //容許登陸後重定向的地址列表,能夠有多個
                    RedirectUris = {"https://localhost:5002/auth.html","https://localhost:5002/home/gettokendata"},
                     //容許訪問的資源
                     //容許訪問的資源
                     AllowedScopes={
                        "secretapi",
                        IdentityServerConstants.StandardScopes.OpenId,
                        "PhoneModel"
                    },
                      AllowOfflineAccess = true,
                     AllowAccessTokensViaBrowser=true
                }

構造請求url。把response_mode設置爲form_post,把redirect_url設置爲控制器路徑  

http://localhost:5000/connect/authorize?client_id=apiClientHybrid&redirect_uri=https://localhost:5002/home/getTokenData&response_type=code token id_token&scope=secretapi openid PhoneModel&nonce=123&response_mode=form_post

 

 

 

四,id_token應用:單點登陸

id_token包含了用戶在openid和用戶基本信息,這代表了該用戶是有來源的,不是黑戶口,若是op(openid provider)值得依賴,第三方客戶端徹底能夠經過解析id_token獲取用戶信息容許用戶登陸,而不須要用戶從新註冊帳戶,從新登陸。

對於web應用來講,實現思路通常是這樣的:用戶打開頁面後,先在Cookie裏查詢有沒有id_token信息,若是有,驗證該id_token,驗證成功則容許訪問,驗證失敗或者Cookie裏沒有存儲id_token則去op請求id_token,用戶在op登陸成功並受權後,op返回id_token到第三方應用後,第三方應用把id_token存儲到cookie裏,用戶下次再打開頁面就走從新驗證id_token過程。如何驗證id_token,請參考https://jwt.io/

按時這個思路,能夠在任何平臺實現單點登陸。若是你用的是asp.net core Mvc平臺,微軟已經把一切都用中間件封裝好了,只須要幾行簡單的配置代碼。

測試步驟:

1,因爲asp.net core的OpenIdConnect驗證方案默認會添加"openid"以及"profile"兩個IdentityResource的請求權限,因此須要在IdentityServer添加這兩個IdentityResource

IdentityServer.Config

 public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new IdentityResource[]
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResource("PhoneModel","User's phone Model",new List<string>(){ "phonemodel","phoneprise"})

            };
        }

把這兩個請求權限受權Client

IdentityServer.Config.GetClients

  AllowedScopes={
                        "secretapi",
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        "PhoneModel"
                    }   

2,由asp.net core的OpenIdConnect驗證方案默認的登陸重定向地址爲signin-oidc,因此把這個地址加入到Client的RedirectUris

 RedirectUris = {"https://localhost:5002/auth.html","https://localhost:5002/home/gettokendata","https://localhost:5002/signin-oidc"},

附asp.net core OpenIdConnect驗證方案默認設置源碼,有幾個地方須要注意:

  CallbackPath:用於登陸重定向地址

  SingedOutCallbackPath:註銷登陸回調地址,後邊咱們加入註銷功能時會用到這個地址

  RemoteSignOutPath:註銷登陸重定向地址

  Scope.Add("openid")和Scope.Add("profile")解釋了上面第一步爲何要添加這兩個IdentityResource。

Microsoft.AspNetCore.Authentication.OpenIdConnect

 public OpenIdConnectOptions()
        {
            CallbackPath = new PathString("/signin-oidc");
            SignedOutCallbackPath = new PathString("/signout-callback-oidc");
            RemoteSignOutPath = new PathString("/signout-oidc");

            Events = new OpenIdConnectEvents();
            Scope.Add("openid");
            Scope.Add("profile");

            ClaimActions.DeleteClaim("nonce");
            ClaimActions.DeleteClaim("aud");
            ClaimActions.DeleteClaim("azp");
            ClaimActions.DeleteClaim("acr");
            ClaimActions.DeleteClaim("iss");
            ClaimActions.DeleteClaim("iat");
            ClaimActions.DeleteClaim("nbf");
            ClaimActions.DeleteClaim("exp");
            ClaimActions.DeleteClaim("at_hash");
            ClaimActions.DeleteClaim("c_hash");
            ClaimActions.DeleteClaim("ipaddr");
            ClaimActions.DeleteClaim("platf");
            ClaimActions.DeleteClaim("ver");

            // http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
            ClaimActions.MapUniqueJsonKey("sub", "sub");
            ClaimActions.MapUniqueJsonKey("name", "name");
            ClaimActions.MapUniqueJsonKey("given_name", "given_name");
            ClaimActions.MapUniqueJsonKey("family_name", "family_name");
            ClaimActions.MapUniqueJsonKey("profile", "profile");
            ClaimActions.MapUniqueJsonKey("email", "email");

            _nonceCookieBuilder = new OpenIdConnectNonceCookieBuilder(this)
            {
                Name = OpenIdConnectDefaults.CookieNoncePrefix,
                HttpOnly = true,
                SameSite = SameSiteMode.None,
                SecurePolicy = CookieSecurePolicy.SameAsRequest,
                IsEssential = true,
            };
        }

3,修改IdentityMvc的Privacy視圖控制器,使其必需通過id_token驗證後方能訪問

IdentityMvc.HomeController

    [Microsoft.AspNetCore.Authorization.Authorize]
        public IActionResult Privacy()
        {
            return View();
        }

4,修改Privacy視圖,展現id_token信息

Privacy.cshtml

@{
    ViewData["Title"] = "Privacy Policy";

}
<h1>@ViewData["Title"]</h1>

<h2>Claims</h2>

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

5,配置客戶端

IdentityMvc.StartUp.ConfigureServices

 services.AddAuthentication(opt=> {
                //默認驗證方案
                opt.DefaultScheme = "Cookies";
                //默認token驗證失敗後的確認驗證結果方案
                opt.DefaultChallengeScheme = "oidc";
            })
                //先添加一個名爲Cookies的Cookie認證方案
                .AddCookie("Cookies")
                //添加OpenIdConnect認證方案
                .AddOpenIdConnect("oidc", options =>
                {
                    //指定遠程認證方案的本地登陸處理方案
                    options.SignInScheme = "Cookies";
                    //遠程認證地址
                    options.Authority = "http://localhost:5000";
                    //Https強制要求標識
                    options.RequireHttpsMetadata = false;
                    //客戶端ID(支持隱藏模式和受權碼模式,密碼模式和客戶端模式不須要用戶登陸)
                    options.ClientSecret = "apiSecret";
                    //令牌保存標識
                    options.SaveTokens = true;
                    //添加訪問secretapi域api的權限,用於access_token
                    options.Scope.Add("secretapi");
                    //請求受權用戶的PhoneModel Claim,隨id_token返回
                    options.Scope.Add("PhoneModel");
                    //使用隱藏模式
                    options.ClientId = "apiClientImpl";
                    //請求返回id_token以及token
                    options.ResponseType = OpenIdConnectResponseType.IdTokenToken;
                });

IdentityMvc.Start.Config

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

注意:UseAuthentication必需在UseMvc前面。  

6,訪問https://localhost:5002/Home/Privacy,會重定向到IdentityServer登陸界面

 

 

 用戶受權後重定向回Mvc客戶端,已經進入Privacy視圖了。

 

 

 7,上面是隱藏模式測試,試一下混合模式,把ClientId改爲混合模式Client的Id,ResponseType改爲CodeIdTokenToken

  services.AddAuthentication(opt=> {
                //默認驗證方案
                opt.DefaultScheme = "Cookies";
                //默認token驗證失敗後的確認驗證結果方案
                opt.DefaultChallengeScheme = "oidc";
            })
                //先添加一個名爲Cookies的Cookie認證方案
                .AddCookie("Cookies")
                //添加OpenIdConnect認證方案
                .AddOpenIdConnect("oidc", options =>
                {
                    //指定遠程認證方案的本地登陸處理方案
                    options.SignInScheme = "Cookies";
                    //遠程認證地址
                    options.Authority = "http://localhost:5000";
                    //Https強制要求標識
                    options.RequireHttpsMetadata = false;
                    //客戶端ID(支持隱藏模式和受權碼模式,密碼模式和客戶端模式不須要用戶登陸)
                    options.ClientSecret = "apiSecret";
                    //令牌保存標識
                    options.SaveTokens = true;
                    //添加訪問secretapi域api的權限,用於access_token
                    options.Scope.Add("secretapi");
                    //請求受權用戶的PhoneModel Claim,隨id_token返回
                    options.Scope.Add("PhoneModel");
                    //使用混合模式
                    options.ClientId = "apiClientHybrid";
                    //請求返回code,id_token以及token
                    options.ResponseType = OpenIdConnectResponseType.CodeIdTokenToken;
                });

8,加入註銷登陸功能

  8.1 配置IdentityServer 客戶端的PostLogoutRedirectUris屬性,值爲第2步講的SingedOutCallbackPath值

IdentityServer.Config.GetClients

                     //容許登陸後重定向的地址列表,能夠有多個
                    RedirectUris = {"https://localhost:5002/auth.html","https://localhost:5002/signin-oidc"},
                    //註銷登陸的回調地址列表,能夠有多個
                    PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },

  8.2,在IdentityMvc項目的HomeController添加一個新的視圖控制器,用於註銷登陸

 public IActionResult Logout()
        {
            return SignOut("Cookies", "oidc");
        }

  8.3,把這個控制器加入佈局頁的菜單

<li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Logout">Logout</a>
                        </li>

  登陸後訪問Privacy頁面,而後訪問Logout視圖,會重定向到IdentityServer執行註銷邏輯

 

 

9,利用access_token訪問被保護的Api

  IdentityMvc.HomeController.Detail

  [Microsoft.AspNetCore.Authorization.Authorize]
        public async Task<IActionResult> Detail()
        {
            var client = new HttpClient();
            var token =await HttpContext.GetTokenAsync("access_token");
            client.SetBearerToken(token);
            string data = await client.GetStringAsync("https://localhost:5001/api/identity");
            JArray json = JArray.Parse(data);
            return new JsonResult(json);
        }

  訪問https://localhost:5002/home/detail,獲取access_token後請求被保護的api並顯示api返回結果。

 示例中的客戶端數據、資源數據、用戶數據都是放在Config類中,若是這些數據須要實時配置,能夠與Sql結合實現,下一篇IdentityServer4結合Mysql

相關文章
相關標籤/搜索