using Microsoft.Extensions.Options; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using System.Security.Claims; using System.Text; namespace StoreHub.Framework.Security; /// /// 基于 的 JWT 令牌服务。 /// public class TokenService : ITokenService { private readonly JwtOptions _options; private readonly IOnlineUserStore _onlineUserStore; public TokenService(IOptions options, IOnlineUserStore onlineUserStore) { _options = options.Value; _onlineUserStore = onlineUserStore; } /// public TokenResult CreateToken(IEnumerable claims) { ArgumentNullException.ThrowIfNull(claims); DateTime now = DateTime.Now; DateTime expires = now.AddMinutes(_options.ExpiresMinutes); string jti = Guid.NewGuid().ToString("N"); List allClaims = [.. claims, new Claim(JwtClaimTypes.Jti, jti)]; SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.SigningKey)); JsonWebTokenHandler handler = new(); string token = handler.CreateToken(new SecurityTokenDescriptor { Issuer = _options.Issuer, Audience = _options.Audience, Claims = ToClaimDictionary(allClaims), NotBefore = now, Expires = expires, SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256), }); // 注册在线会话(单点登录:同用户的新登录会顶替旧会话,旧令牌立即失效) RegisterSession(allClaims, jti, now, expires); return new TokenResult { Token = token, ExpiresAt = expires, }; } /// public async Task ValidateTokenAsync(string token) { if (string.IsNullOrWhiteSpace(token)) { return null; } try { JsonWebTokenHandler handler = new(); TokenValidationResult result = await handler.ValidateTokenAsync(token, _options.ToTokenValidationParameters()); if (!result.IsValid) { return null; } ClaimsPrincipal principal = new(result.ClaimsIdentity); // 单点登录校验:会话必须仍然有效(未被退出、未被新登录顶替) string? userIdValue = principal.FindFirst(JwtClaimTypes.UserId)?.Value; string? jti = principal.FindFirst(JwtClaimTypes.Jti)?.Value; if (!int.TryParse(userIdValue, out int userId) || string.IsNullOrWhiteSpace(jti) || !_onlineUserStore.IsTokenActive(userId, jti)) { return null; } return principal; } catch { return null; } } /// /// 将令牌声明注册为在线会话。 /// private void RegisterSession(IEnumerable claims, string jti, DateTime loginTime, DateTime expiresAt) { string? userIdValue = claims.FirstOrDefault(c => c.Type == JwtClaimTypes.UserId)?.Value; if (!int.TryParse(userIdValue, out int userId)) { return; // 未携带用户ID的令牌不纳入在线会话管理 } _onlineUserStore.SignIn(new OnlineUserInfo { UserId = userId, UserName = claims.FirstOrDefault(c => c.Type == JwtClaimTypes.UserName)?.Value ?? string.Empty, NickName = claims.FirstOrDefault(c => c.Type == JwtClaimTypes.NickName)?.Value ?? string.Empty, Jti = jti, LoginTime = loginTime, ExpiresAt = expiresAt, }); } /// /// 将声明集合转换为 所需的字典; /// 相同类型的多个声明合并为数组。 /// private static Dictionary ToClaimDictionary(IEnumerable claims) { Dictionary result = new(StringComparer.Ordinal); foreach (IGrouping group in claims.GroupBy(c => c.Type, StringComparer.Ordinal)) { object[] values = group.Select(c => (object)c.Value).ToArray(); result[group.Key] = values.Length == 1 ? values[0] : values; } return result; } }