Blazor與IdentityServer4的集成


 本文合並整理自 CSDN博主「65號腕」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。

Blazor與IdentityServer4的集成(一)

IdentityServer4是開源的基於.Net Core的鑒權中間件,有興趣的可以去https://github.com/IdentityServer/IdentityServer4自行了解。

Blazor分為WebAssembly和Server兩種模式,由於到目前為止,筆者主要學習的是WebAssembly模式,所以本文就來嘗試一下WebAssembly與IdentityServer4的集成。

1. 安裝IdentityServer4

安裝IdentityServer4其實很簡單,官方提供了命令行讓你可以從零開始創建一個可以運行的網站,首先創建一個空白解決方案,然后進入解決方案所在目錄,並按順序執行以下命令:

//安裝IdentityServer4的項目模板
dotnet new -i IdentityServer4.Templates

//創建一個空項目,並配置IdentityServer4中間件
dotnet new is4empty -n IdentityServer(這是項目名字,你也可以指定其他名字)

//進入項目所在目錄
cd IdentityServer

//安裝QuickStart, 此時會創建需要的Controller,View等
dotnet new is4ui

最后將新創建的項目添加到你的解決方案,並修改一下Startup.cs文件:

 

 

 主要修改有兩個,一個是把QuickStart提供的測試用戶加入系統(這個很重要,否則Blazor那邊將取不到用戶的基本資料),另一個是打開MVC服務。

現在運行起來就能看到以下頁面了,嘗試點擊紅色框登錄試一試,測試的用戶名和密碼在Quickstart\TestUsers.cs文件當中。

 

2. 配置IdentityServer4

接下來就是配置IdentityServer, 包括三個方面:

  •     Identity Resource
  •     Api Scope
  •     Client


全部配置全都在Config.cs里面,修改起來也很方便,這里主要介紹一下Client的配置,我們需要創建一個Client用來給Blazor登錄使用,代碼如下:

public static IEnumerable<Client> Clients =>
    new Client[]
    {
        new Client
        {
            //唯一id,用來區分不同的Client
            ClientId = "blazorwasm",
            //使用的授權方式
            AllowedGrantTypes = GrantTypes.Code,
            //這里設置為不需要安全碼,當然也可以指定安全碼
            RequireClientSecret = false,
            //Blazor運行時的URL
            AllowedCorsOrigins =     { "https://localhost:5000" },
            //登錄成功之后將要跳轉的Blazor的URL
            RedirectUris = { "https://localhost:5000/authentication/login-callback" },
            //登出之后將要跳轉的Blazor的URL
            PostLogoutRedirectUris = { "https://localhost:5000/" },
            //允許的Scope,openid包含用戶id,profile包含用戶基本資料,api為自定義的scope,也可以為其他名字
            AllowedScopes = { "openid", "profile",  "api" },
        }
    };

到這里,IdentityServer已經完全可以滿足我們Blazor登錄的需求了。

3. 配置Blazor

接下來我們就添加一個Blazor WebAssembly項目,然后開始Blazor這邊的工作。

3.1 安裝依賴

首先需要安裝微軟的認證庫Microsoft.AspNetCore.Components.WebAssembly.Authentication,通過nuget安裝即可。

然后修改_Imports.razor,導入必要的命名空間

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

最后修改index.html,引入所需要的javascript文件:

<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></s

3.2 配置連接

該方案是通過Open Id的方式來連接IdentityServer,因此需要配一個Open Id 的客戶端連接,跟IdentityServer的配置相匹配。

首先修改Program.cs的main函數,添加Open Id服務

builder.Services.AddOidcAuthentication(options =>
{
    builder.Configuration.Bind("Local", options.ProviderOptions);
});

當中的Local對應的是appsettings.json里面的一個配置節,默認是沒有該文件的,因此我們需要先在wwwroot目錄下添加該文件,內容如下:

