更新 JWT

This commit is contained in:
chfei committed 2026-08-28 15:31:45 +08:00
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 -11
View File
@@ -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 -6
View File
@@ -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>
/// 获取当前在线人员列表。
+2 -2
View File
@@ -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>
+2 -4
View File
@@ -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>
+13 -13
View File
@@ -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}";
}
+8 -9
View File
@@ -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,
+2 -4
View File
@@ -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,
});