Skip to content

Commit e855043

Browse files
committed
feat: 新增帳號停用快取機制,提升效能並減少資料庫查詢
1 parent 68ca24b commit e855043

6 files changed

Lines changed: 92 additions & 14 deletions

File tree

src/Netcorext.Auth.Authorization/InjectionConfigs/DbConfig.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Netcorext.EntityFramework.UserIdentityPattern.AspNetCore;
66
using Netcorext.Extensions.Redis.Utilities;
77
using Netcorext.Serialization;
8+
using Microsoft.Extensions.Caching.Memory;
89

910
namespace Netcorext.Auth.Authorization.InjectionConfigs;
1011

@@ -65,5 +66,11 @@ public DbConfig(IServiceCollection services, IConfiguration configuration)
6566
DeserializeRaw = serializer.Deserialize
6667
}).Client;
6768
});
69+
70+
services.AddSingleton<MemoryCacheEntryOptions>(_ => new MemoryCacheEntryOptions
71+
{
72+
Priority = CacheItemPriority.NeverRemove
73+
});
74+
services.AddMemoryCache();
6875
}
6976
}

src/Netcorext.Auth.Authorization/Services/Token/Commands/CreateToken/CreateTokenHandler.cs

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Security.Claims;
22
using FreeRedis;
33
using Microsoft.EntityFrameworkCore;
4+
using Microsoft.Extensions.Caching.Memory;
45
using Microsoft.Extensions.Options;
56
using Netcorext.Algorithms;
67
using Netcorext.Auth.Authorization.Models;
@@ -24,17 +25,19 @@ public class CreateTokenHandler : IRequestHandler<CreateToken, Result<TokenResul
2425
{
2526
private readonly IDispatcher _dispatcher;
2627
private readonly DatabaseContext _context;
28+
private readonly IMemoryCache _cache;
2729
private readonly ISnowflake _snowflake;
2830
private readonly JwtGenerator _jwtGenerator;
2931
private readonly ISerializer _serializer;
3032
private readonly RedisClient _redis;
3133
private readonly ConfigSettings _config;
3234
private readonly AuthOptions _authOptions;
3335

34-
public CreateTokenHandler(IDispatcher dispatcher, DatabaseContextAdapter context, ISnowflake snowflake, JwtGenerator jwtGenerator, RedisClient redis, ISerializer serializer, IOptions<AuthOptions> authOptions, IOptions<ConfigSettings> config)
36+
public CreateTokenHandler(IDispatcher dispatcher, DatabaseContextAdapter context, IMemoryCache cache, ISnowflake snowflake, JwtGenerator jwtGenerator, RedisClient redis, ISerializer serializer, IOptions<AuthOptions> authOptions, IOptions<ConfigSettings> config)
3537
{
3638
_dispatcher = dispatcher;
3739
_context = context;
40+
_cache = cache;
3841
_snowflake = snowflake;
3942
_jwtGenerator = jwtGenerator;
4043
_serializer = serializer;
@@ -74,6 +77,13 @@ private async Task<Result<TokenResult>> CreateClientCredentialsAsync(CreateToken
7477
ErrorDescription = Constants.OAuth.INVALID_REQUEST_ID_OR_SECRET_MESSAGE
7578
});
7679

80+
if (_cache.Get<bool>(ConfigSettings.CACHE_RESOURCE_DISABLED + ":" + clientId))
81+
return Result<TokenResult>.AccountIsDisabled.Clone(new TokenResult
82+
{
83+
Error = Constants.OAuth.ACCESS_DENIED,
84+
ErrorDescription = Constants.OAuth.ACCESS_DENIED_MESSAGE
85+
});
86+
7787
var dsClient = _context.Set<Domain.Entities.Client>();
7888

7989
var client = await dsClient.Include(t => t.Roles)
@@ -88,11 +98,17 @@ private async Task<Result<TokenResult>> CreateClientCredentialsAsync(CreateToken
8898
});
8999