{
  "Local": {
    "Authority": "https://localhost:5001",
    "ClientId": "blazorwasm",
    "DefaultScopes": [
      "api"
    ],
    "PostLogoutRedirectUri": "/",
    "ResponseType": "code"
  }
}
  • Authority: 對應的是IdentityServer的URL
  • ClientId: 對應IdentityServer里面配置的ClientId
  • DefaultScopes: 需要是IdentityServer里面配置的AllowedScopes中的一個或多個,由於openid和profile是默認添加的,因此不需要聲明。
  • PostLogoutRedirectUri: 登出之后的跳轉地址
  • ResponseType: 對應IdentityServer里面配置的GrantTypes

3.3 編寫組件

一切准備就緒,現在可以開始編寫Blazor組件了。

首先我們修改一下App.razor,需要在最外層加上CascadingAuthenticationState,如下:

<CascadingAuthenticationState>
    ...
</CascadingAuthenticationState>

這個組件將允許各組件之間共享認證狀態。

然后我們再添加一個登錄頁面 Pages/Authentication.razor,代碼如下:

@page "/authentication/{action}"

<RemoteAuthenticatorView Action="@Action" />
@code {
    [Parameter]
    public string Action { get; set; }
}

她調用了認證庫里面的RemoteAuthenticatorView組件,來幫助我們實現跳轉。

接下來我們添加一個組件,用來顯示登錄狀態以及登錄登出按鈕, 文件名為 Shared/LoginDisplay.razor, 代碼如下:

@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity.Name!
        <button class="nav-link btn btn-link" @onclick="BeginSignOut">
            Log out
        </button>
    </Authorized>
    <NotAuthorized>
        <a href="authentication/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code {
   
    private async Task BeginSignOut(MouseEventArgs args)
    {
        await SignOutManager.SetSignOutState();
        Navigation.NavigateTo("authentication/logout");
    }
}

功能也比較簡單,登錄登出都是路由到上一步添加的Authentication.razor。

最后我們在布局頁MainLayout.razor的適當位置添加LoginDisplay組件即可。

最后運行效果如下:

 

 4. 源碼

https://gitee.com/bigname65/blazor-identity-server4

 

Blazor與IdentityServer4的集成(二)

我們在上一篇Blazor與IdentityServer4的集成(一)中完成了基本的配置和登錄登出的操作。

本篇主要完成授權的操作,包括:

  •     未登錄的用戶訪問特定頁面時自動跳到登錄頁面
  •     限定頁面給不同的角色訪問
  •     根據不同的角色顯示頁面不同的區域


基於上一篇的實例,我們假定有兩個角色,分別是管理員(Admin)和普通用戶(User),普通用戶可以訪問Counter頁面,管理員除了Counter頁面還可以訪問Fetch data頁面。

1. 配置IdentityServer

首先要做的就是給用戶加上角色定義,找到 “Quickstart/TestUsers.cs” 文件, 這里面定義了兩個測試用戶,我們給alice分配Admin角色,給bob分配User角色:

return new List<TestUser>
{
    new TestUser
    {
        SubjectId = "818727",
        //...
        Username = "alice",
        Claims =
        {
            //...
            //添加Admin角色
            new Claim(JwtClaimTypes.Role,"Admin")
        }
    },
    new TestUser
    {
        SubjectId = "88421113",
        Username = "bob",
        //...
        Claims =
        {
            //...
            //添加User角色
            new Claim(JwtClaimTypes.Role,"User")
        }
    }
};

當然這里只是測試使用,如果應用到實際項目中,需要實現自己的ProfileService。

然后就是修改IdentityServer的配置,把角色屬性暴露出去,找到 “Config.cs” 文件

public static class Config
{
    public static IEnumerable<IdentityResource> IdentityResources =>
        new IdentityResource[]
        {
            //...
            new IdentityResource("role", new string[]{JwtClaimTypes.Role })
        };
    //...
    public static IEnumerable<Client> Clients =>
        new Client[]
        {
            new Client
            {
                //...
                AllowedScopes = { "openid", "profile", "role",  "api" },
            }
        };
}

