ClaimKeyValue 模块初步 1

This commit is contained in:
FalconFly 2024-04-30 15:54:01 +08:00
parent 110db553fd
commit e06576fa46
9 changed files with 510 additions and 0 deletions

View File

@ -0,0 +1,17 @@
namespace Falcon.SugarApi.ClaimTicket
{
/// <summary>
/// 存储claim的结构
/// </summary>
public class ClaimKeyValue
{
/// <summary>
/// 存储Type
/// </summary>
public string Key { get; set; }
/// <summary>
/// 存储值
/// </summary>
public string Value { get; set; }
}
}

View File

@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Threading.Tasks;
namespace Falcon.SugarApi.ClaimTicket
{
/// <summary>
/// 模型绑定
/// </summary>
public class ClaimTicketModelBinding:IModelBinder
{
/// <summary>
/// 票据创建器
/// </summary>
public ITicketBuilder Builter { get; set; }
/// <summary>
/// 配置
/// </summary>
public ClaimTicketOptions Options { get; set; }
/// <summary>
/// 通过票据创建器创建模型绑定
/// </summary>
/// <param name="builter"></param>
/// <param name="options"></param>
public ClaimTicketModelBinding(ITicketBuilder builter,ClaimTicketOptions options) {
Builter = builter;
this.Options = options;
}
Task IModelBinder.BindModelAsync(ModelBindingContext bindingContext) {
if(bindingContext.ModelType != typeof(UserTicket)) {
return FailBind(bindingContext);
}
var token = bindingContext.ValueProvider.GetValue(this.Options.HttpHeaderKey).ToString();
if(token.IsNullOrEmpty()) {
token = bindingContext.HttpContext.Request.Headers[this.Options.HttpHeaderKey].ToString();
}
var model = this.Builter.GetUser(token);
if(model == null) {
return FailBind(bindingContext);
}
bindingContext.Model = model;
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
return Task.CompletedTask;
}
private static Task FailBind(ModelBindingContext bindingContext) {
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System;
namespace Falcon.SugarApi.ClaimTicket
{
/// <summary>
/// ClaimTicket模型绑定提供器
/// </summary>
public class ClaimTicketModelBindingProvider:IModelBinderProvider
{
IModelBinder? IModelBinderProvider.GetBinder(ModelBinderProviderContext context) {
if(context == null) {
throw new ArgumentNullException(nameof(context));
}
if(context.Metadata.ModelType == typeof(UserTicket)) {
return new BinderTypeModelBinder(typeof(ClaimTicketModelBinding));
}
return null;
}
}
}

View File

@ -0,0 +1,27 @@
using Falcon.SugarApi.Encryption;
namespace Falcon.SugarApi.ClaimTicket
{
/// <summary>
/// 票据系统配置
/// </summary>
public class ClaimTicketOptions
{
/// <summary>
/// 加密键
/// </summary>
public string EncryptionKey { get; set; } = "E9319792CB7249AD8E432000E9F2FE7A";
/// <summary>
/// http头票据key
/// </summary>
public string HttpHeaderKey { get; set; } = "_authUserTicket";
/// <summary>
/// 安全加密组件
/// </summary>
public IEncryption Encryption { get; set; }
/// <summary>
/// 序列化组件
/// </summary>
public ISerialize JsonSerialize { get; set; }
}
}

View File

@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Security.Claims;
namespace Falcon.SugarApi.ClaimTicket
{
/// <summary>
/// 票据生成接口
/// </summary>
public interface ITicketBuilder
{
/// <summary>
/// 根据用户生成票据
/// </summary>
/// <param name="userTicket">用户票据</param>
/// <returns>ticket</returns>
string? GetTicket(UserTicket userTicket);
/// <summary>
/// 通过ticket获取用户
/// </summary>
/// <param name="ticket">token</param>
/// <returns>声明组</returns>
UserTicket? GetUser(string ticket);
}
}

View File

@ -0,0 +1,180 @@
## 客户凭据 Falcon.SugarApi.ClaimTicket.ClaimTicket
### 1、添加ClaimTicket支持。
是否使用ClaimTicket由插件自行决定。
如果插件需要使用ClaimTicket按如下方式在插件服务配置中添加。
~~~C#
public class ServicePlugin:IServicePlugin
{
IServiceCollection IServicePlugin.AddServices(IServiceCollection services,IConfiguration configuration) {
//注入使用ClaimTicket的控制器
services.AddPluginsController(this.GetType().Assembly);
//添加ClaimTicket服务支持
services.AddClaimTicket();
return services;
}
}
~~~
### 2、控制器注入
要完成用户的登录认证和绑定工作需要注入ITicketBuilder接口这个接口在上面的services.AddClaimTicket()中已经完成注入。
下面是在HomeController控制器中注入ITicketBuilder的示例。
~~~C#
public ITicketBuilder TicketBuilder { get; set; }
public HomeController(IServiceProvider service) : base(service) {
this.TicketBuilder = service.GetRequiredService<ITicketBuilder>();
}
~~~
### 3、用户登录。
插件需要自行完成用户认证工作,示例代码如下。
~~~c#
[HttpGet]
public string Login(string username) {
//验证用户有效性,自行调用数据库进行验证。
//如果通过验证,生成用户票据
var user = new UserTicket(
//添加用户名
new Claim("name",username),
//添加其他必要的声明
new Claim("role","admin")
);
var ticket = this.TicketBuilder.GetTicket(user);
return ticket ?? "";
}
~~~
客户端在收到返回的票据后自行保管票据。
### 4、验证用户
实例代码如下:
~~~c#
[HttpGet]
public string ViewUser(string ticket) {
//通过票据获取用户
var cs = this.TicketBuilder.GetUser(ticket);
if(cs == null) {
return "TicketBuilder.GetUser 返回空";
}
StringBuilder sb = new();
//轮训票据生成获取登录信息
foreach(var i in cs.Claims) {
sb.AppendLine($"type:{i.Type},Val:{i.Value}");
}
return sb.ToString();
}
~~~
### 5、绑定用户
示例代码如下:
~~~c#
[HttpGet]
public string Viewbind(string _authUserTicket,[FromHeader] UserTicket user) {
StringBuilder sb = new();
foreach(var i in user.Claims) {
sb.AppendLine($"type:{i.Type},Val:{i.Value}");
}
return sb.ToString();
}
~~~
一般客户端通过HTTP头参数_authUserTicket提交用户凭据这个例子中为了方便通过get参数提交票据控制器可以自动将其绑定在UserTicket对象中。
因为action还通过httpbody获取了一个_authUserTicket参数所以UserTicket需要加[FromHeader]特性。
### 6、自定义ClaimTicket配置
通过ClaimTicketOptions对象完成配置。完成配置的途径有两个。
> 1、在插件配置中优先注入自己的ClaimTicketOptions对象。
> ~~~c#
> public class ServicePlugin:IServicePlugin
> {
> IServiceCollection IServicePlugin.AddServices(IServiceCollection services,IConfiguration configuration) {
> //注入使用ClaimTicket的控制器
> services.AddPluginsController(this.GetType().Assembly);
>
> //优先添加自己的服务配置
> services.AddSingleton(new ClaimTicketOptions());
>
> //添加ClaimTicket服务支持
> services.AddClaimTicket();
> return services;
> }
> }
> ~~~
> 2、在插件配置中优先注入自己的ClaimTicketOptions对象。
> ~~~c#
> public class ServicePlugin:IServicePlugin
> {
> IServiceCollection IServicePlugin.AddServices(IServiceCollection services,IConfiguration configuration) {
> //注入使用ClaimTicket的控制器
> services.AddPluginsController(this.GetType().Assembly);
>
> //优先添加自己的服务配置
> //services.AddSingleton(new ClaimTicketOptions());
>
> //添加ClaimTicket服务支持
> services.AddClaimTicket(optionsBuilder => {
> //客户端通过_headCalimTick属性提交凭据
> optionsBuilder.HttpHeaderKey = "_headCalimTick";
> });
> return services;
> }
> }
> ~~~
### 7、扩展ITicketBuilder
插件可以自行扩展票据的生成方式方法有两个一个是继承TicketBuilder对象并重写方法另一个就是自己实现ITicketBuilder接口。
~~~c#
/// <summary>
/// 自定义票据生成方式
/// </summary>
public class MyTicketBuilder:TicketBuilder, ITicketBuilder
{
/// <summary>
/// 使用票据参数构造生成器
/// </summary>
/// <param name="options">生成参数</param>
public MyTicketBuilder(ClaimTicketOptions options) : base(options) { }
/// <inheritdoc/>
public override string? GetTicket(UserTicket userTicket) {
//实现自己的票据生成方法
//或使用基类提供的方法
return base.GetTicket(userTicket);
}
/// <inheritdoc/>
public override UserTicket? GetUser(string ticket) {
//根据票据获取用户声明返回用户信息
//或使用基类提供的方法
return base.GetUser(ticket);
}
}
~~~
然后在调用AddClaimTicket之前注入自己的ITicketBuilder
~~~c#
public class ServicePlugin:IServicePlugin
{
IServiceCollection IServicePlugin.AddServices(IServiceCollection services,IConfiguration configuration) {
//注入使用ClaimTicket的控制器
services.AddPluginsController(this.GetType().Assembly);
//优先添加自己的服务配置
//services.AddSingleton(new ClaimTicketOptions());
//注入扩展MyTicketBuilder
services.TryAddSingleton<ITicketBuilder,MyTicketBuilder>();
//添加ClaimTicket服务支持
services.AddClaimTicket(optionsBuilder => {
//客户端通过_headCalimTick属性提交凭据
optionsBuilder.HttpHeaderKey = "_headCalimTick";
});
return services;
}
}
~~~

View File

@ -0,0 +1,43 @@
using Falcon.SugarApi.Encryption;
using Falcon.SugarApi.JsonSerialize;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Falcon.SugarApi.ClaimTicket
{
/// <summary>
/// 服务扩展
/// </summary>
public static class ServiceCollectionExtend
{
/// <summary>
/// 增加客户凭据支持
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="OptionBuilder">配置创建</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddClaimTicket(this IServiceCollection services,Action<ClaimTicketOptions>? OptionBuilder = null) {
services.TryAddSingleton<ClaimTicketOptions>(p => {
var en = p.GetRequiredService<IEncryption>();
var ser = p.GetRequiredService<IJsonSerialize>();
var option = new ClaimTicketOptions {
Encryption = en,JsonSerialize = ser,
};
OptionBuilder?.Invoke(option);
return option;
});
services.TryAddSingleton<IEncryption,AESProvider>();
services.TryAddSingleton<IJsonSerialize,JsonSerialize.JsonSerialize>();
services.TryAddSingleton<ITicketBuilder,TicketBuilder>();
services.AddControllers(op => {
op.ModelBinderProviders.Insert(0,new ClaimTicketModelBindingProvider());
});
return services;
}
}
}

View File

@ -0,0 +1,63 @@
using Falcon.SugarApi.Encryption;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
namespace Falcon.SugarApi.ClaimTicket
{
/// <summary>
/// 票据生成器
/// </summary>
public class TicketBuilder:ITicketBuilder
{
/// <summary>
/// 构造票据生成器
/// </summary>
/// <param name="options">加密配置</param>
public TicketBuilder(ClaimTicketOptions options) {
this.Encryption = options.Encryption;
this.Serialize = options.JsonSerialize;
this.Options = options;
}
/// <summary>
/// 加密模块
/// </summary>
public IEncryption Encryption { get; }
/// <summary>
/// 序列化模块
/// </summary>
public ISerialize Serialize { get; }
/// <summary>
/// 票据生成配置
/// </summary>
public ClaimTicketOptions Options { get; }
/// <inheritdoc/>
public virtual UserTicket? GetUser(string ticket) {
var str = this.Encryption.Decrypt(this.Options.EncryptionKey,ticket);
if(str.IsNullOrEmpty()) {
return new UserTicket();
}
var list = this.Serialize.Deserialize<List<ClaimKeyValue>>(str);
if(list == null) {
return new UserTicket();
}
return new UserTicket(list.Select(a => new Claim(a.Key,a.Value)));
}
/// <inheritdoc/>
public virtual string? GetTicket(UserTicket userTicket) {
if(userTicket == null) {
return string.Empty;
}
var claims = userTicket.Claims;
if(claims == null || claims.Count == 0) {
return string.Empty;
}
var list = claims.Select(a => new ClaimKeyValue { Key = a.Type,Value = a.Value });
var str = this.Serialize.Serialize(list);
var sstr = this.Encryption.Encrypt(this.Options.EncryptionKey,str);
return sstr;
}
}
}

View File

@ -0,0 +1,77 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
namespace Falcon.SugarApi.ClaimTicket
{
/// <summary>
/// 用户票据
/// </summary>
public class UserTicket
{
/// <summary>
/// 存储用户声明
/// </summary>
public List<Claim> Claims { get; set; } = new List<Claim>();
/// <summary>
/// 构造空白票据
/// </summary>
public UserTicket() { }
/// <summary>
/// 通过声明列表构造票据
/// </summary>
/// <param name="claims">声称</param>
public UserTicket(IEnumerable<Claim> claims) {
this.Claims = claims.ToList();
}
/// <summary>
/// 通过声明列表构造票据
/// </summary>
/// <param name="claims">声称</param>
public UserTicket(params Claim[] claims) : this(claims.ToList()) { }
/// <summary>
/// 增加用户声明
/// </summary>
/// <param name="claims"></param>
public void AddClaims(params Claim[] claims) {
if(claims.Length == 0) {
return;
}
foreach(var c in claims) {
var f = this.Claims.Find(a => a.Type == c.Type);
if(f != null) {
this.Claims.Remove(f);
}
this.Claims.Add(c);
}
}
/// <summary>
/// 为用户添加键和值
/// </summary>
/// <param name="key">键</param>
/// <param name="val">值</param>
public void AddClaim(string key,string val) {
if(key.IsNullOrEmpty()) {
return;
}
if(val.IsNullOrEmpty()) {
var c = this.Claims.Find(a => a.Type == key);
if(c != null) {
this.Claims.Remove(c);
}
}
this.AddClaims(new Claim(key,val));
}
/// <summary>
/// 获取用户声明的值
/// </summary>
/// <param name="key">声明键</param>
/// <returns>值</returns>
public string? GetValue(string key) {
var c = this.Claims.Find(a => a.Type == key);
return c == null ? string.Empty : c.Value;
}
}
}