將驗證和 Identity 移轉至 ASP.NET Core 2.0

作者:Scott AddieHao Kung

ASP.NET Core 2.0 有新的驗證和 Identity 模型,可透過使用服務簡化設定。 使用驗證或 Identity 的 ASP.NET Core 1.x 應用程式可以更新為使用新的模型,如下所述。

更新命名空間

在 1.x 中,可以在 Microsoft.AspNetCore.Identity.EntityFrameworkCore 命名空間中找到 IdentityRoleIdentityUser 這類的類別。

在 2.0 中,Microsoft.AspNetCore.Identity 命名空間成為數個這類類別的新位置。 對於預設的 Identity 程式碼,受影響的類別包括 ApplicationUserStartup。 調整您的 using 陳述式以解決受影響的參考問題。

驗證中介軟體和服務

在 1.x 專案中,會透過中介軟體設定驗證。 會針對您想要支援的每個驗證配置叫用一個中介軟體方法。

下列 1.x 範例會在 Startup.cs 中使用 Identity 設定 Facebook 驗證:

public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>();
}

public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory)
{
    app.UseIdentity();
    app.UseFacebookAuthentication(new FacebookOptions {
        AppId = Configuration["auth:facebook:appid"],
        AppSecret = Configuration["auth:facebook:appsecret"]
    });
}

在 2.0 專案中,會透過服務設定驗證。 每個驗證配置都會在 Startup.csConfigureServices 方法中註冊。 UseIdentity 方法會以 UseAuthentication 來取代。

下列 2.0 範例會在 Startup.cs 中使用 Identity 設定 Facebook 驗證:

public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>();

    // If you want to tweak Identity cookies, they're no longer part of IdentityOptions.
    services.ConfigureApplicationCookie(options => options.LoginPath = "/Account/LogIn");
    services.AddAuthentication()
            .AddFacebook(options =>
            {
                options.AppId = Configuration["auth:facebook:appid"];
                options.AppSecret = Configuration["auth:facebook:appsecret"];
            });
}

public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) {
    app.UseAuthentication();
}

UseAuthentication 方法會新增一個單一的驗證中介軟體元件 (該元件負責自動驗證和遠端驗證要求的處理)。 它會將所有個別的中介軟體元件取代為一個單一的通用中介軟體元件。

以下是每個主要驗證配置的 2.0 移轉指示。

選取下列兩個選項中的其中一項,並在 Startup.cs 中進行必要的變更:

  1. 使用含 Identity 的 cookie

    • 使用 Configure 方法中的 UseAuthentication 取代 UseIdentity

      app.UseAuthentication();
      
    • ConfigureServices 方法中叫用 AddIdentity 方法以新增 cookie 驗證服務。

    • 或者,在 ConfigureServices 方法中叫用 ConfigureApplicationCookieConfigureExternalCookie 方法,以調整 Identitycookie 設定。

      services.AddIdentity<ApplicationUser, IdentityRole>()
              .AddEntityFrameworkStores<ApplicationDbContext>()
              .AddDefaultTokenProviders();
      
      services.ConfigureApplicationCookie(options => options.LoginPath = "/Account/LogIn");
      
  2. 使用不含 Identity 的 cookie

    • Configure 方法中的 UseCookieAuthentication 方法呼叫取代為 UseAuthentication

      app.UseAuthentication();
      
    • ConfigureServices 方法中叫用 AddAuthenticationAddCookie 方法:

      // If you don't want the cookie to be automatically authenticated and assigned to HttpContext.User,
      // remove the CookieAuthenticationDefaults.AuthenticationScheme parameter passed to AddAuthentication.
      services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
              .AddCookie(options =>
              {
                  options.LoginPath = "/Account/LogIn";
                  options.LogoutPath = "/Account/LogOff";
              });
      

JWT 持有人驗證

Startup.cs 中進行下列變更:

  • Configure 方法中的 UseJwtBearerAuthentication 方法呼叫取代為 UseAuthentication

    app.UseAuthentication();
    
  • ConfigureServices 方法中叫用 AddJwtBearer 方法:

    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.Audience = "http://localhost:5001/";
                options.Authority = "http://localhost:5000/";
            });
    

    此程式碼片段不會使用 Identity,因此應該藉由將 JwtBearerDefaults.AuthenticationScheme 傳遞至 AddAuthentication 方法來設定預設的配置。

OpenID Connect (OIDC) 驗證