主要修改有兩個,一個是增加角色的IdentityResource,名為role,另一個是將role增加到Client的scope里面,允許客戶端獲取角色信息。

2. Blazor相關修改

2.1 修改啟動參數

主要有兩個, 一個是修改 “wwwroot/appsettings.json” 如下:

{
  "Local": {
    //...
    "DefaultScopes": [
      "role",
      //...
    ],
    //...
  }
}

她的目的是登錄的時候告訴IdentityServer,需要返回role給Blazor。

另一個修改是Program.cs:

public static async Task Main(string[] args)
{
    //...
    builder.Services.AddOidcAuthentication(options =>
    {
        //增加該行
        options.UserOptions.RoleClaim = "role";

    });
    //...
}

她的意思是初始化上下文的時候從ClaimType=role的claims里面初始化角色信息,只有這樣才能和IdentityServer返回的ClaimType對應上(當然你也可以在IdentityServer使用其他的ClaimType,只要兩邊一致就行了)。

2.2 添加RedirectToLogin組件

我們把該組件放到Shared目錄下.

@inject NavigationManager Navigation
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo($"authentication/login?returnUrl=" +
            Uri.EscapeDataString(Navigation.Uri));
    }
}

她的作用就是用來自動跳轉到登錄頁,“authentication/login” 這個地址其實就是上一篇添加的Authentication組件,她會進一步完成與IdentityServer的交互。

2.3 修改App.razor

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData"
                                DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    @if (!context.User.Identity.IsAuthenticated)
                    {
                        <RedirectToLogin />
                    }
                    else
                    {
                        <p>
                            You are not authorized to access
                            this resource.
                        </p>
                    }
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

這里用到了AuthorizeRouteView這個組件,她可以幫忙完成授權操作,授權失敗之后分兩種情況:

    如果是未登錄用戶,則啟用RedirectToLogin組件,跳到登錄頁。
    如果已登錄,但沒有足夠的權限,則提示用戶。

2.4 給頁面添加授權

接下來就是給不同的頁面添加不同的授權聲明,按前面的假定條件,我們修改Counter組件,添加以下權限:

@attribute [Authorize(Roles = "admin,user")]

做過MVC的對Authorize屬性一定不陌生了,她在Microsoft.AspNetCore.Authorization命名空間下,我們在_Imports.razor頁面添加該引用就可以了,不需要在每個頁面單獨引用。
這里就是限定了該頁面只有admin和user這兩個角色可以訪問。
修改FetchData組件,添加:

@attribute [Authorize(Roles = "admin")]

到這里已經可以看到效果了, 點擊菜單訪問需要授權的頁面的時候會自動跳轉,登錄完自動返回:

 

 

 

2.5 根據角色隱藏菜單

很多時候我們都不希望用戶看到不屬於她的菜單,這時候就需要用到AuthorizeView組件,她一樣也可以指定角色,我們修改以下菜單組件“NavMenu.razor”如下:

<AuthorizeView Roles="Admin,User">
    <li class="nav-item px-3">
        <NavLink class="nav-link" href="counter">
            <span class="oi oi-plus" aria-hidden="true"></span> Counter
        </NavLink>
    </li>
</AuthorizeView>
<AuthorizeView Roles="Admin">
    <li class="nav-item px-3">
        <NavLink class="nav-link" href="fetchdata">
            <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
        </NavLink>
    </li>
</AuthorizeView>

3. 源碼

https://gitee.com/bigname65/blazor-identity-server4

 

Blazor與IdentityServer4的集成(三)-如何處理多角色的問題?

 

前一篇完成了基於角色的權限控制,但是如果一個用戶有多個角色,就會有問題。

原因是因為IdentityServer的多個角色是放在同一個Claim里面,她以JSON數組的形式存在,但是前面提到的Blazor認證組件是識別不了的。

