IdentityServer4揭秘---登錄


IdentityServer4默認提供了的登錄地址是Account/Index 同意頁面是Consent/Index

這里我們可以通過IdentittyServer4的用戶交互自定義配置設置

ConfigureServices服務中添加services.AddIdentityServer() 在參數中提供了UserInteraction設置

 

services.AddIdentityServer(options =>
            {
                options.UserInteraction = new IdentityServer4.Configuration.UserInteractionOptions
                {

                    LoginUrl = "/Account/Login",//【必備】登錄地址  
                    LogoutUrl = "/Account/Logout",//【必備】退出地址 
                    ConsentUrl = "/Account/Consent",//【必備】允許授權同意頁面地址
                    ErrorUrl = "/Account/Error", //【必備】錯誤頁面地址
                    LoginReturnUrlParameter = "ReturnUrl",//【必備】設置傳遞給登錄頁面的返回URL參數的名稱。默認為returnUrl 
                    LogoutIdParameter = "logoutId", //【必備】設置傳遞給注銷頁面的注銷消息ID參數的名稱。缺省為logoutId 
                    ConsentReturnUrlParameter = "ReturnUrl", //【必備】設置傳遞給同意頁面的返回URL參數的名稱。默認為returnUrl
                    ErrorIdParameter = "errorId", //【必備】設置傳遞給錯誤頁面的錯誤消息ID參數的名稱。缺省為errorId
                    CustomRedirectReturnUrlParameter = "ReturnUrl", //【必備】設置從授權端點傳遞給自定義重定向的返回URL參數的名稱。默認為returnUrl
                    
                    CookieMessageThreshold = 5 //【必備】由於瀏覽器對Cookie的大小有限制,設置Cookies數量的限制,有效的保證了瀏覽器打開多個選項卡,一旦超出了Cookies限制就會清除以前的Cookies值
                };


            })
                .AddDeveloperSigningCredential()
                .AddInMemoryIdentityResources(MemoryClients.GetIdentityResources())
                .AddInMemoryApiResources(MemoryClients.GetApiResources())
                .AddInMemoryClients(MemoryClients.GetClients());
View Code

 

這里我指定的都是在我的AccountController中,指定好了頁面,我們來開始做我們的登錄界面

登錄一般需要用戶名、密碼、記住密碼字段,但是在IdentityServer4中還提供了一個ReturnUrl,在Client端OIDC授權訪問的時候會轉接到IdenttityServer4服務端進行驗證並且構建好相關的ReturnUrl地址

ReturnUrl是一個非常重要的參數,它在整個授權過程中充當了重要的作用

想到登錄界面,分析好了模型,接下來就是構建模型 首先構建 界面視圖模型:LoginViewModel

public class LoginViewModel
    {
         /// <summary>
         /// 用戶名
         /// </summary>
        [Required]
        public string username { get; set; }
        /// <summary>
        /// 密碼
        /// </summary>
        [Required]
        public string password { get; set; }
        /// <summary>
        /// 界面上的選擇框  選擇是否記住登錄
        /// </summary>
        public bool RememberLogin { get; set; }
        /// <summary>
        /// 回調授權驗證地址 這個地址與Redirect地址不一樣
        /// 登錄成功后會轉到 ReturnUrl  然后驗證授權登錄后 獲取到客戶端的信息 然后根據Client配置中的RedirectUrl轉到對應的系統
        /// </summary>
        public string ReturnUrl { get; set; }
    }
LoginViewModel

登記界面會涉及到IdentityServer4相關交互,比如客戶端名稱ClientName 、ClientUrl等等

所以在登記界面我們在構建一個與IdentityServer4相關的模型類去繼承LoginViewModel,因為他們是在同一個界面展現:Idr4LoginViewModel

public class Idr4LoginViewModel : LoginViewModel
    {
        public bool AllowRememberLogin { get; set; }
        public bool EnableLocalLogin { get; set; }

        public IEnumerable<ExternalProvider> ExternalProviders { get; set; }
        //public IEnumerable<ExternalProvider> VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName));

        public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1;
        public string ExternalLoginScheme => ExternalProviders?.SingleOrDefault()?.AuthenticationScheme;


        public string ClientName { get; set; }
        public string ClientUrl { get; set; }
        public string ClientLogoUrl { get; set; }

    }
View Code

接下來就是構建登錄頁面的html,這里我構建的比較簡單沒有什么樣式 測試下就行了

