.net Core5.0使用IdentityServer4 进行权限控制
IdentityServer4 ASP.NET Core的一个包含OpenID Connect和OAuth 2.0协议的框架,提供了单点登录,集中控制,API访问控制等功能。
OpenID 和 OAuth 的区别
OpenID :Authentication,即认证,用户是谁?
OAuth :Authorization,即授权,用户能做哪些操作?
OpenID Connect(OIDC):基于OAuth协议,是“认证”和“授权”的结合。
OAuth2提供了Access Token来解决授权第三方客户端访问受保护资源的问题。 OIDC在这个基础上提供了ID Token来解决第三方客户端标识用户身份认证的问题。
授权模式应用场景(IdentityServer4,NET Core下的安全框架)
客户端模式:适用于和用户无关,机器与机器之间直接交互访问资源的场景。
POST https://api.oauth2server.com/token
grant_type = client_credentials&
client_id = CLIENT_ID&
client_secret = CLIENT_SECRET
密码模式:适用于当前的APP是专门为服务端设计的情况。
POST https://api.oauth2server.com/token
grant_type=password&
username=USERNAME&
password=PASSWORD&
client_id=CLIENT_ID
简化模式:适用于浏览器WEB应用,支持
-
-
- 用户认证(JavaScript 应用或传统服务端渲染的Web应用)
- 用户认证+授权(JavaScript应用)
-
简化模式下ID Token和Access Token都是通过浏览器的前端通道传递的。
所以如果是传统服务端Web应用并且仅是在服务端使用Access Token的话,推荐使用Hybrid Flow。
授权码模式:授权码模式通过后台传输Tokens,相对于简化模式会更安全一点。
但每当考虑使用授权码模式的时候,请使用混合模式。混合模式会首先返回一个可验证的ID Token并且有更多其他特性。
混合模式:适用于服务器端 Web 应用程序和原生桌面/移动应用程序。
混合模式是简化模式和授权码模式的组合。混合模式下ID Token通过浏览器的前端通道传递,而Access Token和Refresh Token通过后端通道取得。
下面直接上代码
发布令牌服务 和 验证令牌服务 分成两个服务单独部署同时发布
引用的包 <PackageReference Include="IdentityServer4" Version="3.1.3" />
方式一:
例子一的发布服务
/// <summary> /// 客户端模式 /// </summary> public class ClientInitConfig { /// <summary> /// 定义ApiResource /// 这里的资源(Resources)指的就是管理的API /// </summary> /// <returns>多个ApiResource</returns> public static IEnumerable<ApiResource> GetApiResources() { return new[] { new ApiResource("UserApi", "用户获取API") }; } /// <summary> /// 定义验证条件的Client /// </summary> /// <returns></returns> public static IEnumerable<Client> GetClients() { return new[] { new Client { ClientId = "MengLin.Shopping.Web",//客户端惟一标识 ClientSecrets = new [] { new Secret("MengLin123456".Sha256()) },//客户端密码,进行了加密 AllowedGrantTypes = GrantTypes.ClientCredentials,//Grant类型 AllowedScopes = new [] { "UserApi" },//允许访问的资源 Claims= new List<Claim>() { new Claim(IdentityModel.JwtClaimTypes.Role,"Admin"), new Claim(IdentityModel.JwtClaimTypes.NickName,"豆豆爸爸"), new Claim("EMail","menglin2010@126.com") } } }; } }
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) { services.AddIdentityServer()//怎么处理 .AddDeveloperSigningCredential() .AddInMemoryClients(ClientInitConfig.GetClients())//InMemory 内存模式 .AddInMemoryApiResources(ClientInitConfig.GetApiResources());//能访问啥资源 services.AddControllers(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //app.UseRouting(); app.UseIdentityServer(); app.UseAuthorization(); //app.UseEndpoints(endpoints => //{ // endpoints.MapControllers(); //}); } }
例子一的验证服务:
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) { //鉴权 services.AddAuthentication("Bearer") .AddJwtBearer("Bearer", options => { //ids4的地址,目的: 获取公钥,因为获取获取了公钥才能解密 options.Authority = "http://localhost:5000";//ids4的地址 不是本地启动的地址 options.Audience = "UserApi"; options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false //ValidAudiences = new List<string>() { "api1", "api2", "api3" } }; IdentityModelEventSource.ShowPII = true; }); //自定义授权--必须包含Claim client_role & 必须是Admin services.AddAuthorization(options => { options.AddPolicy("AdminPolicy", policyBuilder => policyBuilder .RequireAssertion(context => context.User.HasClaim(c => c.Type == "client_role") && context.User.Claims.First(c => c.Type.Equals("client_role")).Value.Equals("Admin"))); }); //自定义授权--必须包含Claim client_EMail & 必须qq结尾 services.AddAuthorization(options => { options.AddPolicy("EMailPolicy", policyBuilder => policyBuilder .RequireAssertion(context => context.User.HasClaim(c => c.Type == "client_EMail") && context.User.Claims.First(c => c.Type.Equals("client_EMail")).Value.EndsWith("@qq.com"))); }); services.AddControllers(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } }
namespace IdentityServer4_02.Controllers { [Route("api/[controller]/[action]")] [ApiController] public class HomeController : ControllerBase { [HttpGet] [Authorize(Policy = "AdminPolicy")] public IActionResult Get() { return Ok("Get"); } [HttpGet] [AllowAnonymous] public IActionResult Index() { return Ok("Index"); } [HttpGet] [Authorize(Policy = "EMailPolicy")] public IActionResult Set() { return Ok("Set"); } } }
方式二:
例子二的发布服务
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) { // configure identity server with in-memory stores, keys, clients and scopes services.AddIdentityServer() .AddDeveloperSigningCredential() .AddInMemoryIdentityResources(Config.GetIdentityResourceResources()) .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients()) .AddTestUsers(Config.GetUsers()) .AddProfileService<CustomProfileService>() .AddResourceOwnerValidator<CustomResourceOwnerPasswordValidator>(); services.AddControllers(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseIdentityServer(); } }
public class Config { public static IEnumerable<IdentityResource> GetIdentityResourceResources() { var customProfile = new IdentityResource( name: "custom.profile", displayName: "Custom profile", claimTypes: new[] { "role" } ); return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), customProfile }; #region 方式二 //return new[] //{ // new IdentityResources.OpenId(), // new IdentityResources.Profile(), // new IdentityResources.Email(), // new IdentityResource // { // Name = "role", // UserClaims = new List<string>{"role"} // } //}; #endregion } // scopes define the API resources in your system public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { //new ApiResource("api1", "My API") new ApiResource("api1", "My API",new List<string>(){JwtClaimTypes.Role}) }; } // clients want to access resources (aka scopes) public static IEnumerable<Client> GetClients() { // client credentials client return new List<Client> { new Client { ClientId = "client", AllowedGrantTypes = GrantTypes.ClientCredentials, ClientSecrets = { new Secret("secret".Sha256()) }, AllowedScopes = { "api1" , IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile } }, // resource owner password grant client new Client { ClientId = "ro.client", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, ClientSecrets = { new Secret("secret".Sha256()) }, AllowedScopes = { "api1" , IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "custom.profile" } } }; } public static List<TestUser> GetUsers() { return new List<TestUser> { new TestUser { SubjectId = "1", Username = "alice", Password = "password", Claims = new List<Claim>(){ new Claim(JwtClaimTypes.Role,"superadmin") } }, new TestUser { SubjectId = "2", Username = "bob", Password = "password", Claims = new List<Claim>(){ new Claim(JwtClaimTypes.Role,"admin") }, } }; } }
public class CustomProfileService : IProfileService { /// <summary> /// The logger /// </summary> protected readonly ILogger Logger; /// <summary> /// The users /// </summary> protected readonly TestUserStore Users; /// <summary> /// Initializes a new instance of the <see cref="TestUserProfileService"/> class. /// </summary> /// <param name="users">The users.</param> /// <param name="logger">The logger.</param> public CustomProfileService(TestUserStore users, ILogger<TestUserProfileService> logger) { Users = users; Logger = logger; } /// <summary> /// 只要有关用户的身份信息单元被请求(例如在令牌创建期间或通过用户信息终点),就会调用此方法 /// </summary> /// <param name="context">The context.</param> /// <returns></returns> public virtual Task GetProfileDataAsync(ProfileDataRequestContext context) { context.LogProfileRequest(Logger); //判断是否有请求Claim信息 if (context.RequestedClaimTypes.Any()) { //根据用户唯一标识查找用户信息 var user = Users.FindBySubjectId(context.Subject.GetSubjectId()); if (user != null) { //调用此方法以后内部会进行过滤,只将用户请求的Claim加入到 context.IssuedClaims 集合中 这样我们的请求方便能正常获取到所需Claim context.AddRequestedClaims(user.Claims); } } context.LogIssuedClaims(Logger); return Task.CompletedTask; } /// <summary> /// 验证用户是否有效 例如:token创建或者验证 /// </summary> /// <param name="context">The context.</param> /// <returns></returns> public virtual Task IsActiveAsync(IsActiveContext context) { Logger.LogDebug("IsActive called from: {caller}", context.Caller); var user = Users.FindBySubjectId(context.Subject.GetSubjectId()); context.IsActive = user?.IsActive == true; return Task.CompletedTask; } }
public class CustomResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { /// <summary> /// 这里为了演示我们还是使用TestUser作为数据源, /// 正常使用此处应当传入一个 用户仓储 等可以从 /// 数据库或其他介质获取我们用户数据的对象 /// </summary> private readonly TestUserStore _users; private readonly ISystemClock _clock; public CustomResourceOwnerPasswordValidator(TestUserStore users, ISystemClock clock) { _users = users; _clock = clock; } /// <summary> /// 验证 /// </summary> /// <param name="context"></param> /// <returns></returns> public Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { //此处使用context.UserName, context.Password 用户名和密码来与数据库的数据做校验 if (_users.ValidateCredentials(context.UserName, context.Password)) { var user = _users.FindByUsername(context.UserName); //验证通过返回结果 //subjectId 为用户唯一标识 一般为用户id //authenticationMethod 描述自定义授权类型的认证方法 //authTime 授权时间 //claims 需要返回的用户身份信息单元 此处应该根据我们从数据库读取到的用户信息 添加Claims 如果是从数据库中读取角色信息,那么我们应该在此处添加 context.Result = new GrantValidationResult( user.SubjectId ?? throw new ArgumentException("Subject ID not set", nameof(user.SubjectId)), OidcConstants.AuthenticationMethods.Password, _clock.UtcNow.UtcDateTime, user.Claims); } else { //验证失败 context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "invalid custom credential"); } return Task.CompletedTask; } }
例子二的验证服务
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) { services.AddControllers(); services.AddAuthentication("Bearer") .AddJwtBearer("Bearer", options => { options.Audience = "api1"; options.Authority = "http://localhost:5000"; options.RequireHttpsMetadata = false;//如果不使用Https,则需要配置这个 //options.TokenValidationParameters = new TokenValidationParameters() //{ // ValidAudiences = new List<string>() { "api1", "api2", "api3" } // ValidateAudience = false //}; options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false }; IdentityModelEventSource.ShowPII = true; }); //services.AddAuthentication("Bearer") // .AddJwtBearer("Bearer", options => // { // //ids4的地址,目的: 获取公钥,因为获取获取了公钥才能解密 // options.Authority = "http://localhost:5001"; // options.Audience = "UserApi"; // options.RequireHttpsMetadata = false; // //options.TokenValidationParameters = new TokenValidationParameters() // //{ // // ValidAudiences = new List<string>() { "api1", "api2", "api3" } // //}; // }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); //app.UseCors("default"); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } }
namespace IdentityServer4_04.Controllers { [Route("api/[controller]/[action]")] [ApiController] public class HomeController : ControllerBase { [HttpPost] [Authorize] public IActionResult Get() { return Ok("Get"); } [HttpGet] [Authorize(Policy = "AdminPolicy")] public IActionResult Gets() { return Ok("Gets"); } [HttpGet] [AllowAnonymous] public IActionResult Index() { return Ok("Index"); } [HttpGet] [Authorize(Policy = "EMailPolicy")] public IActionResult Set() { return Ok("Set"); } } }