IdentityServer4 – v4.x .Net中的实践应用

续上篇;先理解其中的概念,可访问上篇<IdentityServer4 - v4.x 概念理解及运行过程>,本篇以代码为主的实现过程。

 

认证授权服务的创建

以下内容以密码授权方式为例。

 

模拟访问DB各数据源

以下为模拟测试准备的数据源包括:Scope / ApiResource / IdentityResource / Client / 用户信息

为模拟准备的数据源类
/// 假设的用户模型 public class TestUser {     public string id { get; set; } = string.Empty;     public string username { get; set; } = string.Empty;     public string password { get; set; } = string.Empty;     public string nickname { get; set; } = string.Empty;     public string gender { get; set; } = string.Empty;     public string email { get; set; } = string.Empty;     public string phone { get; set; } = string.Empty;     public string address { get; set; } = string.Empty; } /// 假设的DB数据 public class DB { 	/// Scope数据源方法(4.x 时 很重要!!!) 	public static IEnumerable<ApiScope> ApiScopes => new ApiScope[] 	{ 		new ApiScope("add","新增"), 		new ApiScope("search","查询"), 		new ApiScope("shopping","购物"), 	};         /// ApiResource 数据源方法 	/// 需要被认证授权的资源(服务站点)数据源 	public static IEnumerable<ApiResource> GetApiResources => new ApiResource[] 	{ 	    new ApiResource("member", "会员服务") 	    { 	        // v4.x 时 很重要!!! 	        Scopes = { "add", "search" },                 // 指定此资源中,需要的身份(用户)信息(因此后续会存于Token中) 	        UserClaims={ JwtClaimTypes.NickName } 	    }, 	    new ApiResource("product", "产品服务")             {                 Scopes = { "add", "shopping" },                 UserClaims = { JwtClaimTypes.Name, JwtClaimTypes.NickName, "email", "depart", "role"}             }, 	    new ApiResource("order", "订单服务")             {                 Scopes = { "add", "shopping"},                 UserClaims = { JwtClaimTypes.Gender, "zip" }             } 	}; 	/// 身份资源配置数据源方法         /// 它定义了一个身份可以具备的所有属性         /// 一个 IdentityResource = 一组 Claim;如下的:Profile、org等 	public static IEnumerable<IdentityResource> IdentityResources => new IdentityResource[] 	{ 		// 必须项 		new IdentityResources.OpenId(), 		new IdentityResources.Profile(), 		// 扩展项 		new IdentityResources.Email(), 		new IdentityResources.Phone(), 		new IdentityResources.Address(), 		// 自定义追加项 		new IdentityResource("org",new string[]{"depart","role"}), 		new IdentityResource("zip",new string[]{"zip"}), 		new IdentityResource("_test",new string[]{"_test"}) 	}; 	/// 客户端数据源方法 	public static IEnumerable<Client> Clients => new Client[] 	{ 	    new Client 	    { 	        ClientId = "Cli-c", 	        ClientName="客户端-C-密码方式认证", 	        AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, 	        ClientSecrets = { new Secret("secret_code".Sha256()) }, 	        // 支持token过期后自动刷新token,增强体验 	        AllowOfflineAccess = true, 	        AccessTokenLifetime = 360000, 	        AllowedScopes = {    // Client.Scopes = Scope + IdentityResource                     // 以下为Scope数据源中必须具备的                     "add", "search", "shopping",                     // 以下为IdentityResource数据源中必须具备的                     IdentityServerConstants.StandardScopes.OpenId,                     IdentityServerConstants.StandardScopes.Profile,                     JwtClaimTypes.Email, "org","zip","_test",                     // 为配合 AllowOfflineAccess 属性                     IdentityServerConstants.StandardScopes.OfflineAccess 	        } 	    } 	}; 	/// 用户数据源方法 	public static IEnumerable<TestUser> Users => new TestUser[] { 	    new TestUser{ 	        id = "10001", username = "sol", password = "123", nickname = "Sol",                 email = "sol@domain.com", phone="13888888888", gender = "男", address="jingan" 	    }, 	    new TestUser{ 	        id = "10002", username = "song", password = "123", nickname = "Song",                 email = "song@domain.com", phone="13888888888", gender = "女", address="jingan" 	    } 	}; 	/// 用户是否激活方法 	public static bool GetUserActive(string userid) 	{ 	    return Users.Any(a => a.id == userid); 	} }

 