因此我們需要做一個轉換,Blazor提供了方式讓我們靈活的實現自己的擴展。

首先建一個自己的工廠類:

using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

   public class CustomUserFactory 
        : AccountClaimsPrincipalFactory<RemoteUserAccount>
    {
        public CustomUserFactory(IAccessTokenProviderAccessor accessor)
        : base(accessor)
        {
        }

        public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
            RemoteUserAccount account,
            RemoteAuthenticationUserOptions options)
        {
            var user = await base.CreateUserAsync(account, options);

            if (user.Identity.IsAuthenticated)
            {
                var identity = (ClaimsIdentity)user.Identity;
                var roleClaims = identity.FindAll(identity.RoleClaimType);

                if (roleClaims != null && roleClaims.Any())
                {
                    foreach (var existingClaim in roleClaims)
                    {
                        identity.RemoveClaim(existingClaim);
                    }

                    var rolesElem = account.AdditionalProperties[identity.RoleClaimType];

                    if (rolesElem is JsonElement roles)
                    {
                        if (roles.ValueKind == JsonValueKind.Array)
                        {
                            foreach (var role in roles.EnumerateArray())
                            {
                                identity.AddClaim(new Claim(options.RoleClaim, role.GetString()));
                            }
                        }
                        else
                        {
                            identity.AddClaim(new Claim(options.RoleClaim, roles.GetString()));
                        }
                    }
                }
            }

            return user;
        }
    }

主要就是從當前用戶的聲明中提取角色信息並重新組裝。

然后在Program.cs中注冊該工廠類就可以了

builder.Services.AddOidcAuthentication(options =>
{
    //...
}).AddAccountClaimsPrincipalFactory<CustomUserFactory>();

源碼請查看:

https://gitee.com/bigname65/blazor-identity-server4

 

Blazor與IdentityServer4的集成(四)-如何實現基於權限的授權

 

前兩篇(二),(三)實現了基於角色的授權,可以基於角色顯示不同的頁面,或者顯示頁面中不同的區域。但是實際項目當中,授權往往更精細,也更靈活。角色很有可能不是固定的,而是由用戶自己創建,然后分配了不同的操作權限。

本篇就以這種動態權限分配的場景,來看看如何實現授權的控制。

1. 配置IdentityServer

IdentityServer作為認證與授權的中心,首先需要把權限信息暴露出來。前兩篇我們增加了類型為role的Claim,這次我們再增加一個類型為permission的Claim。

修改Config.cs:

public static class Config
{
    public static IEnumerable<IdentityResource> IdentityResources =>
        new IdentityResource[]
        {
            //...
            //增加名為permission的IdentityResource,使用類型為permission的Claim
            new IdentityResource("permission",new string[]{ "permission" })
        };

    public static IEnumerable<Client> Clients =>
        new Client[]
        {
            new Client
            {
                ClientId = "blazorwasm",
                //...
                //允許客戶端獲取名稱為permission的Resource
                AllowedScopes = { "permission",  /*others*/ },
            }
        };
}

修改Quicksart/TestUsers.cs,給不同的測試用戶增加不同的Claim,這里增加了4個權限:create,retrieve,update,delete來作為演示。注意這里只是測試用戶,實際項目需要實現自己的ProfileService。

public static List<TestUser> Users
{
    get
    {
        return new List<TestUser>
        {
            new TestUser
            {
                Username = "alice",
                Claims =
                {
                    //...
                    new Claim("permission","create"),
                    new Claim("permission","retrieve"),
                    new Claim("permission","update"),
                    new Claim("permission","delete")
                }
            },
            new TestUser
            {
                Username = "bob",
                Claims =
                {
                    //...
                    new Claim("permission","retrieve")
                }
            },
            new TestUser
            {
                Username = "power",
                Claims =
                {
                    //...
                    new Claim("permission","retrieve"),
                    new Claim("permission","update")
                }
            }
        };
    }
}

2. 配置Blazor客戶端

