IdentityServer4 和 ASP.NET Core5.0 Identity - 基于角色的授权

IdentityServer4 and ASP.NET Core5.0 Identity - Role based Authorization

我想 ASP.NET Core Identity 和 IdentityServer 一起使用,并提供基于角色的授权。

解决方案中的 3 个项目:

Statup.cs 在 API 客户端

public class Startup
{
    public IConfiguration Configuration { get; }
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
        services.AddControllers();
        services.AddAuthentication("Bearer")
                .AddJwtBearer("Bearer", options =>
                {
                    options.Authority = "https://localhost:5001";
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateAudience = false
                    };
                });
        services.AddAuthorization(options =>
        {
            options.AddPolicy("ApiScope", policy =>
            {
                policy.RequireAuthenticatedUser();
                policy.RequireClaim("scope", "api1");
            });
        });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers()
                      .RequireAuthorization("ApiScope");
        });
    }
}

Startup.cs 在 MVC 客户端

public class Startup
{
  public IConfiguration Configuration { get; }
  public Startup(IConfiguration configuration)
  {
    Configuration = configuration;
  }

  public void ConfigureServices(IServiceCollection services)
  {
    services.AddControllersWithViews();
    JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
    services.AddAuthentication(options =>
    {
      options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
      options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
    {
      options.Authority = "https://localhost:5001";
      options.ClientId = "mvc";
      options.ClientSecret = "secret";
      options.ResponseType = "code id_token";
      options.Scope.Add("email");
      options.Scope.Add("roles");
      options.ClaimActions.DeleteClaim("sid");
      options.ClaimActions.DeleteClaim("idp");
      options.ClaimActions.DeleteClaim("s_hash");
      options.ClaimActions.DeleteClaim("auth_time");
      options.ClaimActions.MapJsonKey("role", "role");
      options.Scope.Add("api1");
      options.SaveTokens = true;
      options.GetClaimsFromUserInfoEndpoint = true;
      options.TokenValidationParameters = new TokenValidationParameters
      {
        NameClaimType = "name",
        RoleClaimType = "role"
      };
    });
    services.AddTransient<AuthenticationDelegatingHandler>();
    services.AddHttpClient("ApplicationAPI", client =>
    {
      client.BaseAddress = new Uri("https://localhost:5002/");
      client.DefaultRequestHeaders.Clear();
      client.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json");
    }).AddHttpMessageHandler<AuthenticationDelegatingHandler>();

    services.AddHttpClient("ApplicationIdentityServer", client =>
    {
      client.BaseAddress = new Uri("https://localhost:5001/");
      client.DefaultRequestHeaders.Clear();
      client.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json");
    });
    services.AddHttpContextAccessor();
  }
  public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  {
    if (env.IsDevelopment())
    {
      app.UseDeveloperExceptionPage();
    }
    else
    {
      app.UseExceptionHandler("/Home/Error");
      app.UseHsts();
    }
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
      endpoints.MapControllerRoute(
                  name: "default",
                  pattern: "{area=Admin}/{controller=Home}/{action=Index}/{id?}");
    });
  }
}

MVC 应用程序中的 AuthenticationDelegatingHandler

防止再次token

public class AuthenticationDelegatingHandler : DelegatingHandler
{
  private readonly IHttpContextAccessor _httpContextAccessor;

  public AuthenticationDelegatingHandler(IHttpContextAccessor httpContextAccessor)
  {
    _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
  }

  protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  {
    var accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);

    if (!string.IsNullOrWhiteSpace(accessToken))
    {
      request.SetBearerToken(accessToken);
    }

    return await base.SendAsync(request, cancellationToken);
  }
}

Config.cs 在 IdentityServer

public static class Config
{
  public static IEnumerable<IdentityResource> IdentityResources =>
      new List<IdentityResource>
      {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile(),
        new IdentityResources.Email(),
        new IdentityResource("roles", "Your role(s)", new List<string>() { "role" })
      };
  public static IEnumerable<ApiScope> ApiScopes =>
      new List<ApiScope>
      {
        new ApiScope("api1", "My API")
      };

  public static IEnumerable<Client> Clients =>
    // we can remove API client here because we call API from MVC
    // and pass the token to API Application.
    // Only we get token from IdentityServer with MVC Application 
    new List<Client>
    {
      new Client
      {
          ClientId = "client",
          ClientSecrets = { new Secret("secret".Sha256()) },
          AllowedGrantTypes = GrantTypes.ClientCredentials,
          AllowedScopes = { "api1" }
      },
      new Client
      {
          ClientId = "mvc",
          ClientName = "Application Web",
          AllowedGrantTypes = GrantTypes.Hybrid,
          ClientSecrets = { new Secret("secret".Sha256()) },
          RequirePkce = false,
          AllowRememberConsent = false,
          RedirectUris = { "https://localhost:5003/signin-oidc" },
          PostLogoutRedirectUris = { "https://localhost:5003/signout-callback-oidc" },

          AllowedScopes = new List<string>
          {
              IdentityServerConstants.StandardScopes.OpenId,
              IdentityServerConstants.StandardScopes.Profile,
              IdentityServerConstants.StandardScopes.Email,
              "api1",
              "roles"
          }
      }
    };
}

我在IdentityServer项目中添加了ASP.NET Core Identity

Startup.cs 在 IdentityServer

public class Startup
{
  public IWebHostEnvironment Environment { get; }
  public IConfiguration Configuration { get; }
  public Startup(IWebHostEnvironment environment, IConfiguration configuration)
  {
    Environment = environment;
    Configuration = configuration;
  }
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddControllersWithViews();
    services.AddRazorPages()
        .AddRazorPagesOptions(options =>
        {
          options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage");
        });
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
    {
      options.SignIn.RequireConfirmedEmail = true;
    })
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    var builder = services.AddIdentityServer(options =>
    {
      options.Events.RaiseErrorEvents = true;
      options.Events.RaiseInformationEvents = true;
      options.Events.RaiseFailureEvents = true;
      options.Events.RaiseSuccessEvents = true;
      options.EmitStaticAudienceClaim = true;
      options.UserInteraction.LoginUrl = "/Account/Login";
      options.UserInteraction.LogoutUrl = "/Account/Logout";
      options.Authentication = new AuthenticationOptions()
      {
        CookieLifetime = TimeSpan.FromHours(10),
                CookieSlidingExpiration = true
      };
    })
        .AddInMemoryIdentityResources(Config.IdentityResources)
        .AddInMemoryApiScopes(Config.ApiScopes)
        .AddInMemoryClients(Config.Clients)
        .AddAspNetIdentity<ApplicationUser>();

    if (Environment.IsDevelopment())
    {
      builder.AddDeveloperSigningCredential();
    }
    services.AddAuthentication()
        .AddGoogle(options =>
        {
          options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
          options.ClientId = "copy client ID from Google here";
          options.ClientSecret = "copy client secret from Google here";
        });

    services.AddTransient<IEmailSender, EmailSender>();
  }
  public void Configure(IApplicationBuilder app)
  {
    if (Environment.IsDevelopment())
    {
      app.UseDeveloperExceptionPage();
    }
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseRouting();
    app.UseIdentityServer();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
      endpoints.MapControllerRoute(
                  name: "default",
                  pattern: "{controller=Home}/{action=Index}/{id?}");
      endpoints.MapRazorPages();
    });
  }
}

我在 IdentityServer 项目中使用的 Nuget 包:

    <PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.1.2" />

    <PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="5.0.12" />

    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.12" />

    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="5.0.2" />
    <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />

    <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="5.0.12" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.12">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>

    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.12" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="5.0.12" />

source code repo