Startup.cs 中進行下列變更:

  • Configure 方法中的 UseOpenIdConnectAuthentication 方法呼叫取代為 UseAuthentication

    app.UseAuthentication();
    
  • ConfigureServices 方法中叫用 AddOpenIdConnect 方法:

    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options =>
    {
        options.Authority = Configuration["auth:oidc:authority"];
        options.ClientId = Configuration["auth:oidc:clientid"];
    });
    
  • OpenIdConnectOptions 動作中的 PostLogoutRedirectUri 屬性取代為 SignedOutRedirectUri

    .AddOpenIdConnect(options =>
    {
        options.SignedOutRedirectUri = "https://contoso.com";
    });
    

Facebook 驗證

Startup.cs 中進行下列變更:

  • Configure 方法中的 UseFacebookAuthentication 方法呼叫取代為 UseAuthentication

    app.UseAuthentication();
    
  • ConfigureServices 方法中叫用 AddFacebook 方法:

    services.AddAuthentication()
            .AddFacebook(options =>
            {
                options.AppId = Configuration["auth:facebook:appid"];
                options.AppSecret = Configuration["auth:facebook:appsecret"];
            });
    

Google 驗證

Startup.cs 中進行下列變更:

  • Configure 方法中的 UseGoogleAuthentication 方法呼叫取代為 UseAuthentication

    app.UseAuthentication();
    
  • ConfigureServices 方法中叫用 AddGoogle 方法:

    services.AddAuthentication()
            .AddGoogle(options =>
            {
                options.ClientId = Configuration["auth:google:clientid"];
                options.ClientSecret = Configuration["auth:google:clientsecret"];
            });
    

Microsoft 帳戶驗證

如需 Microsoft 帳戶驗證的詳細資訊,請參閱此 GitHub 問題

Startup.cs 中進行下列變更:

  • Configure 方法中的 UseMicrosoftAccountAuthentication 方法呼叫取代為 UseAuthentication

    app.UseAuthentication();
    
  • ConfigureServices 方法中叫用 AddMicrosoftAccount 方法:

    services.AddAuthentication()
            .AddMicrosoftAccount(options =>
            {
                options.ClientId = Configuration["auth:microsoft:clientid"];
                options.ClientSecret = Configuration["auth:microsoft:clientsecret"];
            });
    

Twitter 驗證

Startup.cs 中進行下列變更:

  • Configure 方法中的 UseTwitterAuthentication 方法呼叫取代為 UseAuthentication

    app.UseAuthentication();
    
  • ConfigureServices 方法中叫用 AddTwitter 方法:

    services.AddAuthentication()
            .AddTwitter(options =>
            {
                options.ConsumerKey = Configuration["auth:twitter:consumerkey"];
                options.ConsumerSecret = Configuration["auth:twitter:consumersecret"];
            });
    

設定預設驗證配置

在 1.x 中,AuthenticationOptions 基底類別的 AutomaticAuthenticateAutomaticChallenge 屬性原預期要在單一驗證配置上設定。 但沒有好的方法來強制執行這一點。

在 2.0 中,這兩個屬性已作為個別 AuthenticationOptions 實例上的屬性被移除。 它們可以在 Startup.csConfigureServices 方法內的 AddAuthentication 方法呼叫中進行設定:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);

在上述程式碼片段中,預設配置會設定為 CookieAuthenticationDefaults.AuthenticationScheme ("Cookie")。

或者,使用多載版本的 AddAuthentication 方法來設定多個屬性。 在下列多載方法範例中,預設配置會設定為 CookieAuthenticationDefaults.AuthenticationScheme。 或者,也可以在您的個別 [Authorize] 屬性或授權原則內指定驗證配置。

services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
});

如果下列其中一個條件成立,請在 2.0 中定義預設配置:

  • 您希望使用者自動被登入
  • 您要在不指定配置的情況下使用 [Authorize] 屬性或授權原則

此規則的例外狀況是 AddIdentity 方法。 此方法會為您新增 cookie,並將預設的驗證和查問配置設定到應用程式 cookieIdentityConstants.ApplicationScheme。 此外,它也會將預設登入配置設定為外部 cookieIdentityConstants.ExternalScheme

使用 HttpCoNtext 驗證擴充功能

IAuthenticationManager 介面是 1.x 驗證系統中的主要進入點。 它已被取代為 Microsoft.AspNetCore.Authentication 命名空間中的一組新 HttpContext 擴充方法。

例如,1.x 專案會參考 Authentication 屬性:

// Clear the existing external cookie to ensure a clean login process
await HttpContext.Authentication.SignOutAsync(_externalCookieScheme);