2.1 修改請求的Scope

修改wwwroot/appsettings.json,這是要告訴IdentityServer返回permission信息。

{
  "Local": {
    "DefaultScopes": [
      //others
      "permission",
    ]
    //...
  }
}

2.2 修改CustomUserFactory.cs

上一篇我們增加了CustomUserFactory,目的是為了處理多個role的情況,這次的permission也一樣,也會涉及到多個,因此我們用同樣的方法處理就行了,修改一下,使她可以同時處理role和permission的Claim:

public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
    RemoteUserAccount account,
    RemoteAuthenticationUserOptions options)
{
    var user = await base.CreateUserAsync(account, options);

    if (user.Identity.IsAuthenticated)
    {
        var identity = (ClaimsIdentity)user.Identity;
        ParseArrayClaims(account, identity, identity.RoleClaimType, options.RoleClaim);
        ParseArrayClaims(account, identity, "permission", "permission");
    }

    return user;
}

private void ParseArrayClaims(RemoteUserAccount account, 
    ClaimsIdentity identity, 
    string srcClaimType, string destClaimType)
{
    var srcClaims = identity.FindAll(srcClaimType);

    if (srcClaims != null && srcClaims.Any())
    {
        foreach (var existingClaim in srcClaims)
        {
            identity.RemoveClaim(existingClaim);
        }

        var srcEle = account.AdditionalProperties[srcClaimType];

        if (srcEle is JsonElement srcClaimValues)
        {
            if (srcClaimValues.ValueKind == JsonValueKind.Array)
            {
                foreach (var srcValue in srcClaimValues.EnumerateArray())
                {
                    identity.AddClaim(new Claim(destClaimType, srcValue.GetString()));
                }
            }
            else
            {
                identity.AddClaim(new Claim(destClaimType, srcClaimValues.GetString()));
            }
        }
    }
}

2.3 基於決策的授權

前兩篇用到了Authorize屬性,以及AuthorizeView組件,她們的用法如下:

[Authorize(Roles = "xxxx")]
<AuthorizeView Roles="xxx">
</AuthorizeView>

那么最重要的就是定義決策了,我們需要修改program.cs:

builder.Services.AddAuthorizationCore(option =>
{
    string[] permissions = new string[]
    {
        "create",
        "retrieve",
        "update",
        "delete"
    };
    foreach (var p in permissions)
    {
        //為每個permission生成一個決策
        option.AddPolicy(p, policy =>
        {
            //該決策需要一個對應的type=permission的Claim
            policy.RequireClaim("permission", new string[] { p });
        });
    }
});

打完收工,相關源碼請查看:

https://gitee.com/bigname65/blazor-identity-server4

Blazor與IdentityServer4的集成(五)-保護你的Web Api

前面幾篇集成了Blazor和IdentityServer,完成了登錄以及指定頁面的授權操作。本篇來實現與Web Api的集成,當然主要還是Web Api的認證操作。

在MVC模式下,通常是借助Cookie,但是Web Api更主流的還是借助Access Token(尤其是Json Web Token),具體的概念這里就不展開說了。

結合IdentityServer的例子,具體流程如下:

  •     Blazor通過IdentityServer完成登錄。
  •     Blazor向IdentityServer請求一個Access Token用來訪問對應的Api(默認是Json Web Token)。
  •     Blazor訪問Api的時候在Http頭里面帶上Token。
  •     Api解析Token,並向IdentityServer驗證合法性,然后完成認證操作。

1 配置IdentityServer

其實在前面的例子中,我們已經配好了IdentityServer,回顧一下IdentityServer的Config.cs文件:

我們已經配置好了ApiScopes:

public static IEnumerable<ApiScope> ApiScopes =>
    new ApiScope[]
    { 
        //這里的ApiScope就是Web Api對應的標識
        new ApiScope("api")
    };

public static IEnumerable<Client> Clients =>
    new Client[]
    {
        new Client
        {
            //對應上面的ApiScope
            AllowedScopes = {  "api" },
        }
    };

