From 912e1146d6b93f0b713c9cad1604d8715a3735a6 Mon Sep 17 00:00:00 2001 From: yubaolee Date: Mon, 6 Jul 2026 21:14:28 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9Bfix:=20#IJX34Q=20=E9=9D=9EIdentity?= =?UTF-8?q?=E7=9A=84token=E6=94=AF=E6=8C=81jwt=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Infrastructure/AppSetting.cs | 10 ++ OpenAuth.App/SSO/JwtTokenHelper.cs | 144 ++++++++++++++++++++ OpenAuth.App/SSO/LocalAuth.cs | 70 ++++++++-- OpenAuth.App/SSO/LoginParse.cs | 27 +++- OpenAuth.WebApi/appsettings.Production.json | 4 +- 5 files changed, 239 insertions(+), 16 deletions(-) create mode 100644 OpenAuth.App/SSO/JwtTokenHelper.cs diff --git a/Infrastructure/AppSetting.cs b/Infrastructure/AppSetting.cs index fdfefa22..96cea888 100644 --- a/Infrastructure/AppSetting.cs +++ b/Infrastructure/AppSetting.cs @@ -42,6 +42,16 @@ namespace Infrastructure /// public string RedisConf { get; set; } + /// + /// JWT签名密钥,用于本地认证模式下生成和验证JWT Token + /// + public string JwtSecret { get; set; } = "openauth_default_jwt_secret_key_2024"; + + /// + /// JWT Token过期天数,默认10天 + /// + public int JwtExpireDays { get; set; } = 10; + //是否是Identity授权方式 public bool IsIdentityAuth => !string.IsNullOrEmpty(IdentityServerUrl); } diff --git a/OpenAuth.App/SSO/JwtTokenHelper.cs b/OpenAuth.App/SSO/JwtTokenHelper.cs new file mode 100644 index 00000000..c700314b --- /dev/null +++ b/OpenAuth.App/SSO/JwtTokenHelper.cs @@ -0,0 +1,144 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace OpenAuth.App.SSO +{ + /// + /// JWT Token辅助类,用于本地认证模式下的Token生成和验证 + /// + public static class JwtTokenHelper + { + /// + /// 生成JWT Token + /// + /// 用户账号 + /// 用户名称 + /// 应用标识 + /// 会话ID,用作缓存Key + /// JWT签名密钥 + /// 过期天数 + /// JWT Token字符串 + public static string GenerateToken(string account, string name, string appKey, string sessionId, string secret, int expireDays = 10) + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(EnsureKeyLength(secret))); + var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(JwtRegisteredClaimNames.Sub, account), + new Claim(JwtRegisteredClaimNames.Jti, sessionId), + new Claim("name", name ?? string.Empty), + new Claim("app_key", appKey ?? string.Empty), + }; + + var token = new JwtSecurityToken( + issuer: "OpenAuth", + audience: "OpenAuth", + claims: claims, + expires: DateTime.UtcNow.AddDays(expireDays), + signingCredentials: credentials + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + /// + /// 验证JWT Token并返回ClaimsPrincipal + /// + /// JWT Token字符串 + /// JWT签名密钥 + /// 验证成功返回ClaimsPrincipal,失败返回null + public static ClaimsPrincipal ValidateToken(string token, string secret) + { + if (string.IsNullOrEmpty(token)) + return null; + + var tokenHandler = new JwtSecurityTokenHandler(); + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(EnsureKeyLength(secret))); + + try + { + var validationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = key, + ValidateIssuer = true, + ValidIssuer = "OpenAuth", + ValidateAudience = true, + ValidAudience = "OpenAuth", + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(5) + }; + + var principal = tokenHandler.ValidateToken(token, validationParameters, out _); + return principal; + } + catch + { + return null; + } + } + + /// + /// 从JWT Token中提取会话ID(jti) + /// + public static string GetSessionId(string token) + { + var principal = ReadTokenWithoutValidation(token); + return principal?.FindFirst(JwtRegisteredClaimNames.Jti)?.Value; + } + + /// + /// 从JWT Token中提取用户账号(sub) + /// + public static string GetAccount(string token) + { + var principal = ReadTokenWithoutValidation(token); + return principal?.FindFirst(JwtRegisteredClaimNames.Sub)?.Value + ?? principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value; + } + + /// + /// 不验证签名,仅读取Token中的Claims(用于快速提取信息) + /// + private static ClaimsPrincipal ReadTokenWithoutValidation(string token) + { + if (string.IsNullOrEmpty(token)) + return null; + + try + { + var tokenHandler = new JwtSecurityTokenHandler(); + if (!tokenHandler.CanReadToken(token)) + return null; + + var jwtToken = tokenHandler.ReadJwtToken(token); + var identity = new ClaimsIdentity(jwtToken.Claims); + return new ClaimsPrincipal(identity); + } + catch + { + return null; + } + } + + /// + /// 确保密钥长度至少为32字节(256位),不足时进行填充 + /// + private static string EnsureKeyLength(string secret) + { + if (string.IsNullOrEmpty(secret)) + secret = "openauth_default_jwt_secret_key_2024"; + + // HMAC-SHA256要求密钥至少128位(16字节),建议256位(32字节) + while (secret.Length < 32) + { + secret += secret; + } + return secret.Substring(0, Math.Max(32, secret.Length)); + } + } +} diff --git a/OpenAuth.App/SSO/LocalAuth.cs b/OpenAuth.App/SSO/LocalAuth.cs index fb604ef5..f22d4c5e 100644 --- a/OpenAuth.App/SSO/LocalAuth.cs +++ b/OpenAuth.App/SSO/LocalAuth.cs @@ -36,6 +36,7 @@ namespace OpenAuth.App.SSO /// /// 如果是Identity,则返回信息为用户账号 + /// 如果是本地认证,则返回JWT Token字符串 /// /// private string GetToken() @@ -54,6 +55,15 @@ namespace OpenAuth.App.SSO return cookie ?? String.Empty; } + /// + /// 从JWT Token中提取会话ID(jti),用于缓存查找 + /// + private string GetSessionIdFromToken(string token) + { + if (string.IsNullOrEmpty(token)) return null; + return JwtTokenHelper.GetSessionId(token); + } + public bool CheckLogin(string token = "", string otherInfo = "") { if (_appConfiguration.Value.IsIdentityAuth) @@ -73,7 +83,21 @@ namespace OpenAuth.App.SSO try { - var result = _cacheContext.Get(token) != null; + // 验证JWT Token签名和有效期 + var principal = JwtTokenHelper.ValidateToken(token, _appConfiguration.Value.JwtSecret); + if (principal == null) + { + return false; + } + + // 检查会话是否在缓存中(确保未被登出失效) + var sessionId = GetSessionIdFromToken(token); + if (string.IsNullOrEmpty(sessionId)) + { + return false; + } + + var result = _cacheContext.Get(sessionId) != null; return result; } catch (Exception ex) @@ -95,17 +119,29 @@ namespace OpenAuth.App.SSO return _app.GetAuthStrategyContext(GetToken()); } AuthStrategyContext context = null; - var user = _cacheContext.Get(GetToken()); - if (user != null) + var token = GetToken(); + + // 从JWT Token中直接提取用户账号 + var account = JwtTokenHelper.GetAccount(token); + if (!string.IsNullOrEmpty(account)) { - context = _app.GetAuthStrategyContext(user.Account); + // 验证会话是否有效(未被登出) + var sessionId = GetSessionIdFromToken(token); + if (!string.IsNullOrEmpty(sessionId)) + { + var session = _cacheContext.Get(sessionId); + if (session != null) + { + context = _app.GetAuthStrategyContext(account); + } + } } return context; } /// /// 获取当前登录的用户名 - /// 通过URL中的Token参数或Cookie中的Token + /// 通过JWT Token中的claims直接提取用户账号 /// /// The account. /// System.String. @@ -116,10 +152,21 @@ namespace OpenAuth.App.SSO return _httpContextAccessor.HttpContext.User.Identity.Name; } - var user = _cacheContext.Get(GetToken()); - if (user != null) + var token = GetToken(); + // 从JWT Token中提取用户账号 + var account = JwtTokenHelper.GetAccount(token); + if (!string.IsNullOrEmpty(account)) { - return user.Account; + // 验证会话是否有效 + var sessionId = GetSessionIdFromToken(token); + if (!string.IsNullOrEmpty(sessionId)) + { + var session = _cacheContext.Get(sessionId); + if (session != null) + { + return account; + } + } } return ""; @@ -173,7 +220,12 @@ namespace OpenAuth.App.SSO try { - _cacheContext.Remove(token); + // 从JWT Token中提取会话ID,删除缓存 + var sessionId = GetSessionIdFromToken(token); + if (!string.IsNullOrEmpty(sessionId)) + { + _cacheContext.Remove(sessionId); + } return true; } catch diff --git a/OpenAuth.App/SSO/LoginParse.cs b/OpenAuth.App/SSO/LoginParse.cs index 9ef93a92..8f3084bb 100644 --- a/OpenAuth.App/SSO/LoginParse.cs +++ b/OpenAuth.App/SSO/LoginParse.cs @@ -13,6 +13,7 @@ using SqlSugar; using Infrastructure.Extensions.AutofacManager; using Infrastructure.Utilities; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; namespace OpenAuth.App.SSO @@ -24,11 +25,13 @@ namespace OpenAuth.App.SSO protected ISqlSugarClient SugarClient; private ICacheContext _cacheContext; private AppManager _appInfoService; + private IOptions _appConfiguration; - public LoginParse( AppManager infoService, ICacheContext cacheContext, ISqlSugarClient client) + public LoginParse( AppManager infoService, ICacheContext cacheContext, ISqlSugarClient client, IOptions appConfiguration) { _appInfoService = infoService; _cacheContext = cacheContext; + _appConfiguration = appConfiguration; string tenantId = Define.DEFAULT_TENANT_ID; var httpContextAccessor = AutofacContainerModule.GetService(); if (httpContextAccessor != null) @@ -87,21 +90,33 @@ namespace OpenAuth.App.SSO throw new Exception("账号状态异常,可能已停用"); } + // 生成唯一的会话ID作为缓存Key + var sessionId = Guid.NewGuid().ToString("N"); + var expireDays = _appConfiguration.Value.JwtExpireDays; + var currentSession = new UserAuthSession { Account = model.Account, Name = sysUserInfo.Name, - Token = Guid.NewGuid().ToString().GetHashCode().ToString("x"), + Token = sessionId, AppKey = model.AppKey, CreateTime = TimeHelper.Now - // , IpAddress = HttpContext.Current.Request.UserHostAddress }; - //创建Session - _cacheContext.Set(currentSession.Token, currentSession, TimeHelper.Now.AddDays(10)); + //创建Session缓存,用于登出时失效判定 + _cacheContext.Set(sessionId, currentSession, TimeHelper.Now.AddDays(expireDays)); + + // 生成JWT Token + var jwtToken = JwtTokenHelper.GenerateToken( + model.Account, + sysUserInfo.Name, + model.AppKey, + sessionId, + _appConfiguration.Value.JwtSecret, + expireDays); result.Code = 200; - result.Token = currentSession.Token; + result.Token = jwtToken; } catch (Exception ex) { diff --git a/OpenAuth.WebApi/appsettings.Production.json b/OpenAuth.WebApi/appsettings.Production.json index 92646a9f..7490d247 100644 --- a/OpenAuth.WebApi/appsettings.Production.json +++ b/OpenAuth.WebApi/appsettings.Production.json @@ -19,6 +19,8 @@ }, "UploadPath": "", //附件上传的路径,如果为空则保存在站点根目录 "RedisConf": "your_redis_server:6379,password=your_redis_password", //redis配置信息 - "HttpHost": "http://*:52789" //启动绑定地址及端口 + "HttpHost": "http://*:52789", //启动绑定地址及端口 + "JwtSecret": "openauth_jwt_secret_key_please_change_in_production", //JWT签名密钥,生产环境请修改 + "JwtExpireDays": 10 //JWT Token过期天数 } }