在 2.0 專案中,匯入 Microsoft.AspNetCore.Authentication 命名空間,並刪除 Authentication 屬性參考:

// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);

Windows 驗證 (HTTP.sys / IISIntegration)

Windows 驗證有兩種變體:

  • 主機只允許已驗證的使用者。 此變體不會受到 2.0 變更的影響。

  • 主機同時允許匿名和已驗證的使用者。 此變體會受到 2.0 變更的影響。 例如,應用程式應允許匿名使用者在 IISHTTP.sys 層級上進行存取,但是在控制器層級上對使用者進行授權。 在此案例中,在 Startup.ConfigureServices 方法中設定預設配置。

    若為 Microsoft.AspNetCore.Server.IISIntegration,請將預設配置設定為 IISDefaults.AuthenticationScheme

    using Microsoft.AspNetCore.Server.IISIntegration;
    
    services.AddAuthentication(IISDefaults.AuthenticationScheme);
    

    若為 Microsoft.AspNetCore.Server.HttpSys,請將預設配置設定為 HttpSysDefaults.AuthenticationScheme

    using Microsoft.AspNetCore.Server.HttpSys;
    
    services.AddAuthentication(HttpSysDefaults.AuthenticationScheme);
    

    無法設定預設配置會阻止授權 (查問) 要求正常運作,並出現下列例外狀況:

    System.InvalidOperationException:未指定 authenticationScheme,且找不到 DefaultChallengeScheme。

如需詳細資訊,請參閱在 ASP.NET Core 中設定 Windows 驗證

IdentityCookie選項實例

2.0 變更的一個副作用是改用具名選項,而不是 cookie 選項實例。 已移除自訂 Identitycookie 配置名稱的功能。

例如,1.x 專案會使用建構函式插入,將 IdentityCookieOptions 參數傳遞至 AccountController.csManageController.cs。 從提供的實例中存取外部 cookie 驗證配置:

public AccountController(
    UserManager<ApplicationUser> userManager,
    SignInManager<ApplicationUser> signInManager,
    IOptions<IdentityCookieOptions> identityCookieOptions,
    IEmailSender emailSender,
    ISmsSender smsSender,
    ILoggerFactory loggerFactory)
{
    _userManager = userManager;
    _signInManager = signInManager;
    _externalCookieScheme = identityCookieOptions.Value.ExternalCookieAuthenticationScheme;
    _emailSender = emailSender;
    _smsSender = smsSender;
    _logger = loggerFactory.CreateLogger<AccountController>();
}

前面提到的建構函式插入在 2.0 專案中就不再需要了,可以刪除 _externalCookieScheme 欄位:

public AccountController(
    UserManager<ApplicationUser> userManager,
    SignInManager<ApplicationUser> signInManager,
    IEmailSender emailSender,
    ISmsSender smsSender,
    ILoggerFactory loggerFactory)
{
    _userManager = userManager;
    _signInManager = signInManager;
    _emailSender = emailSender;
    _smsSender = smsSender;
    _logger = loggerFactory.CreateLogger<AccountController>();
}

1.x 專案使用 _externalCookieScheme 欄位,如下所示:

// Clear the existing external cookie to ensure a clean login process
await HttpContext.Authentication.SignOutAsync(_externalCookieScheme);

在 2.0 專案中,將上述程式碼取代為下列程式碼。 IdentityConstants.ExternalScheme 常數可以直接使用。

// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);

藉由匯入下列命名空間來解決新增 SignOutAsync 呼叫的問題:

using Microsoft.AspNetCore.Authentication;

新增 Identity使用者 POCO 導覽屬性

已移除基本 IdentityUser POCO (Plain Old CLR Object) 的 Entity Framework (EF) Core 導覽屬性。 如果您的 1.x 專案使用這些屬性,請手動將它們新增回 2.0 專案:

/// <summary>
/// Navigation property for the roles this user belongs to.
/// </summary>
public virtual ICollection<IdentityUserRole<int>> Roles { get; } = new List<IdentityUserRole<int>>();

/// <summary>
/// Navigation property for the claims this user possesses.
/// </summary>
public virtual ICollection<IdentityUserClaim<int>> Claims { get; } = new List<IdentityUserClaim<int>>();

/// <summary>
/// Navigation property for this users login accounts.
/// </summary>
public virtual ICollection<IdentityUserLogin<int>> Logins { get; } = new List<IdentityUserLogin<int>>();

