授权属性不适用于 IdentityServer4 和 .NET Core 3.1
Authorize attribute not working with IdentityServer4 and .NET Core 3.1
我有一个 .NET Core 3.1 项目使用 Identity 和 IdentityServer4 来实现资源所有者密码授权类型。我可以毫无问题地获得令牌,但 [Authorize] 属性不起作用,它只是让一切通过。重要的一点是我的 API 和身份服务器在同一个项目中。从网上的评论来看,这似乎是一个中间件顺序问题,但我似乎找不到有效的组合。我已经仔细检查过,当没有附加授权 header 时,端点代码仍然被命中。
这是我的 Startup.cs 文件:
using System;
using System.Collections.Generic;
using IdentityServer4.Models;
using LaunchpadSept2020.App;
using LaunchpadSept2020.App.Repositories;
using LaunchpadSept2020.App.Repositories.Interfaces;
using LaunchpadSept2020.App.Seeds;
using LaunchpadSept2020.Models.Entities;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace LaunchpadSept2020.Api
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Set up the database
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"),
b =>
{
b.MigrationsAssembly("LaunchpadSept2020.App");
})
);
services.AddIdentity<User, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
options.Password.RequiredLength = 6;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireDigit = true;
});
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.ApiName = "launchpadapi";
options.Authority = "http://localhost:25000";
options.RequireHttpsMetadata = false;
});
services.AddIdentityServer()
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder => builder.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"),
npgSqlOptions =>
{
npgSqlOptions.MigrationsAssembly("LaunchpadSept2020.App");
});
})
.AddInMemoryClients(Clients.Get())
.AddAspNetIdentity<User>()
.AddInMemoryIdentityResources(Resources.GetIdentityResources())
.AddInMemoryApiResources(Resources.GetApiResources())
.AddInMemoryApiScopes(Resources.GetApiScopes())
.AddDeveloperSigningCredential();
services.AddControllers();
// Add Repositories to dependency injection
services.AddScoped<ICompanyRepository, CompanyRepository>();
services.AddScoped<IUserRepository, UserRepository>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, UserManager<User> userManager, RoleManager<IdentityRole> roleManager)
{
// Initialize the database
UpdateDatabase(app);
// Seed data
UserAndRoleSeeder.SeedUsersAndRoles(roleManager, userManager);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//app.UseHttpsRedirection();
app.UseRouting();
app.UseIdentityServer(); // Includes UseAuthentication
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
// Update the database to the latest migrations
private static void UpdateDatabase(IApplicationBuilder app)
{
using (var serviceScope = app.ApplicationServices
.GetRequiredService<IServiceScopeFactory>()
.CreateScope())
{
using (var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>())
{
context.Database.Migrate();
}
}
}
}
internal class Clients
{
public static IEnumerable<Client> Get()
{
return new List<Client>
{
new Client
{
ClientId = "mobile",
ClientName = "Mobile Client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets = { new Secret("MySecret".Sha256()) },
AllowedScopes = new List<String> { "launchpadapi.read" }
//AllowAccessTokensViaBrowser = true,
//RedirectUris = { "http://localhost:25000/signin-oidc" },
//PostLogoutRedirectUris = { "http://localhost:25000/signout-callback-oidc" },
//AllowOfflineAccess = true
}
};
}
}
internal class Resources
{
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource
{
Name = "role",
UserClaims = new List<string> {"role"}
}
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new[]
{
new ApiResource
{
Name = "launchpadapi",
DisplayName = "Launchpad API",
Description = "Allow the application to access the Launchpad API on your behalf",
Scopes = new List<string> { "launchpadapi.read", "launchpadapi.write"},
ApiSecrets = new List<Secret> {new Secret("ScopeSecret".Sha256())},
UserClaims = new List<string> {"role"}
}
};
}
public static IEnumerable<ApiScope> GetApiScopes()
{
return new[]
{
new ApiScope("launchpadapi.read", "Read Access to Launchpad API"),
new ApiScope("launchpadapi.write", "Write Access to Launchpad API")
};
}
}
}
还有我的控制器:
using System.Collections.Generic;
using System.Threading.Tasks;
using LaunchpadSept2020.App.Repositories.Interfaces;
using LaunchpadSept2020.Models.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LaunchpadSept2020.Api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class CompanyController : ControllerBase
{
private readonly ICompanyRepository _companyRepository;
public CompanyController(ICompanyRepository companyRepository)
{
_companyRepository = companyRepository;
}
[HttpPost]
[Authorize]
public async Task<ActionResult<CompanyVM>> Create([FromBody] CompanyCreateVM data)
{
// Make sure model has all required fields
if (!ModelState.IsValid)
return BadRequest("Invalid data");
try
{
var result = await _companyRepository.Create(data);
return Ok(result);
}
catch
{
return StatusCode(500);
}
}
[HttpGet]
[Authorize]
public async Task<ActionResult<List<CompanyVM>>> GetAll()
{
try
{
var results = await _companyRepository.GetAll();
return Ok(results);
}
catch
{
return StatusCode(500);
}
}
}
}
我认为一个普遍的问题是您将 IdentityServer 与 ASP.NET 身份混合在同一个应用程序中,总的来说,我的经验是很难知道谁在做什么,也很难完全理解。我总是建议将 IdentityServer 和 API 放在独立的服务中。只是为了清楚地分离关注点。
对于本地 API 身份验证,您需要在启动中进行以下额外配置:
public void ConfigureServices(IServiceCollection services)
{
....
// After services.AddIdentityServer()
services.AddLocalApiAuthentication();
}
参考 docs。
然后您需要将本地 API 政策指定为 API 上 Authorize
属性的一部分:
[Authorize(LocalApi.PolicyName)]
见当地人 API example.
我有一个 .NET Core 3.1 项目使用 Identity 和 IdentityServer4 来实现资源所有者密码授权类型。我可以毫无问题地获得令牌,但 [Authorize] 属性不起作用,它只是让一切通过。重要的一点是我的 API 和身份服务器在同一个项目中。从网上的评论来看,这似乎是一个中间件顺序问题,但我似乎找不到有效的组合。我已经仔细检查过,当没有附加授权 header 时,端点代码仍然被命中。
这是我的 Startup.cs 文件:
using System;
using System.Collections.Generic;
using IdentityServer4.Models;
using LaunchpadSept2020.App;
using LaunchpadSept2020.App.Repositories;
using LaunchpadSept2020.App.Repositories.Interfaces;
using LaunchpadSept2020.App.Seeds;
using LaunchpadSept2020.Models.Entities;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace LaunchpadSept2020.Api
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Set up the database
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"),
b =>
{
b.MigrationsAssembly("LaunchpadSept2020.App");
})
);
services.AddIdentity<User, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
options.Password.RequiredLength = 6;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireDigit = true;
});
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.ApiName = "launchpadapi";
options.Authority = "http://localhost:25000";
options.RequireHttpsMetadata = false;
});
services.AddIdentityServer()
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder => builder.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"),
npgSqlOptions =>
{
npgSqlOptions.MigrationsAssembly("LaunchpadSept2020.App");
});
})
.AddInMemoryClients(Clients.Get())
.AddAspNetIdentity<User>()
.AddInMemoryIdentityResources(Resources.GetIdentityResources())
.AddInMemoryApiResources(Resources.GetApiResources())
.AddInMemoryApiScopes(Resources.GetApiScopes())
.AddDeveloperSigningCredential();
services.AddControllers();
// Add Repositories to dependency injection
services.AddScoped<ICompanyRepository, CompanyRepository>();
services.AddScoped<IUserRepository, UserRepository>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, UserManager<User> userManager, RoleManager<IdentityRole> roleManager)
{
// Initialize the database
UpdateDatabase(app);
// Seed data
UserAndRoleSeeder.SeedUsersAndRoles(roleManager, userManager);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//app.UseHttpsRedirection();
app.UseRouting();
app.UseIdentityServer(); // Includes UseAuthentication
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
// Update the database to the latest migrations
private static void UpdateDatabase(IApplicationBuilder app)
{
using (var serviceScope = app.ApplicationServices
.GetRequiredService<IServiceScopeFactory>()
.CreateScope())
{
using (var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>())
{
context.Database.Migrate();
}
}
}
}
internal class Clients
{
public static IEnumerable<Client> Get()
{
return new List<Client>
{
new Client
{
ClientId = "mobile",
ClientName = "Mobile Client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets = { new Secret("MySecret".Sha256()) },
AllowedScopes = new List<String> { "launchpadapi.read" }
//AllowAccessTokensViaBrowser = true,
//RedirectUris = { "http://localhost:25000/signin-oidc" },
//PostLogoutRedirectUris = { "http://localhost:25000/signout-callback-oidc" },
//AllowOfflineAccess = true
}
};
}
}
internal class Resources
{
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource
{
Name = "role",
UserClaims = new List<string> {"role"}
}
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new[]
{
new ApiResource
{
Name = "launchpadapi",
DisplayName = "Launchpad API",
Description = "Allow the application to access the Launchpad API on your behalf",
Scopes = new List<string> { "launchpadapi.read", "launchpadapi.write"},
ApiSecrets = new List<Secret> {new Secret("ScopeSecret".Sha256())},
UserClaims = new List<string> {"role"}
}
};
}
public static IEnumerable<ApiScope> GetApiScopes()
{
return new[]
{
new ApiScope("launchpadapi.read", "Read Access to Launchpad API"),
new ApiScope("launchpadapi.write", "Write Access to Launchpad API")
};
}
}
}
还有我的控制器:
using System.Collections.Generic;
using System.Threading.Tasks;
using LaunchpadSept2020.App.Repositories.Interfaces;
using LaunchpadSept2020.Models.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LaunchpadSept2020.Api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class CompanyController : ControllerBase
{
private readonly ICompanyRepository _companyRepository;
public CompanyController(ICompanyRepository companyRepository)
{
_companyRepository = companyRepository;
}
[HttpPost]
[Authorize]
public async Task<ActionResult<CompanyVM>> Create([FromBody] CompanyCreateVM data)
{
// Make sure model has all required fields
if (!ModelState.IsValid)
return BadRequest("Invalid data");
try
{
var result = await _companyRepository.Create(data);
return Ok(result);
}
catch
{
return StatusCode(500);
}
}
[HttpGet]
[Authorize]
public async Task<ActionResult<List<CompanyVM>>> GetAll()
{
try
{
var results = await _companyRepository.GetAll();
return Ok(results);
}
catch
{
return StatusCode(500);
}
}
}
}
我认为一个普遍的问题是您将 IdentityServer 与 ASP.NET 身份混合在同一个应用程序中,总的来说,我的经验是很难知道谁在做什么,也很难完全理解。我总是建议将 IdentityServer 和 API 放在独立的服务中。只是为了清楚地分离关注点。
对于本地 API 身份验证,您需要在启动中进行以下额外配置:
public void ConfigureServices(IServiceCollection services)
{
....
// After services.AddIdentityServer()
services.AddLocalApiAuthentication();
}
参考 docs。
然后您需要将本地 API 政策指定为 API 上 Authorize
属性的一部分:
[Authorize(LocalApi.PolicyName)]
见当地人 API example.