为 Client 实现 IClientStore 接口

/// 客户端数据查询 public class ClientStore : IClientStore {         // 客户端验证方法 	public Task<Client> FindClientByIdAsync(string clientId) 	{ 		// 数据库查询 Client 信息 		var client = DB.Clients.FirstOrDefault(c => c.ClientId == clientId) ?? new Client(); 		client.AccessTokenLifetime = 36000; 		return Task.FromResult(client); 	} }

为 ApiResource 实现 IResourceStore 接口

从中可以理出 IdentityResource、ApiResource、ApiScope 三者的关系。

/// <summary> /// 各个资源数据的查询方法 /// 包括:IdentityResource、ApiResource、ApiScope 三项资源 /// </summary> public class ResourceStore : IResourceStore {     public Task<IEnumerable<ApiResource> FindApiResourcesByNameAsync(IEnumerable<string> apiResourceNames)     {         if (apiResourceNames == null) throw new ArgumentNullException(nameof(apiResourceNames));         var result = DB.GetApiResources.Where(r => apiResourceNames.Contains(r.Name));         return Task.FromResult(result);     }     public Task<IEnumerable<ApiResource> FindApiResourcesByScopeNameAsync(IEnumerable<string> scopeNames)     {         if (scopeNames == null) throw new ArgumentNullException(nameof(scopeNames));         var result = DB.GetApiResources.Where(t => t.Scopes.Any(item => scopeNames.Contains(item)));         return Task.FromResult(result);     }     public Task<IEnumerable<ApiScope> FindApiScopesByNameAsync(IEnumerable<string> scopeNames)     {         if (scopeNames == null) throw new ArgumentNullException(nameof(scopeNames));         var result = DB.ApiScopes.Where(w => scopeNames.Contains(w.Name));         return Task.FromResult(result);     }     public Task<IEnumerable<IdentityResource> FindIdentityResourcesByScopeNameAsync(IEnumerable<string> scopeNames)     {         if (scopeNames == null) throw new ArgumentNullException(nameof(scopeNames));         var result = DB.IdentityResources.Where(w => scopeNames.Contains(w.Name));         return Task.FromResult(result);     }     public Task<Resources> GetAllResourcesAsync()     {         return Task.FromResult(new Resources(DB.IdentityResources, DB.GetApiResources, DB.ApiScopes));     } }

密码方式验证用户,实现 IResourceOwnerPasswordValidator 接口