為了防止在執行 EF Core 移轉時出現重複的外部索引鍵,請將下列內容新增至 IdentityDbContext 類別的 OnModelCreating 方法中 (在 base.OnModelCreating(); 呼叫之後):

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);
    // Customize the ASP.NET Core Identity model and override the defaults if needed.
    // For example, you can rename the ASP.NET Core Identity table names and more.
    // Add your customizations after calling base.OnModelCreating(builder);

    builder.Entity<ApplicationUser>()
        .HasMany(e => e.Claims)
        .WithOne()
        .HasForeignKey(e => e.UserId)
        .IsRequired()
        .OnDelete(DeleteBehavior.Cascade);

    builder.Entity<ApplicationUser>()
        .HasMany(e => e.Logins)
        .WithOne()
        .HasForeignKey(e => e.UserId)
        .IsRequired()
        .OnDelete(DeleteBehavior.Cascade);

    builder.Entity<ApplicationUser>()
        .HasMany(e => e.Roles)
        .WithOne()
        .HasForeignKey(e => e.UserId)
        .IsRequired()
        .OnDelete(DeleteBehavior.Cascade);
}

取代 GetExternalAuthenticationSchemes

已移除同步方法 GetExternalAuthenticationSchemes,取而代之的是非同步版本。 1.x 專案在 Controllers/ManageController.cs 中具有下列程式碼:

var otherLogins = _signInManager.GetExternalAuthenticationSchemes().Where(auth => userLogins.All(ul => auth.AuthenticationScheme != ul.LoginProvider)).ToList();

此方法也會出現在 Views/Account/Login.cshtml 中:

@{
    var loginProviders = SignInManager.GetExternalAuthenticationSchemes().ToList();
    if (loginProviders.Count == 0)
    {
        <div>
            <p>
                There are no external authentication services configured. See <a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>
                for details on setting up this ASP.NET application to support logging in via external services.
            </p>
        </div>
    }
    else
    {
        <form asp-controller="Account" asp-action="ExternalLogin" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal">
            <div>
                <p>
                    @foreach (var provider in loginProviders)
                    {
                        <button type="submit" class="btn btn-default" name="provider" value="@provider.AuthenticationScheme" title="Log in using your @provider.DisplayName account">@provider.AuthenticationScheme</button>
                    }
                </p>
            </div>
        </form>
    }
}

在 2.0 專案中,使用 GetExternalAuthenticationSchemesAsync 方法。 ManageController.cs 中的變更類似下列程式碼:

var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync();
var otherLogins = schemes.Where(auth => userLogins.All(ul => auth.Name != ul.LoginProvider)).ToList();

Login.cshtml 中,foreach 迴圈中存取的 AuthenticationScheme 屬性會變更為 Name

@{
    var loginProviders = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList();
    if (loginProviders.Count == 0)
    {
        <div>
            <p>
                There are no external authentication services configured. See <a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>
                for details on setting up this ASP.NET application to support logging in via external services.
            </p>
        </div>
    }
    else
    {
        <form asp-controller="Account" asp-action="ExternalLogin" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal">
            <div>
                <p>
                    @foreach (var provider in loginProviders)
                    {
                        <button type="submit" class="btn btn-default" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
                    }
                </p>
            </div>
        </form>
    }
}

ManageLoginsViewModel 屬性變更

ManageLoginsViewModel 物件會用於 ManageController.csManageLogins 動作。 在 1.x 專案中,物件的 OtherLogins 屬性傳回類型是 IList<AuthenticationDescription>。 此傳回類型需要匯入 Microsoft.AspNetCore.Http.Authentication

using System.Collections.Generic;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Identity;

namespace AspNetCoreDotNetCore1App.Models.ManageViewModels
{
    public class ManageLoginsViewModel
    {
        public IList<UserLoginInfo> CurrentLogins { get; set; }

        public IList<AuthenticationDescription> OtherLogins { get; set; }
    }
}

在 2.0 專案中,傳回類型會變更為 IList<AuthenticationScheme>。 這個新的傳回類型需要以 Microsoft.AspNetCore.Authentication 匯入取代 Microsoft.AspNetCore.Http.Authentication 匯入。

using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;

namespace AspNetCoreDotNetCore2App.Models.ManageViewModels
{
    public class ManageLoginsViewModel
    {
        public IList<UserLoginInfo> CurrentLogins { get; set; }

        public IList<AuthenticationScheme> OtherLogins { get; set; }
    }
}

其他資源

如需詳細資訊,請參閱 GitHub 上的 Auth 2.0 問題討論