@using SSOServer.Models;
@{
    ViewData["Title"] = "Index";
}
@model Idr4LoginViewModel
<h2>用戶登錄</h2>
<form asp-action="Login">

    @if (Model.EnableLocalLogin)
    {
        <div><img src="@Model.ClientLogoUrl" width="100" height="100" /></div>
        <div>@Model.ClientName</div>
        <div>@Model.ClientUrl</div>

    }
    <div>用戶名:<input type="text" asp-for="username" /></div>
    <div>密碼:<input type="text" asp-for="password" /></div>
    <input type="hidden" asp-for="ReturnUrl" />
    <button type="submit">登錄</button>
    <div asp-validation-summary="All"> </div>
</form>
HTML 登錄

這里也可以獲取Client的信息,都可以自定義按需求處理

在前面的UserInteraction中做了登錄界面的設置並且指定了參數ReturnUrl,所以到連接轉到視圖頁面時候,需要Get請求接受一個ReturnUrl的參數

 [HttpGet]
        public async Task<IActionResult> Login(string ReturnUrl)
        {
            //創建視圖模型
            var vm = await CreateIdr4LoginViewModelAsync(ReturnUrl);
            //判斷來之其他客戶端的登錄
            if (vm.IsExternalLoginOnly)
            {
                return await ExternalLogin(vm.ExternalLoginScheme, ReturnUrl);
            }
            return View(vm);
        }
Get LoginView

那么登錄界面怎么來做來,這里就需要介紹IdentityServer4中的幾個接口類了:

IIdentityServerInteractionService:用戶交互相關接口

IResourceStore:獲取資源接口:這里包括2中資源 一種是IdentityResource 和 ApiResource

IClientStore:獲取客戶端相關接口

IEventService:事件服務

UserStoreServices:自定義的用戶服務,這里我沒有用IdentityServer4的TestUserStore是為了方面自定義處理

轉到登錄視圖頁面,首先要做的就是構建視圖模型,頁面上要展示什么數據,包括用戶名,密碼,Idr4相關

這個時候就是ReturnUrl發揮其重要性的時候了:

DotNetCore自帶的有DependencyInjection這樣的依賴注入,可以不用Autofac之類也非常方便

在AccountController中注入相關接口

 private readonly IIdentityServerInteractionService _identityServerInteractionService;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly IAuthenticationSchemeProvider _schemeProvider;
        private readonly IResourceStore _resourceStore;
        private readonly IClientStore _clientStore;
        private readonly IEventService _events;
        private readonly UserStoreServices _testUserStore;

        //private readonly TestUserStore _testUserStore;
        public AccountController(IIdentityServerInteractionService identityServerInteractionService, UserStoreServices testUserStore, IEventService events, IHttpContextAccessor httpContextAccessor, IAuthenticationSchemeProvider schemeProvider, IClientStore clientStore, IResourceStore resourceStore)
        {
            _identityServerInteractionService = identityServerInteractionService;
            _testUserStore = testUserStore;
            _events = events;
            _httpContextAccessor = httpContextAccessor;
            _schemeProvider = schemeProvider;
            _clientStore = clientStore;
            _resourceStore = resourceStore;


        }
接口及構造

這里調用用戶交互接口以及客戶端接口構建如下

/// <summary>
        /// 構造下Idr4登陸界面顯示視圖模型
        /// </summary>
        /// <param name="ReturnUrl"></param>
        /// <returns></returns>
        private async Task<Idr4LoginViewModel> CreateIdr4LoginViewModelAsync(string ReturnUrl)
        {
            Idr4LoginViewModel vm = new Idr4LoginViewModel();
            var context = await _identityServerInteractionService.GetAuthorizationContextAsync(ReturnUrl);
            if (context != null)
            {
                if (context?.IdP != null)
                {
                    // 擴展外部擴展登錄模型處理
                    vm.EnableLocalLogin = false;
                    vm.ReturnUrl = ReturnUrl;
                    vm.username = context?.LoginHint;
                    vm.ExternalProviders = new ExternalProvider[] { new ExternalProvider { AuthenticationScheme = context.IdP } };

                }

            }
            //外部登陸 獲取所有授權信息 並查找當前可用的授權信息
            var schemes = await _schemeProvider.GetAllSchemesAsync();

            var providers = schemes
                .Where(x => x.DisplayName != null)
                .Select(x => new ExternalProvider
                {
                    DisplayName = x.DisplayName,
                    AuthenticationScheme = x.Name
                }).ToList();

            var allowLocal = true;

            if (context?.ClientId != null)
            {
                var client = await _clientStore.FindEnabledClientByIdAsync(context.ClientId);
                if (client != null)
                {
                    allowLocal = client.EnableLocalLogin;
                    vm.ClientName = client.ClientName;
                    vm.ClientUrl = client.ClientUri;
                    vm.ClientLogoUrl = client.LogoUri;
                    if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any())
                    {
                        providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList();
                    }
                }
            }
            vm.AllowRememberLogin = AccountOptions.AllowRememberLogin;
            vm.EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin;
            vm.ReturnUrl = ReturnUrl;
            vm.username = context?.LoginHint;
            vm.ExternalProviders = providers.ToArray();

            return vm;
        }