/// <summary> /// 密码方式认证过程 /// </summary> public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator {     /// <summary>     /// 1、验证 用户是否合法     /// 2、设定 身份基本信息     /// 3、设定 返回给调用者的 Response 结果信息     /// </summary>     /// <param name="context"></param>     /// <returns></returns>     public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)     {         try         {             //验证用户,用户名和密码是否正确             var user = DB.Users.FirstOrDefault(u => u.username == context.UserName && u.password == context.Password);              if (user != null)             {                 #region 设置 身份(用户)基本信息                 // 身份信息的相关属性,带入到ids4中                 var claimList = new List<Claim>()                 {                     // Claim 多(自定义)属性                     new Claim(JwtClaimTypes.Name,user.username),                     new Claim(JwtClaimTypes.NickName,user.nickname),                     new Claim(JwtClaimTypes.Email,user.email),                     new Claim(JwtClaimTypes.Gender,user.gender),                     new Claim(JwtClaimTypes.PhoneNumber,user.phone),                     new Claim("zip","200000"),                     new Claim("_test","_测试")                 };                                  // 追加Claim自定义用户属性(角色/所属部门)                 string[] roles = new string[] { "SupperManage", "manage", "admin", "member" };                 string[] departs = new string[] { "销售部", "人事部", "总经理办公室" };                 foreach (var rolename in roles)                 {                     claimList.Add(new Claim(JwtClaimTypes.Role, rolename));                 }                 foreach (var departname in departs)                 {                     claimList.Add(new Claim("depart", departname));                 }                 #endregion                   #region 设置 返回给调用者的Response信息                  // 在以下 GrantValidationResult 类中                 // 1、通过以上已组装的 ClaimList,再追加上系统必须的Claim项,组装成最终的Claims                 // 2、用 Claims ==> 创建出 ClaimsIdentity ==> 再创建出 ClaimsPrincipal                 // 以完成 Response 的 json 结果 返回给 调用者                  context.Result = new GrantValidationResult(                     subject: user.id,                     claims: claimList,                     authenticationMethod: "db_pwdmode",                     // Response 的 json 自定义追加项                     customResponse: new Dictionary<string, object> {                         { "custom_append_author", "认证授权请求的Response自定义追加效果" },                         { "custom_append_discription", "认证授权请求的Response自定义追加效果" }                     }                 );                 #endregion             }             else if (user == null)             {                 context.Result = new GrantValidationResult(                     TokenRequestErrors.InvalidGrant,                     "用户认证失败,账号或密码不存在;无效的自定义证书。"                 );             }         }         catch (Exception ex)         {             context.Result = new GrantValidationResult()             {                 IsError = true,                 Error = ex.Message             };         }         return Task.CompletedTask;     } }

用户信息 Profile 的接口实现

/// <summary> /// 认证通过的用户资料信息 的处理,后续公布到Token中 /// </summary> public class UserProfileService : IProfileService {     // 把需要公开到Token中的用户claim信息,放到指定的IssuedClaims中,为后续生成 Token 所用     public Task GetProfileDataAsync(ProfileDataRequestContext context)     {         var userid = context.Subject.GetSubjectId();         if (userid != null)         {             var claims = context.Subject.Claims.ToList();              // 此方法,会依据Client请求的Scope(IdentityResource.Claims),过滤Claim后的集合放入到 IssuedClaims 中             // 1、Client.Scope(IdentityResource)找到身份中的Claims             // 2、与用户信息生成的Claims匹配,将结果放入IssuedClaims中             context.AddRequestedClaims(claims);              // 不按 Client.Scope(IdentityResource.Claims) 的过滤,所有的用户claim全部放入             // context.IssuedClaims = claims.ToList();         }         return Task.CompletedTask;     }      public Task IsActiveAsync(IsActiveContext context)     {         string userid = context.Subject.GetSubjectId();         // 查询 DB,ids4需要知道 用户是否已激活         context.IsActive = DB.GetUserActive(userid);         return Task.CompletedTask;     } }

认证授权服务配置

