天天看點

【實戰 Ids4】║ 又一個項目遷移完成(MVC)

新年還有兩周時間就要到了,學習可不能停,這幾天一直在加班調休,周末也如此,不過也是趁着半夜淩晨的時間,繼續遷移我的項目到IdentityServer4統一認證授權中心Blog.IdentityServer上,也是基本統一了,目前進度如下:

01、前後端分離全家桶已經完成更新:Blog.Core為api,Blog.Admin為背景管理,Blog.Vue為前台資訊展示已經全部搞定,具體的代碼檢視指定Github的分支即可,分支名基本都是Is4,Ids4等字樣;

02、Nuxt.tBug項目目前正在更新中,其實和Vue的前後端分離是一樣的,都是使用的同一個元件架構oidc-client,這裡就不多說了,如果真的差别大,我就單寫一篇文章,否則直接看我的代碼就行;

03、ChristDDD MVC項目已經完成遷移,就是今天本文講解的。

04、WPF項目在進度種,到時候簡單寫個小Demo就行,我會在我的視訊中,給大家講解,預計春節後出來。

上邊共涉及到了我開源的六個項目,三個後端,三個前端,想想這一年也是夠可以了,但是在遷移的IdentityServer4中,隻用到了常用的兩種模式,Implicit和Code模式,其實一般我們web開發,掌握四種就行,除了這兩個,還有Hybrid和Client,其他的如果沒有精力,可以放一放,那下邊我們就快速的說一下如何将MVC項目遷移到Ids4上。這裡就簡單的說一下操作過程,不會講解原理,原理我會在視訊教程中,詳細說到。

Idp項目如何配置

具體的原型圖,運作原理,等我視訊吧,直接看代碼,這裡要說一下,如果你是第一次開發學習,我建議盡量使用記憶體模式,這樣會很好的調試,如果直接生成到資料庫的話,可能有時候修改了一個配置,還需要重新生成資料庫,這個有些浪費時間。

在我們的Config.cs中,建立一個Client,用來應對我們的MVC用戶端:

// interactive ASP.NET Core MVC client
new Client
{
    ClientId = "chrisdddmvc",
    ClientName="Chris DDD MVC項目",
    ClientSecrets = { new Secret("secret".Sha256()) },

    AllowedGrantTypes = GrantTypes.Code,
    RequireConsent = false,
    RequirePkce = true,
    AlwaysIncludeUserClaimsInIdToken=true,//将使用者所有的claims包含在IdToken内

    // 登入回調
    RedirectUris = { "http://ddd.neters.club/signin-oidc" },

    // 登出回調位址
    PostLogoutRedirectUris = { "http://ddd.neters.club/signout-callback-oidc" },

    // 注意這些scope,一定是上邊已經定義好的資源
    AllowedScopes = new List<string>
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
        IdentityServerConstants.StandardScopes.Email,
        "roles",
        "rolename",
    }
}           

複制

這裡就強調兩點,就是配置一下回調位址,然後就是AlwaysIncludeUserClaimsInIdToken要設定為true,以友善我們後邊要從claims聲明中擷取傳回的值。

當然,最後還有一個知識點,就是scope中,如果想要自定義的話,需要先在claims中注冊添加,然後在GetIdentityResources中配置:

// scopes define the resources in your system
 public static IEnumerable<IdentityResource> GetIdentityResources()
 {
     return new List<IdentityResource>
     {
         new IdentityResources.OpenId(),
         new IdentityResources.Profile(),
         new IdentityResources.Email(),
         new IdentityResource("roles", "角色", new List<string> { JwtClaimTypes.Role }),
         new IdentityResource("rolename", "角色名", new List<string> { "rolename" }),
     };
 }           

複制

這裡配置就是很簡單的,咱們繼續看看如何在MVC中配置。

ChristDDD如何配置

如果你之前看過或者用到了我的DDD項目,會發現其實本來是用Identity寫的,這次我們遷移到Ids4後,需要做一些變化,具體的直接下載下傳我的Ids4分支就行了,修改的内容比較多。

首先我們把響應的認證服務給抽出來,單獨封裝,上邊的是Ids4的,下邊的是普通的Identity的:

【實戰 Ids4】║ 又一個項目遷移完成(MVC)

然後注入服務:

// IdentityServer4 注入
 services.AddId4OidcSetup();           

複制

那我們直接看看服務是如何設定的:

