ASP.NET Core3.1使用IdentityServer4中間件系列隨筆(五):創建使用[Code-授權碼]授權模式的客戶端

配套源碼:https://gitee.com/jardeng/IdentitySolution

 

本篇將創建使用[Code-授權碼]授權模式的客戶端,來對受保護的API資源進行訪問。

1、接上一篇項目,因爲之前創建IdentityServer認證服務器沒有使用IdentityServer4提供的模板,在Code授權碼模式就沒有進行登錄、授權的界面,所以重新創建一下IdentityServer項目。

重新使用IdentityServer4模板 - is4inmem創建項目。

將之前IdentityServer認證服務器Config.cs複製到新建的IdentityServer服務器即可,最後的IdentityServer認證服務器項目結構爲:

然後在IdentityServer項目Config.cs中添加一個返回身份資源的方法

然後在IdentityServer項目Config.cs中添加一個客戶端

注意:localhost:6001指的是我們將要創建的MVC客戶端的項目地址,並非IdentityServer認證服務器的地址

/// 授權碼模式(Code)
///     適用於保密客戶端(Confidential Client),比如ASP.NET MVC等服務器端渲染的Web應用
new Client
{
    ClientId = "mvc client",
    ClientName = "ASP.NET Core MVC Client",

    AllowedGrantTypes = GrantTypes.Code,
    ClientSecrets = { new Secret("mvc secret".Sha256()) },

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

    AlwaysIncludeUserClaimsInIdToken = true,
    AllowOfflineAccess = true,
    AllowedScopes =
    {
        "api1",
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
        IdentityServerConstants.StandardScopes.Email,
        IdentityServerConstants.StandardScopes.Address,
        IdentityServerConstants.StandardScopes.Phone
    }
}

其中,RedirectUris的signin-oidc / FrontChannelLogoutUri的signout-oidc / PostLogoutRedirectUris的signout-callback-oidc,都是固定的地址寫法。

完整的Config.cs代碼:

using IdentityModel;
using IdentityServer4;
using IdentityServer4.Models;
using IdentityServer4.Test;
using System.Collections.Generic;
using System.Security.Claims;

namespace IdentityServer
{
    /// <summary>
    /// IdentityServer資源和客戶端配置文件
    /// </summary>
    public static class Config
    {
        /// <summary>
        /// 身份資源集合
        /// </summary>
        public static IEnumerable<IdentityResource> Ids =>
            new IdentityResource[]
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Email(),
                new IdentityResources.Address(),
                new IdentityResources.Phone()
            };

        /// <summary>
        /// API資源集合
        ///     如果您將在生產環境中使用此功能,那麼給您的API取一個邏輯名稱就很重要。
        ///     開發人員將使用它通過身份服務器連接到您的api。
        ///     它應該以簡單的方式向開發人員和用戶描述您的api。
        /// </summary>
        public static IEnumerable<ApiResource> Apis => new List<ApiResource> { new ApiResource("api1", "My API") };

        /// <summary>
        /// 客戶端集合
        /// </summary>
        public static IEnumerable<Client> Clients =>
            new Client[]
            {
                /// 客戶端模式(Client Credentials)
                ///     可以將ClientId和ClientSecret視爲應用程序本身的登錄名和密碼。
                ///     它將您的應用程序標識到身份服務器,以便它知道哪個應用程序正在嘗試與其連接。
                new Client
                { 
                    //客戶端標識
                    ClientId = "client",
                    //沒有交互用戶,使用clientid/secret進行身份驗證,適用於和用戶無關,機器與機器之間直接交互訪問資源的場景。
                    AllowedGrantTypes = GrantTypes.ClientCredentials,
                    //認證密鑰
                    ClientSecrets = { new Secret("secret".Sha256()) },
                    //客戶端有權訪問的作用域
                    AllowedScopes = { "api1" }
                },
                /// 資源所有者密碼憑證(ResourceOwnerPassword)
                ///     Resource Owner其實就是User,所以可以直譯爲用戶名密碼模式。
                ///     密碼模式相較於客戶端憑證模式,多了一個參與者,就是User。
                ///     通過User的用戶名和密碼向Identity Server申請訪問令牌。
                new Client
                {
                    ClientId = "client1",
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                    ClientSecrets = { new Secret("secret".Sha256()) },
                    AllowedScopes = { "api1" }
                },           
                /// 授權碼模式(Code)
                ///     適用於保密客戶端(Confidential Client),比如ASP.NET MVC等服務器端渲染的Web應用
                new Client
                {
                    ClientId = "mvc client",
                    ClientName = "ASP.NET Core MVC Client",

                    AllowedGrantTypes = GrantTypes.Code,
                    ClientSecrets = { new Secret("mvc secret".Sha256()) },

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

                    AlwaysIncludeUserClaimsInIdToken = true,
                    AllowOfflineAccess = true,
                    AllowedScopes =
                    {
                        "api1",
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.Email,
                        IdentityServerConstants.StandardScopes.Address,
                        IdentityServerConstants.StandardScopes.Phone
                    }
                }
            };