注意區分ApiScope和ApiResource, 這里的ApiScope並不會生成Access Token里面的Audience屬性,因此我們設置EmitStaticAudienceClaim=true(見IdentityServer的Starup文件)來指定一個固定的Audience。
如果需要使用Api的名稱作為Audience,則要使用ApiResource
參考:https://identityserver4.readthedocs.io/en/latest/topics/resources.html#authorization-based-on-scopes
When using the scope-only model, no aud (audience) claim will be added to the token, since this concept does not apply. If you need an aud claim, you can enable the EmitStaticAudience setting on the options. This will emit an aud claim in the issuer_name/resources format. If you need more control of the aud claim, use API resources.

2 添加Web Api項目

首先我們要創建一個Web Api項目,使用默認的模板就行了,她包含一個WeatherForecast的Api(即WeatherForecastController文件),為了方便測試,我們給她加上[Authorize]屬性修飾符,表示該Api需要認證之后才能訪問,直接訪問的話你會發現她返回一個401的錯誤碼,表示未經授權。

2.1 添加對應的包

為了能解析Blazor傳遞過來的Token,我們首先需要添加一個包:

Microsoft.AspNetCore.Authentication.JwtBearer

她包含了Json Web Token的相關操作。

2.2 修改Starup

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

    //添加認證服務
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            //指定IdentityServer的地址
            options.Authority = "https://localhost:5001";
            //由於我們在IdentityServer中指定了EmitStaticAudienceClaim=true(見IdentityServer的Starup文件)
            //所以Audience是固定的
            options.Audience = "https://localhost:5001/resources";
        });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    //...
    app.UseRouting();

    //添加認證與授權的中間件
    app.UseAuthentication();
    app.UseAuthorization();

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

3 修改Blazor

3.1 appsettings.json

前面的例子已經添加了appsettings.json, 並且包含了名為api的Scope,對應IdentityServer里面聲明的ApiScope。

{
  "Local": {
    "Authority": "https://localhost:5001",
    "ClientId": "blazorwasm",
    "DefaultScopes": [
      "role",
      "permission",
      "api"
    ],
    "PostLogoutRedirectUri": "/",
    "ResponseType": "code"
  }
}

3.2 添加包

涉及到一些Http擴展,我們需要添加包:

Microsoft.Extensions.Http

3.3 添加MessageHandler

這是一個自定義的消息處理程序,其實就是指定了訪問Api的時候需要附帶哪個scope的認證。

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

namespace BlazorWasm
{
    public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
    {
        public CustomAuthorizationMessageHandler(IAccessTokenProvider provider,
            NavigationManager navigationManager)
            : base(provider, navigationManager)
        {
            ConfigureHandler(
                //這是Web Api的根地址
                authorizedUrls: new[] { "https://localhost:5002" },
                //對應Api Scope, 表示請求上面的Web Api之前需要先獲取該Scope對應的Access Token,並附在Http頭里面
                scopes: new[] { "api"});
        }
    }
}

3.4 修改Program.cs

注冊上面的自定義處理程序

builder.Services.AddScoped<CustomAuthorizationMessageHandler>();
builder.Services.AddHttpClient("ServerAPI",
        //修改HttpClient的根地址
        client => client.BaseAddress = new Uri("https://localhost:5002"))
        //聲明使用以上自定義的處理程序
    .AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
    .CreateClient("ServerAPI"));

3.5 修改默認的FetchData頁面

只要把請求的URL改成Web Api的相對地址就行了:

protected override async Task OnInitializedAsync()
{
    forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
}

到這里已經可以運行了,具體的源代碼在:

https://gitee.com/bigname65/blazor-identity-server4

4 關於Token的過期時間

Access Token都有過期時間,默認通常是一個小時,但是我們不需要為這個擔心,上面的處理程序已經幫我們處理好了一切,當Token過期的時候,她會自動請求一個新的。

