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

 2022-12-29 21:50:44  阅读 0

IdentityServer4、 IClientStore、IResourceStore、IProfileService、IResourceOwnerPasswordValidator 在.NET中的实现。

续上篇;先理解其中的概念,可访问上篇,本篇以代码为主的实现过程。

 

认证授权服务的创建

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

创建API服务后,NuGet 安装 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 ApiScopes => new ApiScope[]
	{
		new ApiScope("add","新增"),
		new ApiScope("search","查询"),
		new ApiScope("shopping","购物"),
	};
        /// ApiResource 数据源方法
	/// 需要被认证授权的资源(服务站点)数据源
	public static IEnumerable 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 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 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 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);
	}
}

 

以下需要实现几个重要接口,可按照实际情况编写逻辑,来完成认证授权的整个过程。

  • IClientStore:客户端合法性的验证
  • IResourceStore:各资源间的关联关系
  • IResourceOwnerPasswordValidator:密码验证实现
  • IProfileService:用户(身份)信息的储存

 

为 Client 实现 IClientStore 接口

请求的Client是否合法,自定义客户端的验证逻辑。

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

 

为各资源实现 IResourceStore 接口

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

总结出来两条逻辑关联线:Scope、Claim。最后会得出:哪些服务被授权,哪些身份信息被认证。

/// 
/// 各个资源数据的查询方法 /// 包括:IdentityResource、ApiResource、ApiScope 三项资源 /// public class ResourceStore : IResourceStore { public Task FindApiResourcesByNameAsync(IEnumerable 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 FindApiResourcesByScopeNameAsync(IEnumerable 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 FindApiScopesByNameAsync(IEnumerable 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 FindIdentityResourcesByScopeNameAsync(IEnumerable 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 GetAllResourcesAsync() { return Task.FromResult(new Resources(DB.IdentityResources, DB.GetApiResources, DB.ApiScopes)); } }

 

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

实现自定义密码验证的逻辑,并将用户信息以Claims的方式返回验证结果。

/// 
/// 密码方式认证过程 /// public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { /// /// 1、验证 用户是否合法 /// 2、设定 身份基本信息 /// 3、设定 返回给调用者的 Response 结果信息 /// /// /// 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 多(自定义)属性 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", customResponse: new Dictionary { // Response 的 json 自定义追加项 { "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 的接口实现

用户信息(Claims)的认定过程,需要将哪些Claim作为身份信息,可按实际状况自定义实现逻辑。
这里按ids4提供的方式,按请求参数Client.Scope(IdentityResource)为标准,过滤不需要的Claim;或者全部Claim都作为身份信息。

/// 
/// 认证通过的用户资料信息 的处理,后续公布到Token中 /// 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; } }

以上主要的逻辑实现已经完成,以下开始注册配置ids4于服务中。

 

认证授权服务的注册

配置并启用已实现的接口类;或者追加额外的自定义扩展授权验证。

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 数据源中的产品服务、订单服务的Scopes都包含了`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产品服务,NuGet 安装 IdentityServer4.AccessTokenValidation(这里用的是v3.0.1)

授权成功的测试

注册产品服务的认证授权:

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 Get()
{
    /// 授权后的身份(用户)信息(从Token中提取的用户属性信息)
    var Principal = HttpContext.User;
    
    /// 返回 获取到的身份(用户)信息
    return new List { 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中的实践应用

注:用Token首次访问授权的产品服务时,产品服务会向[认证授权服务]请求jwks的验证,以确认[认证授权服务]是此Token的发布者。

 

授权失败的测试

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

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

 

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

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

 

标签:

如本站内容信息有侵犯到您的权益请联系我们删除,谢谢!!


Copyright © 2020 All Rights Reserved 京ICP5741267-1号 统计代码