        /// <summary>
        /// 用戶集合
        /// </summary>
        public static List<TestUser> Users =>
            new List<TestUser>
            {
                new TestUser{SubjectId = "818727", Username = "alice", Password = "alice",
                    Claims =
                    {
                        new Claim(JwtClaimTypes.Name, "Alice Smith"),
                        new Claim(JwtClaimTypes.GivenName, "Alice"),
                        new Claim(JwtClaimTypes.FamilyName, "Smith"),
                        new Claim(JwtClaimTypes.Email, "[email protected]"),
                        new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                        new Claim(JwtClaimTypes.WebSite, "http://alice.com"),
                        new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json)
                    }
                },
                new TestUser{SubjectId = "88421113", Username = "bob", Password = "bob",
                    Claims =
                    {
                        new Claim(JwtClaimTypes.Name, "Bob Smith"),
                        new Claim(JwtClaimTypes.GivenName, "Bob"),
                        new Claim(JwtClaimTypes.FamilyName, "Smith"),
                        new Claim(JwtClaimTypes.Email, "[email protected]"),
                        new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                        new Claim(JwtClaimTypes.WebSite, "http://bob.com"),
                        new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json),
                        new Claim("location", "somewhere")
                    }
                }
            };
    }
}
View Code

2、創建一個名爲 CodeMvcApp 的ASP.NET Core MVC客戶端應用。

 選擇Web 應用程序(模型視圖控制器)模板

 

 創建完成後的項目截圖

3、添加nuget包:IdentityServer4、IdentityModel、System.IdentityModel.Tokens.Jwt

4、配置MVC客戶端

> Config.cs的ConfigureServices方法:

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

    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    }).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
    {
        options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.Authority = "http://localhost:5000";
        options.RequireHttpsMetadata = false;
        options.ClientId = "mvc client";
        options.ClientSecret = "mvc secret";
        options.SaveTokens = true;
        options.ResponseType = "code";

        options.Scope.Clear();
        options.Scope.Add("api1");
        options.Scope.Add(OidcConstants.StandardScopes.OpenId);
        options.Scope.Add(OidcConstants.StandardScopes.Profile);
        options.Scope.Add(OidcConstants.StandardScopes.Email);
        options.Scope.Add(OidcConstants.StandardScopes.Phone);
        options.Scope.Add(OidcConstants.StandardScopes.Address);
        options.Scope.Add(OidcConstants.StandardScopes.OfflineAccess);
    });
}

> Config.cs的Configure方法:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

給HomeController控制器加上[Authorize]特性

IdentityServer認證服務器需要在開發環境才能出現首頁,所以另外打開這個項目並啓動。

 再啓動CodeMvcApp項目

我們看到MVC客戶端默認跳轉到了localhost:5000(IdentityServer認證服務器)的登錄頁(Account/Login),因爲MVC客戶端默認啓動的是Home/Index,且Home控制器已被標記Authorize特性,需要登錄才能訪問

使用 alice / alice 進行登錄,進入到了IdentityServer認證服務器的授權頁面(consent),點擊Yes, Allow

進入到了MVC客戶端首頁

 

我們打開IdentityServer認證服務器地址:http://localhost:5000

可以看到IdentityServer認證服務器顯示了當前的登錄用戶,此時點擊用戶名可以顯示出Logout登出按鈕,點擊登出即可完成註銷登錄

5、獲取accecc_token並訪問受保護API資源,修改HomeController的Index方法

public async Task<IActionResult> Index()
{
    HttpClient client = new HttpClient();
    DiscoveryDocumentResponse disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000/");
    if (disco.IsError)
    {
        throw new Exception(disco.Error);
    }

    string accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
    client.SetBearerToken(accessToken);

    HttpResponseMessage response = await client.GetAsync("http://localhost:6000/WeatherForecast");
    if (!response.IsSuccessStatusCode)
    {
        throw new Exception(response.ReasonPhrase);
    }

    string content = await response.Content.ReadAsStringAsync();
    return View("Index", content);
}

命名空間

using System;
using System.Diagnostics;
using System.Net.Http;
using System.Threading.Tasks;

using CodeMvcApp.Models;

using IdentityModel.Client;

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
View Code

修改Index.cshtml來顯示訪問API的結果

@{
    ViewData["Title"] = "Home Page";
}
@model string

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

<p>@Model</p>

重新啓動MVC客戶端,成功獲取access_token,並使用access_tokem訪問受保護的API資源

 6、顯示登錄的用戶,並實現登出

修改Views/Shared/_Layout.cshtml,增加當前登錄用戶名稱和登出按鈕的顯示

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - CodeMvcApp</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">CodeMvcApp</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
                    <ul class="navbar-nav flex-grow-1" style="position: relative;">
                        <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>
                        @if (User.Identity.IsAuthenticated)
                        {
                            <li class="nav-item" style="position: absolute; right: 0;">
                                <span>Welcome,@User.Claims.FirstOrDefault(x => x.Type.Equals("given_name")).Value</span>
                                <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Logout" style="display: inline-block;">Logout</a>
                            </li>
                        }
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2020 - CodeMvcApp - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @RenderSection("Scripts", required: false)
</body>
</html>
View Code

修改HomeController,增加Logout方法

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

public async Task Logout()
{
    await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
}
View Code

重新運行項目,導航欄右側就顯示了當前用戶名和登出按鈕

 點擊Logout登出,跳轉到了IdentityServer認證服務器的登出頁面(Account/Logout),此時已經登出了,但是界面停在了IdentityServer的註銷成功頁面

點擊“here”,可以跳轉到MVC客戶端,但是不是很友好

 此時我們打開IdentityServer認證服務器地址:http://localhost:5000,看到IdentityServer認證服務器的用戶已經顯示被註銷

然後來解決上面不友好的問題,修改IdentityServer服務器,打開Quickstart/Account/AccountOptions.cs,將AutomaticRedirectAfterSignOut設置爲true,即登出後自動跳轉

修改完成後重啓IdentityServer認證服務器,再重啓MVC客戶端即可解決。

 

Over, Thanks!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章