Blazor與IdentityServer4的集成(六)-Blazor Server如何使用Identity Server?

前面幾篇都是基於Blazor WebAssembly 的,這次來嘗試一下Blazor的Server模式。根據官方的介紹,Blazor Server的認證方式其實和MVC的認證方式是一樣的,同時Identity Server 也有專門介紹MVC的集成:
https://identityserver4.readthedocs.io/en/latest/quickstarts/2_interactive_aspnetcore.html#creating-an-mvc-client
那么這里就以此為基礎來開始我們的代碼。

1. Blazor Server

首先當然是創建一個Blazor Server項目,使用默認模板就行,然后還要添加包:Microsoft.AspNetCore.Authentication.OpenIdConnect, 她將用來與Identity Server的交互。

1.1 修改Startup.cs

Startup文件,基本上就是根據Identity Server的官方指引來修改:

public void ConfigureServices(IServiceCollection services)
{
    //...

    //添加認證相關的服務
    JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
            
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = "Cookies";
        options.DefaultChallengeScheme = "oidc";
    })
        .AddCookie("Cookies")
        .AddOpenIdConnect("oidc", options =>
        {
            //Identity Server 的地址
            options.Authority = "https://localhost:5001";
            //Identity Server配置的Client 以及 Secret
            options.ClientId = "blazorserver";
            options.ClientSecret = "secret";
            //認證模式
            options.ResponseType = "code";
            //保存token到本地
            options.SaveTokens = true;
            
        });

}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    //...

    app.UseRouting();

    //添加認證與授權中間件
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute().RequireAuthorization();
        endpoints.MapBlazorHub();
        endpoints.MapFallbackToPage("/_Host");
    });
}

1.2 修改App.razor

其實跟WebAssembly是一樣的,都用到了CascadingAuthenticationState和AuthorizeRouteView

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData"
                                DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    @if (!context.User.Identity.IsAuthenticated)
                    {
                        <RedirectToLogin></RedirectToLogin>
                    }
                    else
                    {
                        <p>
                            You are not authorized to access
                            this resource.
                        </p>
                    }
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

1.3 添加RedirectToLogin組件

App.razor 里面用到了該組件,這個系統是沒有的,得我們自己添加,代碼如下:

@inject NavigationManager Navigation
@code {
    protected override void OnAfterRender(bool firstRender)
    {
        Navigation.NavigateTo($"account/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}", true);
    }
}

也很簡單,就是跳轉到account/login頁面,但是需要注意的是這個login並不是一個Blazor組件,而是一個Controller(MVC模式),至於能不能不用Controller,而是像WebAssembly一樣使用純粹的Blazor的模式,這個筆者到目前為止還不清楚。

1.4 添加AccountController

接上文,我們需要添加一個Controller來處理登錄登出操作,其實作用也就是構造鏈接跳轉到Identity Server,這個中間件都已經幫我們做了,所以代碼也很少:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

public class AccountController : Controller
{
    [HttpGet]
    public IActionResult Login(string returnUrl)
    {
        if (string.IsNullOrEmpty(returnUrl)) returnUrl = "/";

        // start challenge and roundtrip the return URL and scheme 
        var props = new AuthenticationProperties
        {
            RedirectUri = returnUrl
        };

        return Challenge(props, "oidc");
    }

    [HttpGet]
    public async Task<IActionResult> Logout()
    {
        if (User?.Identity.IsAuthenticated == true)
        {
            // delete local authentication cookie
            await HttpContext.SignOutAsync();

        }

        return SignOut(new AuthenticationProperties { RedirectUri = "/" }, "oidc");
    }
}

2. Identity Server

Identity Server 我們還是使用之前WebAssembly的項目,添加一個新的Client就行:

