续上篇;先理解其中的概念,可访问上篇<IdentityServer4 - v4.x 概念理解及运行过程>,本篇以代码为主的实现过程。
作者:[Sol·wang] - 博客园,原文出处:https://www.cnblogs.com/Sol-wang/p/17010169.html
一、创建认证授权服务
以下内容以密码授权方式为例。
创建API服务后,NuGet安装IdentityServer4(v4.x)
1.1 模拟访问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);
}
}
以下需要实现几个重要接口,可按照实际情况编写逻辑,来完成认证授权的整个过程。
- IClientStore:客户端合法性的验证
- IResourceStore:各类资源间的关联关系
- IResourceOwnerPasswordValidator:密码验证实现
- IProfileService:用户(身份)信息的储存
1.2 为Client实现IClientStore接口
请求的Client是否合法,自定义客户端的验证逻辑。
/// 客户端数据查询
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);
}
}
1.3 为各类资源实现IResourceStore接口
从中可以理出 IdentityResource、ApiResource、ApiScope 三者的关系,蛮重要的。
总结出来两条逻辑关联线:Scope、Claim。最后会得出:哪些服务被授权,哪些身份信息被认证。
/// <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));
}
}
1.4 密码方式验证用户,实现IResourceOwnerPasswordValidator接口
实现自定义密码验证的逻辑,并将用户信息以Claims的方式返回验证结果。
/// <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",
customResponse: new Dictionary<string, object> {
// 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;
}
}
1.5 用户信息Profile的接口实现
用户信息(Claims)的认定过程,需要将哪些Claim作为身份信息,可按实际状况自定义实现逻辑。
这里按ids4提供的方式,按请求参数Client.Scope(IdentityResource)为标准,过滤不需要的Claim;或者全部Claim都作为身份信息。
/// <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;
}
}
以上主要的逻辑实现已经完成,以下开始注册配置ids4于服务中。
1.6 认证授权服务的注册
配置并启用已实现的接口类;或者追加额外的自定义扩展授权验证。
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();
}
}
二、运行认证授权服务
2.1 认证授权服务请求效果
从上图看出:用户密码验证成功、客户端及密钥Secret验证成功。
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中包含了此用户信息。
2.2 取接口 /connect/userinfo 的数据
上图结果显示:Client.Scope(IdentityResource.Claims) 匹配到的ApiResources.UserClaims合并的结果
2.3 解析 Token 数据
上图显示:
aud:已授权的(Client.Scope匹配到的)ApiResource服务名称集合(product/order)
name/email/role/zip/...的Claims:已授权服务(product/order)下的UserClaims合并的结果
client_id:申请的客户端标识
nbf/exp:认证授权时间/token过期时间
三、令牌访问已被授权的服务
创建一个用于授权的API产品服务,NuGet安装IdentityServer4.AccessTokenValidation(这里用的是v3.0.1)
3.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<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取得当前身份(用户)部分信息效果图:
用Token首次访问授权的产品服务时,产品服务会向[认证授权服务]请求jwks的验证,以确认[认证授权服务]是此Token的发布者。
3.2 授权失败的测试
按产品服务的创建过程,再创建一个API会员服务member;
ApiResource数据源会员服务Scopes中不存在sopping;所以以上过程 Token 的 aud 只有 product/order,不存在会员服务member。
用以上原 Token 访问会员服务的测试,预期结果:授权失败。如下图:
- IdentityServer4 IdentityServer Net v4identityserver4 identityserver net v4 identityserver4 identityserver ocelot net6 identityserver4 identityserver net6 net identityserver4 identityserver策略net6 identityserver4 identityserver4 identityserver tokenrequestvalidator identityserver4 identityserver identityserver4 identityserver密码 模式 identityserver4 identityserver证书 问题 identityserver4 identityserver客户端 模式