模型

IIdentityServerInteractionService 用戶交互下提供了很多接口方法,可以詳細了解下

對應代碼中的擴展登錄可以注釋掉 目前不做那塊相關

到了這里基本可以展示代碼了,下面運行下代碼看下:

本生的客戶端系統我寄宿到5001端口,IdentityServer4寄宿到5000端口,訪問5000中授權限制訪問頁面,會轉到Idr4 服務端

這里我們可以看到ReturnUrl,分析下這個地址:

http://localhost:5000/Account/Login?ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fclient_id%3Dliyouming%26redirect_uri%3Dhttp%253A%252F%252Flocalhost%253A5001%252Fsignin-oidc%26response_type%3Dcode%2520id_token%26scope%3Dopenid%2520profile%26response_mode%3Dform_post%26nonce%3D636592109017726544.MGI1MDJkNDYtMmUwOS00YmUxLWJmODgtODY0NWZlYzQyZGEyMjY1MGExMTItNjc3Yi00M2ExLWJhNmItZWM0OWRlYTEwOWQx%26state%3DCfDJ8GJf-n3goONOsPJOurEXDE-aBinqSDzf_TJntjbg5FIJpAFEeJm36TR7MxDhYJB_K3yzkedqbCi1P2V_F4dJ5wrOEbvhkVBJr447GQCdJKoFV1Ms2POKRn-_kB03Xp4ydGttsBUDJflnaLYcC3BnN7UTAcHV55ALZBTgGTNTGPnzIhotUonX9IM6SgOTaNZTmlwrIRz6s-XksqJQ5-gsnLXh_MRqcKAxzC3-HLIc34re2H6cTnJT1CNab0B7MxJGUpeOZ09_x7U7gw9DnF0aMvAae9-_dTPDgo2xEbMw9y5hLaFwIPfMbrftrHJoFI87tF-TmHHKm9NvJfLfueWZ02o%26x-client-SKU%3DID_NET%26x-client-ver%3D2.1.4.0

這里面有授權回調地址,就是登錄成功后會Post到 授權callback地址進行認證,成功后會轉到redirect_uri,這里面還指定了 請求的scope ,repsonsetype等等,可以看下oauth2相關資料

當登錄的時候我們需要一個Post的登錄Action,這里注意的是 這個ReturnUrl 會貫穿這個登錄流程,所以在登錄視圖界面會有一個隱藏域把這個存起來,在post請求的時候要帶過來

 [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(Idr4LoginViewModel model)
        {

            #region  Idr4驗證處理 這里主要對ReturnUrl處理

            var context = await _identityServerInteractionService.GetAuthorizationContextAsync(model.ReturnUrl);
            if (context == null)
            {
                //不存在客戶端信息
                Redirect("~/");

            }
            #endregion
            #region 基礎驗證
            if (string.IsNullOrEmpty(model.username))
            {
                ModelState.AddModelError("", "請輸入用戶名");
            }
            if (string.IsNullOrEmpty(model.password))
            {
                ModelState.AddModelError("", "請輸入密碼");
            }
            #endregion
            if (ModelState.IsValid)
            {
                if (_testUserStore.ValidatorUser(model.username, model.password))
                {

                    //查詢用戶信息
                    var user = await _testUserStore.GetByUserNameAsync(); //得到信息
                    await _events.RaiseAsync(new UserLoginSuccessEvent(user.username, user.guid.ToString(), user.username));
                    //記住登錄
                    AuthenticationProperties authenticationProperties = null;
                    if (AccountOptions.AllowRememberLogin && model.RememberLogin)
                    {
                        authenticationProperties = new AuthenticationProperties
                        {
                            IsPersistent = true,
                            ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
                        };
                    }
                    //SignIn
                    await HttpContext.SignInAsync(user.guid.ToString(), user.username, authenticationProperties);

                    if (_identityServerInteractionService.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl))
                    {
                        return Redirect(model.ReturnUrl);
                    }

                    return Redirect("~/");

                }
                else
                {

                    await _events.RaiseAsync(new UserLoginFailureEvent(model.username, "登錄失敗"));
                    ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage);
                }
            }
            //防止驗證失敗后返回視圖后 界面模型參數不存在 所以這里需要構建一次模型
            var vm = await CreateIdr4LoginViewModelAsync(model.ReturnUrl);
            return View(vm);
        }