public static IEnumerable<Client> Clients =>
    new Client[]
    {
        new Client
        {
            //唯一id,用來區分不同的Client
            ClientId = "blazorserver",
            //使用的授權方式
            AllowedGrantTypes = GrantTypes.Code,
            //這里設置安全碼,當然也可以不指定
            ClientSecrets = { new Secret("secret".Sha256()) },
            //Blazor運行時的URL
            AllowedCorsOrigins =     { "https://localhost:5005" },
            //登錄成功之后將要跳轉的Blazor的URL
            RedirectUris = { "https://localhost:5005/signin-oidc" },
            //登出之后將要跳轉的Blazor的URL
            PostLogoutRedirectUris = { "https://localhost:5005/signout-callback-oidc" },
            //允許的Scope,openid包含用戶id,profile包含用戶基本資料
            AllowedScopes = { "openid", "profile", "role","permission"},
        }
    };

到這里已經可以實現登錄操作了,就這么簡單。

3. 關於授權

前面的代碼只完成了認證操作,你如果想在當前用戶的Claim里面獲取基本資料或者授權信息,不好意思,是沒有的,因此我們還需要做一些額外的工作。

還是修改Blazor項目的Startup文件

services.AddAuthentication(options =>
{
    options.DefaultScheme = "Cookies";
    options.DefaultChallengeScheme = "oidc";
})
    .AddCookie("Cookies")
    .AddOpenIdConnect("oidc", options =>
    {
        //...
        //添加以下代碼
        //指定從Identity Server的UserInfo地址來取Claim
        options.GetClaimsFromUserInfoEndpoint = true;
        //指定要取哪些資料(除Profile之外,Profile是默認包含的)
        options.Scope.Add("role");
        options.Scope.Add("permission");
        //這里是個ClaimType的轉換,Identity Server的ClaimType和Blazor中間件使用的名稱有區別,需要統一。
        options.TokenValidationParameters.RoleClaimType = "role";
        options.TokenValidationParameters.NameClaimType = "name";
        options.Events.OnUserInformationReceived = (context) =>
            {
                //回顧之前關於WebAssembly的例子,涉及到數組的轉換,這里也一樣要處理

                ClaimsIdentity claimsId = context.Principal.Identity as ClaimsIdentity;

                var roleElement = context.User.RootElement.GetProperty("role");
                if (roleElement.ValueKind == System.Text.Json.JsonValueKind.Array)
                {
                    var roles = context.User.RootElement.GetProperty("role").EnumerateArray().Select(e =>
                    {
                        return e.ToString();
                    });
                    claimsId.AddClaims(roles.Select(r => new Claim("role", r)));
                }
                else
                {
                    claimsId.AddClaim(new Claim("role", roleElement.ToString()));
                }

                var permissionElement = context.User.RootElement.GetProperty("permission");
                if (permissionElement.ValueKind == System.Text.Json.JsonValueKind.Array)
                {
                    var permissions = permissionElement.EnumerateArray().Select(e =>
                    {
                        return e.ToString();
                    });
                    claimsId.AddClaims(permissions.Select(p => new Claim("permission", p)));
                }
                else
                {
                    claimsId.AddClaim(new Claim("permission", permissionElement.ToString()));
                }


                return Task.CompletedTask;
            };
    });

// 這里是基於決策的授權操作,WebAssembly的例子中有相關的說明,Blazor Server的使用方式也一樣
services.AddAuthorizationCore(option =>
{
    string[] permissions = new string[]
    {
        "create",
        "retrieve",
        "update",
        "delete"
    };
    foreach (var p in permissions)
    {
        option.AddPolicy(p, policy =>
        {
            policy.RequireClaim("permission", new string[] { p });
        });
    }
});

好了,這樣我們就可以在當前用戶的Claim里面取到基本資料以及角色,權限等信息了,那么應該如何使用?答案是和WebAssembly一樣的,你可以使用@attribute來添加Authorize屬性,也可以使用AuthorizeView組件。

完整的源代碼可以查看以下地址:

https://gitee.com/bigname65/blazor-identity-server4




免責聲明!

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



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