private static readonly string config= "https://ids.neters.club";
  
  public static void AddId4OidcSetup(this IServiceCollection services)
  {
      if (services == null) throw new ArgumentNullException(nameof(services));
      //關閉預設映射,否則它可能修改從授權服務傳回的各種claim屬性
      JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
      //添加認證服務,并設定其有關選項
      services.AddAuthentication(options =>
      {
          // 用戶端應用設定使用"Cookies"進行認證
          options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
          // identityserver4設定使用"oidc"進行認證
          options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;

      }).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
           // 對使用的OpenIdConnect進行設定,此設定與Identityserver的config.cs中相應client配置一緻才可能登入授權成功
           .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
           {
               options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
               options.Authority = config;
               options.RequireHttpsMetadata = false;//必須https協定
               options.ClientId = "chrisdddmvc";//idp項目中配置的client
               options.ClientSecret = "secret";
               options.SaveTokens = true;
               options.ResponseType = "code";//響應類型

               // 下邊是所有的scope,必須要和idp項目中一緻,至少是一部分
               options.Scope.Clear();
               options.Scope.Add("roles");//"roles"
               options.Scope.Add("rolename");//"roles"
               options.Scope.Add(OidcConstants.StandardScopes.OpenId);//"openid"
               options.Scope.Add(OidcConstants.StandardScopes.Profile);//"profile"
               options.Scope.Add(OidcConstants.StandardScopes.Email);//"email"

           });

  }           

複制

這裡有幾個注意事項:ClientId一定要填對,Scope必須是Idp項目中配置的子集,Scope一定要寫對,不然的話,會報錯,比如我們随便把roles改成roles3:

【實戰 Ids4】║ 又一個項目遷移完成(MVC)

當然全部粘貼過去就行,其他的都有注釋,看看即可。

這裡配置也是很簡單的,運作到了這裡,我們就可以簡單的調試了,所有的位址,都可以換成localhost來調試。

沒有錯誤的話,我們就可以正式的跳轉登入,登入成功後,跳轉回來MVC項目,下面我們就說說如何在MVC用戶端項目中,進行政策授權。

MVC用戶端做政策授權

上邊我們已經登入成功,并也跳回了,那現在就要根據情況,設計授權了,畢竟有些頁面是test使用者不能通路的,隻有超級管理者才能通路的:

首先,在聲明政策,然後在控制器配置政策

services.AddAuthorization(options =>
   {
       options.AddPolicy("CanWriteStudentData", policy => policy.Requirements.Add(new ClaimRequirement("Students", "Write")));
       options.AddPolicy("CanRemoveStudentData", policy => policy.Requirements.Add(new ClaimRequirement("Students", "Remove")));
       options.AddPolicy("CanWriteOrRemoveStudentData", policy => policy.Requirements.Add(new ClaimRequirement("Students", "WriteOrRemove")));
   });

 // 這裡的政策内容可以任意擴充
 [HttpGet]
 [Authorize(Policy = "CanWriteStudentData")]
 public IActionResult Edit(Guid? id)
 {
 }           

複制

接着,我們就來定義授權政策處理器

public class ClaimsRequirementHandler : AuthorizationHandler<ClaimRequirement>
  {
      protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ClaimRequirement requirement)
      {

          var roleId = context.User.Claims.FirstOrDefault(c => c.Type == "role");
          var rolename = context.User.Claims.FirstOrDefault(c => c.Type == "rolename");
          var loginUserName = context.User.Claims.FirstOrDefault(c => c.Type == "preferred_username");
          if (roleId != null && roleId.Value == "4" && rolename != null && rolename.Value == "SuperAdmin")
          {
              context.Succeed(requirement);
          }

          return Task.CompletedTask;
      }
  }           

複制

複雜政策授權如何寫,邏輯如何調,上下文中的claims聲明如何擷取,這裡就不多說了,預設已經會了我的第一個項目的Blog.Core的相關内容,這裡我們隻是來看看是不是能擷取到相應的Claims就行:

【實戰 Ids4】║ 又一個項目遷移完成(MVC)

可以看到我們已經擷取到了這個scope,這樣我們就可以任意的擴充了。

登入與登出設計

這個其實就很簡單了,我們在用戶端裡,直接登出就行,我寫的比較low,當然你可以自己找找例子,我就簡單的寫了寫:

[Authorize]
 public IActionResult Login()
 {
     return Redirect("index");

 }
 public async Task<IActionResult> Logout()
 {
     await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
     await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);

     return View("Index");
 }           

複制

登入取了個巧,不寫内容,直接加了個Authorize,這樣肯定就跳轉到登入頁了。

然後設計下UI展示 _LoginPartial.cshtml ,注入服務就行:

@inject Christ3D.Domain.Interfaces.IUser SignInManager