90100
if (client.Disabled)
101+
{
102+
_cache.Set(ConfigSettings.CACHE_RESOURCE_DISABLED + ":" + client.Id, true, TimeSpan.FromMilliseconds(_config.AppSettings.CacheResourceDisabledExpires));
103+
91104
return Result<TokenResult>.AccountIsDisabled.Clone(new TokenResult
92105
{
93106
Error = Constants.OAuth.ACCESS_DENIED,
94107
ErrorDescription = Constants.OAuth.ACCESS_DENIED_MESSAGE
95108
});
109+
}
110+
111+
96112

97113
var secret = request.ClientSecret!.Pbkdf2HashCode(client.CreationDate.ToUnixTimeMilliseconds());
98114

@@ -195,13 +211,27 @@ private async Task<Result<TokenResult>> CreatePasswordCredentialsAsync(CreateTok
195211
ErrorDescription = Constants.OAuth.UNAUTHORIZED_CLIENT_MESSAGE
196212
});
197213

214+
if (_cache.Get<bool>(ConfigSettings.CACHE_RESOURCE_DISABLED + ":" + clientId))
215+
return Result<TokenResult>.AccountIsDisabled.Clone(new TokenResult
216+
{
217+
Error = Constants.OAuth.ACCESS_DENIED,
218+
ErrorDescription = Constants.OAuth.ACCESS_DENIED_MESSAGE
219+
});
220+
198221
if (request.Username.IsEmpty() || request.Password.IsEmpty())
199222
return Result<TokenResult>.InvalidInput.Clone(new TokenResult
200223
{
201224
Error = Constants.OAuth.INVALID_REQUEST,
202225
ErrorDescription = Constants.OAuth.INVALID_REQUEST_USERNAME_OR_PASSWORD_MESSAGE
203226
});
204227

228+
if (_cache.Get<bool>(ConfigSettings.CACHE_RESOURCE_DISABLED + ":" + request.Username!.ToUpper()))
229+
return Result<TokenResult>.AccountIsDisabled.Clone(new TokenResult
230+
{
231+
Error = Constants.OAuth.ACCESS_DENIED,
232+
ErrorDescription = Constants.OAuth.ACCESS_DENIED_MESSAGE
233+
});
234+
205235
var dsClient = _context.Set<Domain.Entities.Client>();
206236

207237
var client = await dsClient.FirstOrDefaultAsync(t => t.Id == clientId, cancellationToken);
@@ -214,11 +244,15 @@ private async Task<Result<TokenResult>> CreatePasswordCredentialsAsync(CreateTok
214244
});
215245

216246
if (client.Disabled)
217-
return Result<TokenResult>.Forbidden.Clone(new TokenResult
218-
{
219-
Error = Constants.OAuth.ACCESS_DENIED,
220-
ErrorDescription = Constants.OAuth.ACCESS_DENIED_MESSAGE
221-
});
247+
{
248+
_cache.Set(ConfigSettings.CACHE_RESOURCE_DISABLED + ":" + client.Id, true, TimeSpan.FromMilliseconds(_config.AppSettings.CacheResourceDisabledExpires));
249+
250+
return Result<TokenResult>.AccountIsDisabled.Clone(new TokenResult
251+
{
252+
Error = Constants.OAuth.ACCESS_DENIED,
253+
ErrorDescription = Constants.OAuth.ACCESS_DENIED_MESSAGE
254+
});
255+
}
222256

223257
var secret = request.ClientSecret!.Pbkdf2HashCode(client.CreationDate.ToUnixTimeMilliseconds());
224258

@@ -243,11 +277,16 @@ private async Task<Result<TokenResult>> CreatePasswordCredentialsAsync(CreateTok
243277
});
244278

