更新 JWT
This commit is contained in:
1 parent
f84c4c1949
commit
8526e5e0d2
10 files changed
+40
-63
No files matched your search
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning.OpenApi;
|
||||
using Asp.Versioning.OpenApi;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -68,12 +68,12 @@ public static class JwtServiceCollectionExtensions
|
||||
{
|
||||
IOnlineUserStore onlineUserStore = context.HttpContext.RequestServices.GetRequiredService<IOnlineUserStore>();
|
||||
ClaimsPrincipal? principal = context.Principal;
|
||||
string? userIdValue = principal?.FindFirst(JwtClaimTypes.UserId)?.Value;
|
||||
string? userName = principal?.FindFirst(JwtClaimTypes.UserName)?.Value;
|
||||
string? jti = principal?.FindFirst(JwtClaimTypes.Jti)?.Value;
|
||||
|
||||
if (!int.TryParse(userIdValue, out int userId)
|
||||
if (string.IsNullOrWhiteSpace(userName)
|
||||
|| string.IsNullOrWhiteSpace(jti)
|
||||
|| !onlineUserStore.IsTokenActive(userId, jti))
|
||||
|| !onlineUserStore.IsTokenActive(userName, jti))
|
||||
{
|
||||
context.Fail(new SecurityTokenException("登录已失效,请重新登录"));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
|
||||
@@ -24,16 +24,6 @@ public class CurrentUser : ICurrentUser
|
||||
/// <inheritdoc />
|
||||
public bool IsAuthenticated => Principal?.Identity?.IsAuthenticated == true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int? Id
|
||||
{
|
||||
get
|
||||
{
|
||||
string? value = GetClaimValue(JwtClaimTypes.UserId);
|
||||
return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int id) ? id : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? UserName => GetClaimValue(JwtClaimTypes.UserName);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Security.Claims;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace StoreHub.Framework.Security;
|
||||
|
||||
@@ -12,11 +12,6 @@ public interface ICurrentUser
|
||||
/// </summary>
|
||||
bool IsAuthenticated { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前用户ID(未登录为 null)
|
||||
/// </summary>
|
||||
int? Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前登录账户名(未登录为 null)
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace StoreHub.Framework.Security;
|
||||
namespace StoreHub.Framework.Security;
|
||||
|
||||
/// <summary>
|
||||
/// 在线用户会话存储:基于 <see cref="Microsoft.Extensions.Caching.Memory.IMemoryCache"/> 的进程内实现,
|
||||
@@ -12,14 +12,14 @@ public interface IOnlineUserStore
|
||||
void SignIn(OnlineUserInfo info);
|
||||
|
||||
/// <summary>
|
||||
/// 判断指定用户、指定令牌会话是否仍然有效(未被退出、未被新登录顶替)。
|
||||
/// 判断指定登录账户、指定令牌会话是否仍然有效(未被退出、未被新登录顶替)。
|
||||
/// </summary>
|
||||
bool IsTokenActive(int userId, string jti);
|
||||
bool IsTokenActive(string userName, string jti);
|
||||
|
||||
/// <summary>
|
||||
/// 退出登录:销毁该用户的在线会话,令牌立即失效。
|
||||
/// 退出登录:销毁该账户的在线会话,令牌立即失效。
|
||||
/// </summary>
|
||||
void SignOut(int userId);
|
||||
void SignOut(string userName);
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前在线人员列表。
|
||||
|
||||
@@ -18,9 +18,9 @@ public class TokenResult
|
||||
public string Token { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 过期时间(本地时间)
|
||||
/// 有效时间(分钟)
|
||||
/// </summary>
|
||||
public DateTime ExpiresAt { get; set; }
|
||||
public uint ExpiresInMinutes { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
namespace StoreHub.Framework.Security;
|
||||
namespace StoreHub.Framework.Security;
|
||||
|
||||
/// <summary>
|
||||
/// JWT 自定义声明类型常量。
|
||||
/// </summary>
|
||||
public static class JwtClaimTypes
|
||||
{
|
||||
public const string UserId = "user_name";
|
||||
|
||||
/// <summary>登录账户</summary>
|
||||
/// <summary>登录账户(唯一)</summary>
|
||||
public const string UserName = "user_name";
|
||||
|
||||
/// <summary>用户昵称</summary>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
namespace StoreHub.Framework.Security;
|
||||
namespace StoreHub.Framework.Security;
|
||||
|
||||
/// <summary>
|
||||
/// 在线用户会话信息(单点登录:同一用户仅保留一个有效会话)。
|
||||
/// </summary>
|
||||
public class OnlineUserInfo
|
||||
{
|
||||
/// <summary>用户ID</summary>
|
||||
public int UserId { get; set; }
|
||||
|
||||
/// <summary>登录账户</summary>
|
||||
/// <summary>登录账户(唯一,作为会话键)</summary>
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>用户昵称</summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StoreHub.Framework.Security;
|
||||
@@ -11,7 +11,7 @@ public class OnlineUserStore : IOnlineUserStore
|
||||
private const string CacheKeyPrefix = "online:user:";
|
||||
private readonly IMemoryCache _cache;
|
||||
// 在线人员索引:IMemoryCache 不提供公共枚举 API,这里用字典维护便于查询在线列表
|
||||
private readonly ConcurrentDictionary<int, OnlineUserInfo> _onlineIndex = new();
|
||||
private readonly ConcurrentDictionary<string, OnlineUserInfo> _onlineIndex = new(StringComparer.Ordinal);
|
||||
|
||||
public OnlineUserStore(IMemoryCache cache)
|
||||
{
|
||||
@@ -23,33 +23,33 @@ public class OnlineUserStore : IOnlineUserStore
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(info);
|
||||
|
||||
// 以 userId 为键覆盖写入:旧会话(jti)被顶替,实现单点登录
|
||||
_cache.Set(GetKey(info.UserId), info, new MemoryCacheEntryOptions
|
||||
// 以登录账户为键覆盖写入:旧会话(jti)被顶替,实现单点登录
|
||||
_cache.Set(GetKey(info.UserName), info, new MemoryCacheEntryOptions
|
||||
{
|
||||
// 缓存条目与令牌同时过期,过期后自动失效
|
||||
AbsoluteExpiration = info.ExpiresAt,
|
||||
});
|
||||
_onlineIndex[info.UserId] = info;
|
||||
_onlineIndex[info.UserName] = info;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsTokenActive(int userId, string jti)
|
||||
public bool IsTokenActive(string userName, string jti)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(jti))
|
||||
if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(jti))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _cache.TryGetValue(GetKey(userId), out OnlineUserInfo? info)
|
||||
return _cache.TryGetValue(GetKey(userName), out OnlineUserInfo? info)
|
||||
&& info is not null
|
||||
&& string.Equals(info.Jti, jti, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SignOut(int userId)
|
||||
public void SignOut(string userName)
|
||||
{
|
||||
_cache.Remove(GetKey(userId));
|
||||
_onlineIndex.TryRemove(userId, out _);
|
||||
_cache.Remove(GetKey(userName));
|
||||
_onlineIndex.TryRemove(userName, out _);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -57,7 +57,7 @@ public class OnlineUserStore : IOnlineUserStore
|
||||
{
|
||||
DateTime now = DateTime.Now;
|
||||
List<OnlineUserInfo> result = [];
|
||||
foreach (KeyValuePair<int, OnlineUserInfo> pair in _onlineIndex)
|
||||
foreach (KeyValuePair<string, OnlineUserInfo> pair in _onlineIndex)
|
||||
{
|
||||
if (pair.Value.ExpiresAt > now)
|
||||
{
|
||||
@@ -75,6 +75,6 @@ public class OnlineUserStore : IOnlineUserStore
|
||||
/// <inheritdoc />
|
||||
public int Count => GetOnlineUsers().Count;
|
||||
|
||||
private static string GetKey(int userId) => $"{CacheKeyPrefix}{userId}";
|
||||
private static string GetKey(string userName) => $"{CacheKeyPrefix}{userName}";
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Security.Claims;
|
||||
@@ -50,7 +50,7 @@ public class TokenService : ITokenService
|
||||
return new TokenResult
|
||||
{
|
||||
Token = token,
|
||||
ExpiresAt = expires,
|
||||
ExpiresInMinutes = (uint)_options.ExpiresMinutes,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,9 +74,9 @@ public class TokenService : ITokenService
|
||||
ClaimsPrincipal principal = new(result.ClaimsIdentity);
|
||||
|
||||
// 单点登录校验:会话必须仍然有效(未被退出、未被新登录顶替)
|
||||
string? userIdValue = principal.FindFirst(JwtClaimTypes.UserId)?.Value;
|
||||
string? userName = principal.FindFirst(JwtClaimTypes.UserName)?.Value;
|
||||
string? jti = principal.FindFirst(JwtClaimTypes.Jti)?.Value;
|
||||
if (!int.TryParse(userIdValue, out int userId) || string.IsNullOrWhiteSpace(jti) || !_onlineUserStore.IsTokenActive(userId, jti))
|
||||
if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(jti) || !_onlineUserStore.IsTokenActive(userName, jti))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -94,16 +94,15 @@ public class TokenService : ITokenService
|
||||
/// </summary>
|
||||
private void RegisterSession(IEnumerable<Claim> claims, string jti, DateTime loginTime, DateTime expiresAt)
|
||||
{
|
||||
string? userIdValue = claims.FirstOrDefault(c => c.Type == JwtClaimTypes.UserId)?.Value;
|
||||
if (!int.TryParse(userIdValue, out int userId))
|
||||
string? userName = claims.FirstOrDefault(c => c.Type == JwtClaimTypes.UserName)?.Value;
|
||||
if (string.IsNullOrWhiteSpace(userName))
|
||||
{
|
||||
return; // 未携带用户ID的令牌不纳入在线会话管理
|
||||
return; // 未携带登录账户的令牌不纳入在线会话管理
|
||||
}
|
||||
|
||||
_onlineUserStore.SignIn(new OnlineUserInfo
|
||||
{
|
||||
UserId = userId,
|
||||
UserName = claims.FirstOrDefault(c => c.Type == JwtClaimTypes.UserName)?.Value ?? string.Empty,
|
||||
UserName = userName,
|
||||
NickName = claims.FirstOrDefault(c => c.Type == JwtClaimTypes.NickName)?.Value ?? string.Empty,
|
||||
Jti = jti,
|
||||
LoginTime = loginTime,
|
||||
|
||||
@@ -47,7 +47,6 @@ namespace StoreHub.Controllers
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(JwtClaimTypes.UserId, user.Id.ToString()),
|
||||
new(JwtClaimTypes.UserName, user.UserName),
|
||||
new(JwtClaimTypes.NickName, user.NickName),
|
||||
};
|
||||
@@ -63,9 +62,9 @@ namespace StoreHub.Controllers
|
||||
[Authorize]
|
||||
public ApiResult<object?> Logout()
|
||||
{
|
||||
if (_currentUser.Id is int userId)
|
||||
if (_currentUser.UserName is string username)
|
||||
{
|
||||
_onlineUserStore.SignOut(userId);
|
||||
_onlineUserStore.SignOut(username);
|
||||
}
|
||||
|
||||
return ApiResult.Success<object?>(null);
|
||||
@@ -80,7 +79,6 @@ namespace StoreHub.Controllers
|
||||
{
|
||||
return ApiResult.Success<object?>(new
|
||||
{
|
||||
UserId = _currentUser.Id,
|
||||
UserName = _currentUser.UserName,
|
||||
NickName = _currentUser.NickName,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user