@if (SignInManager.IsAuthenticated())
{
    <form asp-area="" asp-controller="Home" asp-action="Logout" method="post" id="logoutForm" class="navbar-right">
        <ul class="nav navbar-nav navbar-right">
            <li>
                <a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage">Hello @SignInManager.Name!</a>
            </li>
            <li>
                <button type="submit" class="btn btn-link navbar-btn navbar-link">Log out</button>
            </li>
        </ul>
    </form>
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li><a asp-area="" asp-controller="Home" asp-action="Login">Log in</a></li>
    </ul>
}           

複制

最終的展示效果是醬紫的,登出:

【實戰 Ids4】║ 又一個項目遷移完成(MVC)

登入:

【實戰 Ids4】║ 又一個項目遷移完成(MVC)

到了這裡,我們就已經完成了整體流程了!下邊就是部署了。

生産環境部署聯調

現在還是兩個後端項目,一個是IdentityServer4的部署,很簡單的,我目前用的是Nginx部署的,Https安全協定。

用戶端是MVC項目,但是用的IIS部署的,因為如何也用Nginx部署的話,用戶端向授權中心認證的時候,一直報錯,錯誤是回調位址不比對,因為nginx部署,顯示的位址還是本地的:

【實戰 Ids4】║ 又一個項目遷移完成(MVC)

但是我在idp項目裡,明明配置的是ddd域名:

【實戰 Ids4】║ 又一個項目遷移完成(MVC)

錯誤資訊是這樣的:

【實戰 Ids4】║ 又一個項目遷移完成(MVC)
【實戰 Ids4】║ 又一個項目遷移完成(MVC)

但是在IIS中配置,是一切正常的,真的是我學術不精啊,有小夥伴知道的,歡迎給我留言私信拍磚,這裡我來個賞金(20大洋),給開源事業做貢獻了。

這個時候,PC端已經一切正常了,正當高興的時候,手機通路,又不行了,這次我很機智,有了上次的JS用戶端經驗,我直接加了一個Cookie

手機移動端适配

在DDD項目中,建立一個擴充:

public static class SameSiteHandlingExtensions
 {
     public static IServiceCollection AddSameSiteCookiePolicy(this IServiceCollection services)
     {
         services.Configure<CookiePolicyOptions>(options =>
         {
             options.MinimumSameSitePolicy = (SameSiteMode)(-1);
             options.OnAppendCookie = cookieContext => 
                 CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
             options.OnDeleteCookie = cookieContext => 
                 CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
         });

         return services;
     }
     
     private static void CheckSameSite(HttpContext httpContext, CookieOptions options)
     {
         if (options.SameSite == SameSiteMode.None)
         {
             var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
             if (DisallowsSameSiteNone(userAgent))
             {
                 // For .NET Core < 3.1 set SameSite = (SameSiteMode)(-1)
                 options.SameSite = (SameSiteMode)(-1);
             }
         }
     }

     private static bool DisallowsSameSiteNone(string userAgent)
     {
         // Cover all iOS based browsers here. This includes:
         // - Safari on iOS 12 for iPhone, iPod Touch, iPad
         // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad
         // - Chrome on iOS 12 for iPhone, iPod Touch, iPad
         // All of which are broken by SameSite=None, because they use the iOS networking stack
         if (userAgent.Contains("CPU iPhone OS 12") || userAgent.Contains("iPad; CPU OS 12"))
         {
             return true;
         }

         // Cover Mac OS X based browsers that use the Mac OS networking stack. This includes:
         // - Safari on Mac OS X.
         // This does not include:
         // - Chrome on Mac OS X
         // Because they do not use the Mac OS networking stack.
         if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") && 
             userAgent.Contains("Version/") && userAgent.Contains("Safari"))
         {
             return true;
         }

         // Cover Chrome 50-69, because some versions are broken by SameSite=None, 
         // and none in this range require it.
         // Note: this covers some pre-Chromium Edge versions, 
         // but pre-Chromium Edge does not require SameSite=None.
         if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6"))
         {
             return true;
         }

         return false;
     }
 }           

複制

然後服務配置:

【實戰 Ids4】║ 又一個項目遷移完成(MVC)

常見的錯誤

剛剛上邊我們已經遇到了兩個錯誤,其實總的來說,都是配置的問題,我會在部落格園單寫一篇文章,來總結IdentityServer4的所有錯誤,目前還沒有,過一段時間檢視就行,現在開發的還比較少。注意這兩個錯誤,然後會調試就行,調試主要在F12,去檢視network,看看請求的資料是否異常即可。

【實戰 Ids4】║ 又一個項目遷移完成(MVC)
【實戰 Ids4】║ 又一個項目遷移完成(MVC)

到了這裡,基本就結束了,還是建議大家多看看官網和官方Demo,真的很有用。