245279
if (user.Disabled)
280+
{
281+
_cache.Set(ConfigSettings.CACHE_RESOURCE_DISABLED + ":" + user.Id, true, TimeSpan.FromMilliseconds(_config.AppSettings.CacheResourceDisabledExpires));
282+
_cache.Set(ConfigSettings.CACHE_RESOURCE_DISABLED + ":" + user.NormalizedUsername, true, TimeSpan.FromMilliseconds(_config.AppSettings.CacheResourceDisabledExpires));
283+
246284
return Result<TokenResult>.AccountIsDisabled.Clone(new TokenResult
247285
{
248286
Error = Constants.OAuth.ACCESS_DENIED,
249287
ErrorDescription = Constants.OAuth.ACCESS_DENIED_MESSAGE
250288
});
289+
}
251290

252291
var password = request.Password!.Pbkdf2HashCode(user.CreationDate.ToUnixTimeMilliseconds());
253292

@@ -422,11 +461,15 @@ private async Task<Result<TokenResult>> CreateRefreshTokenAsync(CreateToken requ
422461
var client = await dsClient.FirstAsync(t => t.Id == clientId, cancellationToken);
423462

424463
if (client.Disabled)
464+
{
465+
_cache.Set(ConfigSettings.CACHE_RESOURCE_DISABLED + ":" + client.Id, true, TimeSpan.FromMilliseconds(_config.AppSettings.CacheResourceDisabledExpires));
466+
425467
return Result<TokenResult>.Forbidden.Clone(new TokenResult
426468
{
427469
Error = Constants.OAuth.ACCESS_DENIED,
428470
ErrorDescription = Constants.OAuth.ACCESS_DENIED_MESSAGE
429471
});
472+
}
430473

431474
var secret = request.ClientSecret!.Pbkdf2HashCode(client.CreationDate.ToUnixTimeMilliseconds());
432475

@@ -491,14 +534,21 @@ private async Task<Result<TokenResult>> CreateRefreshTokenAsync(CreateToken requ
491534

492535
try
493536
{
494-
var (verified, disabled, roles, hasPassword, emailConfirmed, phoneNumberConfirmed, label, allowedRefreshToken, tokenExpireSeconds, refreshTokenExpireSeconds, _) = await GetResourceExpireSecondsAsync(resourceType, resourceId!);
537+
var (resourceName, verified, disabled, roles, hasPassword, emailConfirmed, phoneNumberConfirmed, label, allowedRefreshToken, tokenExpireSeconds, refreshTokenExpireSeconds, _) = await GetResourceExpireSecondsAsync(resourceType, resourceId!);
495538

496539
if (disabled)
540+
{
541+
_cache.Set(ConfigSettings.CACHE_RESOURCE_DISABLED + ":" + resourceId, TimeSpan.FromMilliseconds(_config.AppSettings.CacheResourceDisabledExpires));
542+
_cache.Set(ConfigSettings.CACHE_RESOURCE_DISABLED + ":" + resourceName.ToUpper(), TimeSpan.FromMilliseconds(_config.AppSettings.CacheResourceDisabledExpires));
543+
497544
return Result<TokenResult>.Forbidden.Clone(new TokenResult
498545
{
499546
Error = Constants.OAuth.ACCESS_DENIED,
500547
ErrorDescription = Constants.OAuth.ACCESS_DENIED_MESSAGE
501548
});
549+
}
550+
551+
502552

503553
if (request.Scope == "*")
504554
scope = roles.Any() ? roles.Select(t => t.Id.ToString()).Aggregate((c, n) => c + " " + n) : null;
@@ -607,7 +657,7 @@ private async Task<Result<TokenResult>> CreateRefreshTokenAsync(CreateToken requ
607657
}
608658
}
609659

610-
private async Task<(bool Verified, bool Disabled, Role[] Roles, bool? HasPassword, bool? EmailConfirmed, bool? PhoneNumberConfirmed, string? Label, bool AllowedRefreshToken, int? TokenExpireSeconds, int? RefreshTokenExpireSeconds, int? CodeExpireSeconds)> GetResourceExpireSecondsAsync(ResourceType resourceType, string resourceId)
660+
private async Task<(string nameOrId, bool Verified, bool Disabled, Role[] Roles, bool? HasPassword, bool? EmailConfirmed, bool? PhoneNumberConfirmed, string? Label, bool AllowedRefreshToken, int? TokenExpireSeconds, int? RefreshTokenExpireSeconds, int? CodeExpireSeconds)> GetResourceExpireSecondsAsync(ResourceType resourceType, string resourceId)
611661
{
612662
return resourceType switch
613663
{
@@ -617,7 +667,7 @@ private async Task<Result<TokenResult>> CreateRefreshTokenAsync(CreateToken requ
617667
};
618668
}
619669

620-
private async Task<(bool Verified, bool Disabled, Role[] Roles, bool? HasPassword, bool? EmailConfirmed, bool? PhoneNumberConfirmed, string? Label, bool AllowedRefreshToken, int? TokenExpireSeconds, int? RefreshTokenExpireSeconds, int? CodeExpireSeconds)> GetUserExpireSecondsAsync(string resourceId)
670+
private async Task<(string username, bool Verified, bool Disabled, Role[] Roles, bool? HasPassword, bool? EmailConfirmed, bool? PhoneNumberConfirmed, string? Label, bool AllowedRefreshToken, int? TokenExpireSeconds, int? RefreshTokenExpireSeconds, int? CodeExpireSeconds)> GetUserExpireSecondsAsync(string resourceId)
621671
{
622672
if (resourceId.IsEmpty() || !long.TryParse(resourceId, out var id)) throw new ArgumentException($"Invalid {nameof(resourceId)}.");
623673

@@ -647,10 +697,10 @@ private async Task<Result<TokenResult>> CreateRefreshTokenAsync(CreateToken requ
647697
? roles[0].Name
648698
: null;
649699

650-
return (entity.Verified, entity.Disabled, roles, !entity.Password.IsEmpty(), entity.EmailConfirmed, entity.PhoneNumberConfirmed, label, entity.AllowedRefreshToken, entity.TokenExpireSeconds, entity.RefreshTokenExpireSeconds, entity.CodeExpireSeconds);
700+
return (entity.Username, entity.Verified, entity.Disabled, roles, !entity.Password.IsEmpty(), entity.EmailConfirmed, entity.PhoneNumberConfirmed, label, entity.AllowedRefreshToken, entity.TokenExpireSeconds, entity.RefreshTokenExpireSeconds, entity.CodeExpireSeconds);
651701
}
652702

653-
private async Task<(bool Verified, bool Disabled, Role[] Roles, bool? HasPassword, bool? EmailConfirmed, bool? PhoneNumberConfirmed, string? Label, bool AllowedRefreshToken, int? TokenExpireSeconds, int? RefreshTokenExpireSeconds, int? CodeExpireSeconds)> GetClientExpireSecondsAsync(string resourceId)
703+
private async Task<(string id, bool Verified, bool Disabled, Role[] Roles, bool? HasPassword, bool? EmailConfirmed, bool? PhoneNumberConfirmed, string? Label, bool AllowedRefreshToken, int? TokenExpireSeconds, int? RefreshTokenExpireSeconds, int? CodeExpireSeconds)> GetClientExpireSecondsAsync(string resourceId)
654704
{
655705
if (resourceId.IsEmpty() || !long.TryParse(resourceId, out var id)) throw new ArgumentException($"Invalid {nameof(resourceId)}.");
656706

@@ -680,7 +730,7 @@ private async Task<Result<TokenResult>> CreateRefreshTokenAsync(CreateToken requ
680730
? roles[0].Name
681731
: null;
682732

683-
return (false, entity.Disabled, roles, null, null, null, label, entity.AllowedRefreshToken, entity.TokenExpireSeconds, entity.RefreshTokenExpireSeconds, entity.CodeExpireSeconds);
733+
return (entity.Id.ToString(), false, entity.Disabled, roles, null, null, null, label, entity.AllowedRefreshToken, entity.TokenExpireSeconds, entity.RefreshTokenExpireSeconds, entity.CodeExpireSeconds);
684734
}
685735

686736
private Task<bool> IsValidAsync(string grantType)

src/Netcorext.Auth.Authorization/Services/User/Commands/ExternalSignIn/ExternalSignInHandler.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using FreeRedis;
22
using Microsoft.EntityFrameworkCore;
3+
using Microsoft.Extensions.Caching.Memory;
34
using Microsoft.Extensions.Options;
45
using Netcorext.Algorithms;
56
using Netcorext.Auth.Authorization.Models;
@@ -21,17 +22,19 @@ public class ExternalSignInHandler : IRequestHandler<ExternalSignIn, Result<Toke
2122
{
2223
private readonly IDispatcher _dispatcher;
2324
private readonly DatabaseContext _context;
25+
private readonly IMemoryCache _cache;
2426
private readonly RedisClient _redis;
2527
private readonly IHttpContextAccessor _httpContextAccessor;
2628
private readonly ISnowflake _snowflake;
2729
private readonly JwtGenerator _jwtGenerator;
2830
private readonly ConfigSettings _config;
2931
private readonly AuthOptions _authOptions;
3032

31-
public ExternalSignInHandler(IDispatcher dispatcher, DatabaseContextAdapter context, RedisClient redis, IHttpContextAccessor httpContextAccessor, ISnowflake snowflake, JwtGenerator jwtGenerator, IOptions<ConfigSettings> config, IOptions<AuthOptions> authOptions)
33+
public ExternalSignInHandler(IDispatcher dispatcher, DatabaseContextAdapter context, IMemoryCache cache, RedisClient redis, IHttpContextAccessor httpContextAccessor, ISnowflake snowflake, JwtGenerator jwtGenerator, IOptions<ConfigSettings> config, IOptions<AuthOptions> authOptions)
3234
{
3335
_dispatcher = dispatcher;
3436
_context = context;
37+
_cache = cache;
3538
_redis = redis;
3639
_httpContextAccessor = httpContextAccessor;
3740
_snowflake = snowflake;
@@ -47,6 +50,9 @@ public async Task<Result<TokenResult>> Handle(ExternalSignIn request, Cancellati
4750
var username = request.Username;
4851
var creationDate = DateTimeOffset.UtcNow;
4952

53+
if (_cache.Get<bool>(ConfigSettings.CACHE_RESOURCE_DISABLED + ":" + request.Username) || _cache.Get<bool>(ConfigSettings.CACHE_RESOURCE_DISABLED + ":" + request.UniqueId))
54+
return Result<TokenResult>.AccountIsDisabled;
55+
5056
var entity = await dsUser.Include(t => t.Roles)
5157
.ThenInclude(t => t.Role)
5258
.FirstOrDefaultAsync(t => t.NormalizedUsername == username.ToUpper(), cancellationToken);
@@ -60,6 +66,9 @@ public async Task<Result<TokenResult>> Handle(ExternalSignIn request, Cancellati
6066
{
6167
if (entity.Disabled)
6268
{
69+
_cache.Set(ConfigSettings.CACHE_RESOURCE_DISABLED + ":" + entity.Id, true, TimeSpan.FromMilliseconds(_config.AppSettings.CacheResourceDisabledExpires));
70+
_cache.Set(ConfigSettings.CACHE_RESOURCE_DISABLED + ":" + entity.NormalizedUsername, true, TimeSpan.FromMilliseconds(_config.AppSettings.CacheResourceDisabledExpires));
71+
6372
await SetSignInFailureStateAsync(entity, cancellationToken);
6473

6574
return Result<TokenResult>.AccountIsDisabled;

0 commit comments

Comments
 (0)