Skip to content

add DynamicTokenProvider. #239

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using System.Linq;
using System;
using System.Net;
using System.Net.Http.Headers;
using System.Threading.Tasks;
Expand All @@ -9,7 +11,7 @@ namespace WebApiClientCore.Attributes
/// <summary>
/// 表示token应用特性
/// 需要为接口或接口的基础接口注册TokenProvider
/// </summary>
/// </summary>
/// <remarks>
/// <para>• Client模式:services.AddClientCredentialsTokenProvider</para>
/// <para>• Password模式:services.AddPasswordCredentialsTokenProvider</para>
Expand All @@ -21,14 +23,21 @@ public class OAuthTokenAttribute : ApiFilterAttribute
/// </summary>
public TypeMatchMode TokenProviderSearchMode { get; set; } = TypeMatchMode.TypeOrBaseTypes;

private static string GetDynamicTokenKey(ApiRequestContext context)
{
context.Properties.TryGetValue(typeof(OAuthTokenAttribute), out string? key);
return key ?? string.Empty;
}

/// <summary>
/// 请求之前
/// </summary>
/// <param name="context">上下文</param>
/// <returns></returns>
public sealed override async Task OnRequestAsync(ApiRequestContext context)
{
var token = await this.GetTokenProvider(context).GetTokenAsync().ConfigureAwait(false);
var key = GetDynamicTokenKey(context);
var token = await this.GetTokenProvider(context).GetTokenAsync(key).ConfigureAwait(false);
this.UseTokenResult(context, token);
}