public class Program {     public static void Main(string[] args)     {         var builder = WebApplication.CreateBuilder(args);         builder.Services.AddControllers();          #region IdentityServer 的配置         builder.Services.AddIdentityServer()             // 支持开发环境的签名证书             .AddDeveloperSigningCredential()             // 分别注册各自接口的实现类             .AddResourceStore().AddClientStore().AddResourceOwnerValidator().AddProfileService();             // 可追加的扩展             //.AddExtensionGrantValidator<微信自定义扩展模式>();         #endregion         builder.Services.AddEndpointsApiExplorer();         builder.Services.AddSwaggerGen();            var app = builder.Build();         if (app.Environment.IsDevelopment())         {             app.UseSwagger();             app.UseSwaggerUI();         }         app.UseRouting();                  #region 使用 ids4 服务         // 它需要在 [路由] 之后,[授权] 之前。         app.UseIdentityServer();         app.UseAuthorization();         #endregion           app.UseEndpoints(endpoints =>         {             endpoints.MapControllers();         });         app.Run();     } }

认证授权服务请求效果

IdentityServer4 - v4.x .Net中的实践应用

从上图看出:用户密码验证成功、客户端及密钥Secret验证成功。

这里重点解释下Scope:

Client参数Scope中包含了: Scope(shopping) + IdentityResource(openid+profile+org+email+zip)

ApiResource 数据源中的产品服务、订单服务都包含了shopping,所以access_token可以访问这两个服务。

Client/IdentityResource/ApiResource 数据源中已定义了 openid+profile+org+email+zip,所以access_token中包含了此用户信息。

认证授权服务 /connect/userinfo 取得的身份信息图例:

IdentityServer4 - v4.x .Net中的实践应用

上图结果显示:Client.Scope(IdentityResource.Claims) 匹配到的 ApiResources.UserClaims 合并的结果

解析Token数据图例:

IdentityServer4 - v4.x .Net中的实践应用

上图显示:

aud:已授权的(Client.Scope匹配到的)ApiResource服务名称集合(product/order)

name/email/role/zip/...的Claims:已授权服务(product/order)下的UserClaims合并的结果

client_id:申请的客户端标识

nbf/exp:认证授权时间/token过期时间

 

Token访问授权服务

授权成功的测试

创建一个API产品服务,配置产品服务

var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers();   #region Authentication 授权认证 builder.Services.AddAuthorization(); builder.Services.AddAuthentication(options => {     // 数据格式设定,以 IdentityServer 风格为准     options.DefaultScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;     options.DefaultAuthenticateScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;     options.DefaultChallengeScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;     options.DefaultForbidScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;     options.DefaultSignInScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;     options.DefaultSignOutScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme; }) .AddIdentityServerAuthentication(options => {     options.Authority = "http://localhost:5007";    // IdentityServer 授权服务地址     options.RequireHttpsMetadata = false;           // 不需要https     options.ApiName = "product";                    // 当前服务名称(与认证授权服务中 ApiResources 的名称对应) }); #endregion    builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); if (app.Environment.IsDevelopment()) {     app.UseSwagger();     app.UseSwaggerUI(); } app.UseRouting();   #region IdentityServer4 注册 // 放在路由之后,授权之前 app.UseAuthentication(); app.UseAuthorization(); #endregion   app.MapControllers(); app.Run();

Product产品服务中设定Authorize必须授权并且角色为SupperManage的Action:

/// 获取当前身份信息 [HttpGet, Authorize(Roles = "SupperManage")] public IEnumerable<object> Get() {     /// 授权后的身份(用户)信息(从Token中提取的用户属性信息)     var Principal = HttpContext.User;          /// 返回 获取到的身份(用户)信息     return new List<object> { new     {         product_service_claims = new {             UserId = Principal.Claims.FirstOrDefault(oo => oo.Type == "sub")?.Value,             UserName = Principal.Claims.FirstOrDefault(oo => oo.Type == JwtClaimTypes.Name)?.Value,             NickName = Principal.Claims.FirstOrDefault(oo => oo.Type == JwtClaimTypes.NickName)?.Value,             Email = Principal.Claims.FirstOrDefault(oo => oo.Type == JwtClaimTypes.Email)?.Value         },         order_service_claims = new {             Gender = Principal.Claims.FirstOrDefault(oo => oo.Type == JwtClaimTypes.Gender)?.Value,             Zip = Principal.Claims.FirstOrDefault(oo => oo.Type == "zip")?.Value         },         ApiResource中不存在的Claim = new {             _Test = Principal.Claims.FirstOrDefault(oo => oo.Type == "_test")?.Value         }     }}; }

以上Product产品服务中Action取得当前身份(用户)部分信息效果图:

IdentityServer4 - v4.x .Net中的实践应用

 

授权失败的测试

按产品服务的创建过程,再创建一个API会员服务member;

ApiResource数据源会员服务Scopes中不存在sopping;以上过程 Token 的 aud 只有 product/order,不存在会员服务member;

 

用以上 Token 访问会员服务的测试,预期结果:授权失败。如下图:

IdentityServer4 - v4.x .Net中的实践应用

 

发表评论

相关文章