Post Login

post里面就可以做一些處理就行了比如驗證之類,驗證失敗或者處理失敗都要回到登錄頁面上,所以最后還是需要構建一次視圖模型返回到View上

到這里登錄基本就結束了

在擴充一點內存配置

public class MemoryClients
    {


        public static List<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResource{
                    Name="openid",
                     Enabled=true,
                      Emphasize=true,
                       Required=true,
                       DisplayName="用戶授權認證信息",
                       Description="獲取你的授權認證"
        },
                new IdentityResource{
                    Name="profile",
                     Enabled=true,
                      Emphasize=false,
                       Required=true,
                        DisplayName="用戶個人信息",
                         Description="獲取你的個人基本資料信息,如:姓名、性別、年齡等"
                }

};

        }
        public static List<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
               //普通的通過構造函數限制 指定scope以及displayname 就行了
             //  new ApiResource("liyouming","打印雲服務接口")

               //做一些更加嚴格的限制要求
               new  ApiResource(){
                    Enabled=true,
                    Name="liyouming",
                    DisplayName="打印雲服務接口",
                    Description="選擇允許即同意獲取你的個人打印服務權限",
                    Scopes={
                                new Scope()
                                {

                                    Emphasize=false,
                                    Required=false,
                                    Name="liyouming",
                                    DisplayName="打印雲服務接口",
                                    Description="選擇允許即同意獲取你的個人打印服務權限"
                                }
                            }

               }
            };
        }

        public static List<Client> GetClients()
        {
            return new List<Client> {
                new Client(){

                    ClientId="liyouming",
                    ClientName="ChinaNetCore",
                    ClientUri="http://www.chinanetcore.com",
                    LogoUri="http://img05.tooopen.com/images/20160109/tooopen_sy_153858412946.jpg",
                    ClientSecrets={new Secret("liyouming".Sha256()) },
                    AllowedGrantTypes= GrantTypes.Hybrid,
                    AccessTokenType= AccessTokenType.Jwt,
                    RequireConsent=true,
                    RedirectUris={ "http://localhost:5001/signin-oidc" },
                    PostLogoutRedirectUris={"http://localhost:5001/signout-callback-oidc" },
                    AllowedScopes={
                       "openid",
                       "profile",
                       "liyouming",

                    },
                    BackChannelLogoutUri="http://localhost:5001/Default/LogoutByElse",
                    BackChannelLogoutSessionRequired=true
                   


                }
            };
        }


    }
Client Resource 配置

其他站點請求授權可OIDC配置,在DotNetCore中自帶了OpenIdConnect

    services.AddAuthentication(option =>
            {
                option.DefaultScheme = "Cookies";
                option.DefaultChallengeScheme = "oidc";
            })
            .AddCookie("Cookies")
            .AddOpenIdConnect("oidc", options =>
            {
                options.SignInScheme = "Cookies";
                options.Authority = "http://localhost:5000";
                options.RequireHttpsMetadata = false;
                options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
                options.ClientId = "liyouming";
                options.ClientSecret = "liyouming";
                options.SignedOutRedirectUri = "http://localhost:5001/signout-callback-oidc";
                options.SaveTokens = false;
              
                options.Events = new Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents {
                    OnRedirectToIdentityProviderForSignOut= OnRedirectToIdentityProviderForSignOut

                };

            });
代碼

這里配置好並添加好相關Controller的授權訪問后即可

登錄失敗后提示

登錄成功后

這里來到了Conset授權同意頁面,這里我在后面繼續講解

同意后進入授權訪問頁面

 

 登錄到這里就結束了,后面會繼續介紹 Consent  及 Logout等操作和其他一些DotNetCore相關實戰運用  

 


免責聲明!

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



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