Expand All @@ -41,7 +50,8 @@ public sealed override Task OnResponseAsync(ApiResponseContext context)
{
if (this.IsUnauthorized(context) == true)
{
this.GetTokenProvider(context).ClearToken();
var key = GetDynamicTokenKey(context);
this.GetTokenProvider(context).ClearToken(key);
}
return Task.CompletedTask;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Threading.Tasks;

namespace WebApiClientCore.Attributes
{
/// <summary>
/// 动态应用标识
/// </summary>
public class OAuthTokenKeyAttribute : ApiParameterAttribute
{
/// <summary>
/// http请求之前
/// </summary>
public override Task OnRequestAsync(ApiParameterContext context)
{
context.Properties.Set(typeof(OAuthTokenAttribute), context.ParameterValue);
return Task.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace WebApiClientCore.Extensions.OAuths.Exceptions
{
/// <summary>
/// 表示Token 应用标识异常
/// </summary>
public class TokenIdentifierException : Exception
{
/// <summary>
/// Token异常
/// </summary>
/// <param name="message">消息</param>
public TokenIdentifierException(string? message)
: base(message)
{
}

/// <summary>
/// 表示Token 应用标识异常
/// </summary>
public TokenIdentifierException()
{
}

/// <summary>
/// 表示Token 应用标识异常
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference (<see langword="Nothing" /> in Visual Basic) if no inner exception is specified.</param>
public TokenIdentifierException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
14 changes: 13 additions & 1 deletion WebApiClientCore.Extensions.OAuths/ITokenProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,20 @@ public interface ITokenProvider

/// <summary>
/// 获取token信息
/// </summary>
/// </summary>
/// <returns></returns>
Task<TokenResult> GetTokenAsync();

/// <summary>
/// 强制清除token以支持下次获取到新的token
/// </summary>
/// <param name="key">应用标识</param>
void ClearToken(string key);

/// <summary>
/// 根据应用标识获取token信息
/// </summary>
/// <param name="key">应用标识</param>
Task<TokenResult> GetTokenAsync(string key);
}
}
85 changes: 58 additions & 27 deletions WebApiClientCore.Extensions.OAuths/TokenProviders/TokenProvider.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using WebApiClientCore.Extensions.OAuths.Exceptions;

Expand All @@ -11,21 +12,23 @@ namespace WebApiClientCore.Extensions.OAuths.TokenProviders
/// </summary>
public abstract class TokenProvider : ITokenProvider
{
private class TokenItem
{
public AsyncRoot AsyncRoot { get; } = new AsyncRoot();
public TokenResult? TokenResult { get; set; }
}

/// <summary>
/// 最近请求到的token
/// token缓存
/// </summary>
private TokenResult? token;
private static readonly ConcurrentDictionary<string, Lazy<TokenItem?>> keyTokens
= new ConcurrentDictionary<string, Lazy<TokenItem?>>();

/// <summary>
/// 服务提供者
/// </summary>
private readonly IServiceProvider services;

/// <summary>
/// 异步锁
/// </summary>
private readonly AsyncRoot asyncRoot = new AsyncRoot();

/// <summary>
/// 获取或设置别名
/// </summary>
Expand Down Expand Up @@ -56,53 +59,81 @@ public TOptions GetOptionsValue<TOptions>()
/// </summary>
public void ClearToken()
{
using (this.asyncRoot.Lock())
{
this.token = null;
}
ClearToken(string.Empty);
}
/// <summary>
/// 强制清除token以支持下次获取到新的token
/// </summary>
/// <param name="key">应用标识</param>
public void ClearToken(string key)
{
keyTokens.TryRemove(key, out _);
}

/// <summary>
/// 获取token信息
/// </summary>
/// </summary>
/// <returns></returns>
public async Task<TokenResult> GetTokenAsync()
{
using (await this.asyncRoot.LockAsync().ConfigureAwait(false))
return await GetTokenAsync(string.Empty);
}

/// <summary>
/// 获取token信息
/// </summary>
/// <param name="key">应用标识</param>
public async Task<TokenResult> GetTokenAsync(string key)
{
var tokenItem = keyTokens.GetOrAdd(key, _ => new Lazy<TokenItem?>(() => new TokenItem())).Value!;

using (await tokenItem.AsyncRoot.LockAsync().ConfigureAwait(false))
{
if (this.token == null)
if (tokenItem.TokenResult == null)
{
using var scope = this.services.CreateScope();
this.token = await this.RequestTokenAsync(scope.ServiceProvider).ConfigureAwait(false);
using var scope = services.CreateScope();
tokenItem.TokenResult = await RequestTokenAsync(scope.ServiceProvider, key).ConfigureAwait(false);
}
else if (this.token.IsExpired() == true)
else if (tokenItem.TokenResult.IsExpired())
{
using var scope = this.services.CreateScope();

this.token = this.token.CanRefresh() == false
? await this.RequestTokenAsync(scope.ServiceProvider).ConfigureAwait(false)
: await this.RefreshTokenAsync(scope.ServiceProvider, this.token.Refresh_token ?? string.Empty).ConfigureAwait(false);
using var scope = services.CreateScope();
tokenItem.TokenResult = tokenItem.TokenResult.CanRefresh()
? await RefreshTokenAsync(scope.ServiceProvider, tokenItem.TokenResult.Refresh_token!).ConfigureAwait(false)
: await RequestTokenAsync(scope.ServiceProvider, key).ConfigureAwait(false);
}

if (this.token == null)
if (tokenItem.TokenResult == null)
{
throw new TokenNullException();
}
return this.token.EnsureSuccess();

return tokenItem.TokenResult.EnsureSuccess();
}
}


/// <summary>
/// 请求获取token
/// </summary>
/// </summary>
/// <param name="serviceProvider">服务提供者</param>
/// <returns></returns>
protected abstract Task<TokenResult?> RequestTokenAsync(IServiceProvider serviceProvider);

/// <summary>
/// 请求获取token
/// </summary>
/// <param name="serviceProvider">服务提供者</param>
/// <param name="key">应用标识</param>
protected virtual Task<TokenResult?> RequestTokenAsync(IServiceProvider serviceProvider, string? key)
{
if (string.IsNullOrEmpty(key))
return RequestTokenAsync(serviceProvider);

throw new NotImplementedException();
}

/// <summary>
/// 刷新token
/// </summary>
/// </summary>
/// <param name="serviceProvider">服务提供者</param>
/// <param name="refresh_token">刷新token</param>
/// <returns></returns>
Expand Down
18 changes: 14 additions & 4 deletions WebApiClientCore.Extensions.OAuths/TokenResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class TokenResult

/// <summary>
/// expires_in
/// 过期时间戳(秒)
/// access_token 过期时间戳(秒)
/// </summary>
[JsonPropertyName("expires_in")]
public long Expires_in { get; set; }
Expand All @@ -45,6 +45,13 @@ public class TokenResult
[JsonPropertyName("refresh_token")]
public string? Refresh_token { get; set; }

/// <summary>
/// refresh_expires_in
/// refresh_token 过期时间戳(秒)
/// </summary>
[JsonPropertyName("refresh_expires_in")]
public long? Refresh_expires_in { get; set; }

/// <summary>
/// error
/// </summary>
Expand Down Expand Up @@ -74,7 +81,7 @@ public virtual bool IsSuccess()
}

/// <summary>
/// 返回是否已过期
/// 返回是否已过期
/// </summary>
/// <returns></returns>
public virtual bool IsExpired()
Expand All @@ -83,12 +90,15 @@ public virtual bool IsExpired()
}

/// <summary>
/// 返回token是否支持刷新
/// 返回token是否支持刷新,需要同时满足:refresh_token有值,refresh_expires_in无值,若refresh_expires_in有值 必须当前没有过期。
/// </summary>
/// <returns></returns>
public virtual bool CanRefresh()
{
return string.IsNullOrEmpty(this.Refresh_token) == false;
return string.IsNullOrEmpty(this.Refresh_token) == false
&& (!this.Refresh_expires_in.HasValue
|| (this.Refresh_expires_in.HasValue
&& DateTime.Now.Subtract(this.createTime) < TimeSpan.FromSeconds(this.Refresh_expires_in.Value)));
}
}
}
18 changes: 18 additions & 0 deletions WebApiClientCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiClientCore.Extensions
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApiClientCore.Analyzers.SourceGenerator", "WebApiClientCore.Analyzers.SourceGenerator\WebApiClientCore.Analyzers.SourceGenerator.csproj", "{DFE35D63-0888-4EE2-A9F1-AC45756F5909}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "demos", "demos", "{8EC3018E-3F42-4ED6-ADE5-435F1D2D609D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApplication1", "demos\WebApplication1\WebApplication1.csproj", "{A92F8FAC-6680-4615-B865-193C23BB1500}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApplication2", "demos\WebApplication2\WebApplication2.csproj", "{67DB4476-D27D-4480-8AAF-AF75BE63B729}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -81,10 +87,22 @@ Global
{DFE35D63-0888-4EE2-A9F1-AC45756F5909}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DFE35D63-0888-4EE2-A9F1-AC45756F5909}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DFE35D63-0888-4EE2-A9F1-AC45756F5909}.Release|Any CPU.Build.0 = Release|Any CPU
{A92F8FAC-6680-4615-B865-193C23BB1500}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A92F8FAC-6680-4615-B865-193C23BB1500}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A92F8FAC-6680-4615-B865-193C23BB1500}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A92F8FAC-6680-4615-B865-193C23BB1500}.Release|Any CPU.Build.0 = Release|Any CPU
{67DB4476-D27D-4480-8AAF-AF75BE63B729}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{67DB4476-D27D-4480-8AAF-AF75BE63B729}.Debug|Any CPU.Build.0 = Debug|Any CPU
{67DB4476-D27D-4480-8AAF-AF75BE63B729}.Release|Any CPU.ActiveCfg = Release|Any CPU
{67DB4476-D27D-4480-8AAF-AF75BE63B729}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{A92F8FAC-6680-4615-B865-193C23BB1500} = {8EC3018E-3F42-4ED6-ADE5-435F1D2D609D}
{67DB4476-D27D-4480-8AAF-AF75BE63B729} = {8EC3018E-3F42-4ED6-ADE5-435F1D2D609D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {915190C5-E5CB-440F-84B4-AE76368EA776}
EndGlobalSection
Expand Down
Loading