using DocumentFormat.OpenXml.Office2010.Excel; using Lazy.Captcha.Core; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NPOI.SS.Formula.Functions; using System.Security.Claims; using YLErp.Configuration; namespace YLErp.Web.Controllers { public class AccountController : Controller { private const string _auth_login = "_auth_login_"; private readonly IHttpClientFactory _httpClientFactory; private readonly ICaptcha _captcha; public AccountController(IHttpClientFactory httpClientFactory, ICaptcha captcha) { _httpClientFactory = httpClientFactory; _captcha = captcha; } //--------------------------------------- // 安全逻辑: // 每个浏览器客户端应该有一个guid确定其唯一性 // 如果没有guid则在cookie中设置并需要输入验证码 // 如果有guid,则根据服务端缓存判断此浏览器是否上次登录失败 // 如果上次登录失败,则显示验证码输入,否则不需要 //--------------------------------------- [ResponseCache(NoStore = true)] public ActionResult Login(bool skipSso = false) { var logger = LogFactory.GetLogger(); var remoteIp = HttpContext.Connection.RemoteIpAddress?.ToString(); // 记录所有 Cookie var allCookies = Request.Cookies.Select(c => $"{c.Key}={(c.Value?.Length > 10 ? c.Value?.Substring(0, 10) + "..." : c.Value)}").ToList(); var hasAccessToken = Request.Cookies.ContainsKey("Access-Token"); logger.Info($"[登录页面] 访问登录页,IP: {remoteIp}, skipSso: {skipSso}, HasAccessToken: {hasAccessToken}, Cookies: [{string.Join(", ", allCookies)}]"); var keys = Request.Cookies.Keys.ToArray(); logger.Info($"[登录页面] 清除 {keys.Length} 个 Cookie(保留 {_auth_login})"); foreach (var key in keys) { if (key != _auth_login) { Response.Cookies.Delete(key); logger.Info($"[登录页面] 删除 Cookie: {key}"); } } // 检查天风SSO if (SsoLoginConfig.SSO_Enable && PS.Config.Company == CompanyEnum.天风) { logger.Info($"[登录页面] 天风SSO已启用,跳转到天风登录页"); return View("TianFengLogin"); } // 检查是否禁用密码登录 var disablePasswordLogin = PS.Config.ErpElement.DisablePasswordLogin; var strictAuthorize = PS.Config.ErpElement.StrictAuthorize; logger.Info($"[登录页面] 配置检查 - DisablePasswordLogin: {disablePasswordLogin}, StrictAuthorize: {strictAuthorize}, Company: {PS.Config.Company}"); // 注意:DisablePasswordLogin 的逻辑在 Login.cshtml 视图中处理 // 这里不再直接跳转,而是显示登录页,由页面决定显示什么内容 return View(); } public ActionResult Login2() { return Login(true); } /// /// 内置超管账密登录兜底页 /// public ActionResult AdminLogin() { return LoginError(null); } /// /// 登录错误 /// private ActionResult LoginError(string errorMessage, Exception ex = null) { var strictAuthorize = PS.Config.ErpElement.StrictAuthorize; var model = new LogOnModel { ErrorMessage = errorMessage, NeedCaptcha = strictAuthorize, Exception = ex, LoadTime = DateTime.Now }; if (strictAuthorize) { var hasError = !string.IsNullOrEmpty(errorMessage); Request.Cookies.TryGetValue(_auth_login, out var guid); if (string.IsNullOrEmpty(guid) || guid.Length < 30) { guid = Guid.NewGuid().ToString("N"); Response.Cookies.Append(_auth_login, guid, new CookieOptions { HttpOnly = true, Expires = DateTimeOffset.Now.AddHours(1) }); } Server.CacheProvider.Set(_auth_login + guid, hasError ? "##" : "#", TimeSpan.FromMinutes(20)); var sid = guid + "#" + model.LoadTime.ToString("yyyyMMddHHmmss") + (model.NeedCaptcha ? "#1" : "#0"); model.SID = DataProtectHelper.Encrypt(sid); } return View(nameof(AdminLogin), model); } [HttpPost, ResponseCache(NoStore = true)] public async Task LoginHandle(LogOnModel model) { var logger = LogFactory.GetLogger(); var hasToken = Request.Cookies.ContainsKey("Access-Token"); logger.Info($"[登录处理] 收到登录请求 - UserName: {model?.UserName}, HasAccessToken: {hasToken}"); if (string.IsNullOrWhiteSpace(model?.UserName) || string.IsNullOrWhiteSpace(model?.Password)) { logger.Info("[登录处理] 用户名或密码为空"); return LoginError("用户名和密码必须填写"); } if (model.LoadTime.Year < 2000) { return LoginError("登录失败,非正常请求"); } if (PS.Config.ErpElement.StrictAuthorize) { if (string.IsNullOrEmpty(model.SID) || DateTime.Now > model.LoadTime.AddMinutes(20)) { return LoginError("请求已过期,请重新登录"); } Request.Cookies.TryGetValue(_auth_login, out var guid); if (string.IsNullOrEmpty(guid)) { return LoginError("请求无效,请重新登录"); } try { var sids = DataProtectHelper.Decrypt(model.SID).Split('#'); if (sids.Length < 3 || sids[0] != guid ) { return LoginError("请求参数失效,请重新登录"); } if (sids[2] == "1") { var key = _auth_login + guid; if (!_captcha.Validate(key, model.CaptchaCode)) { return LoginError("验证码错误"); } } } catch (Exception ex) { return LoginError("验证出错", ex); } } try { model.UserName = Decode(model.UserName); model.Password = Decode(model.Password); } catch (Exception ex) { return LoginError("验证出错", ex); } var errorNumStr = Server.CacheProvider.Get($"login:user:error:{model.UserName.ToLower()}")?.ToString(); var errorNum = string.IsNullOrEmpty(errorNumStr) ? 0 : Convert.ToInt32(errorNumStr); if (errorNum >= PS.Config.ErpElement.PasswordErrorLimit && PS.Config.ErpElement.StrictAuthorize) { return LoginError("账户已被锁定,请稍后再试"); } SystemUser user = null; if (SsoLoginConfig.SSO_Enable) { switch (model.UserName.ToLowerInvariant()) { case "admin": case "supera": break; default: if (PS.Config.Company == CompanyEnum.天风) { return LoginError("非管理员账户,请刷新页面后使用普通用户登录方式"); } break; } } if (user == null) { var result = SystemUser.CheckUser(model.UserName, model.Password, out user); if (result != UserCheckResult.验证通过) { var errorResult = result.ToString(); if (PS.Config.ErpElement.StrictAuthorize) { errorNum++; var passwordErrorLimit = PS.Config.ErpElement.PasswordErrorLimit; if (errorNum < passwordErrorLimit) { errorResult = $"{errorResult},您还有{passwordErrorLimit - errorNum}次机会登录尝试"; } else { var lockMinutes = PS.Config.ErpElement.PasswordErrorLockMinutes; errorResult = $"{errorResult},账号已被锁定,请{lockMinutes}分钟后再尝试!"; } Server.CacheProvider.Set($"login:user:error:{model.UserName.ToLower()}", errorNum, TimeSpan.FromMinutes(PS.Config.ErpElement.PasswordErrorLockMinutes)); } return LoginError(errorResult); } if (user == null || user.Id < 1) { return LoginError("验证失败," + result.ToString()); } } var userToken = Guid.NewGuid().ToString("N"); using (var db = DbContextFactory.GetErpBaseContext()) { user = db.SystemUsers.Find(user.Id); user.UserToken = userToken; db.SystemUserLoginfo.Add(new SystemUserLoginfo { IPAddress = HttpContext.Connection.RemoteIpAddress?.ToString(), LogTime = DateTime.Now, LogType = "登入", UserId = user.Id, UserName = user.Name, LoginName = model.UserName, MACAddress = "", LoadTime = model.LoadTime }); db.SaveChanges(); } //保存到cookie Server.CacheProvider.Set($"login:user:error:{model.UserName.ToLower()}", 0, TimeSpan.FromMinutes(PS.Config.ErpElement.PasswordErrorLockMinutes)); var identity = UserManager.CreateIdentity(user.Id.ToString(), user.Name, userToken); await HttpContext.SignInAsync(AuthHelper.CookieAuthType, new ClaimsPrincipal(identity)); // 先删除可能存在的旧Token(避免黑名单影响) Response.Cookies.Delete("Access-Token"); Response.Cookies.Delete("Access-Token-Encrypt"); var jwtToken = AuthHelper.CreateJwtToken(identity); Response.Cookies.Append("Access-Token", jwtToken, new CookieOptions { MaxAge = TimeSpan.FromSeconds(AuthHelper.ExpireInSeconds) }); Response.Cookies.Append("Access-Token-Encrypt", DataProtectHelper.Encrypt(jwtToken)); //保存到缓存 var userInfo = UserInfo.FromDbUser(user); Server.CacheProvider.Set("loginUser^" + user.Id, userInfo, TimeSpan.FromMinutes(30)); if (!PasswordValidator.Validate(model.Password)) { Server.CacheProvider.Set("简单密码" + user.Id, "是", TimeSpan.FromMinutes(60)); } return RedirectToAction("Index", "Home", new { g = Guid.NewGuid() }); } public ActionResult LogOut() { var logger = LogFactory.GetLogger(); var userId = User.GetUserId(); var loginName = User.GetUserName(); var remoteIp = HttpContext.Connection.RemoteIpAddress?.ToString(); // 记录所有 Cookie var allCookies = Request.Cookies.Select(c => $"{c.Key}={c.Value?.Substring(0, Math.Min(10, c.Value?.Length ?? 0))}...").ToList(); logger.Info($"[退出登录] 用户请求退出 - UserId: {userId}, LoginName: {loginName}, IP: {remoteIp}, Cookies: [{string.Join(", ", allCookies)}]"); if (userId > 0) { Server.CacheProvider.Remove("loginUser^" + userId); try { using (var db = Modules.DbContextFactory.GetErpBaseContext()) { var user = db.SystemUsers.Find(userId); if (user != null) { user.UserToken = string.Empty; loginName = user.LoginName; } db.SystemUserLoginfo.Add(new SystemUserLoginfo { IPAddress = remoteIp, LogTime = DateTime.Now, LogType = "登出", UserId = userId, UserName = user?.Name ?? User.GetUserName(), MACAddress = string.Empty }); db.SaveChanges(); logger.Info($"[退出登录] 用户退出记录已保存 - UserId: {userId}, LoginName: {loginName}"); } } catch (Exception ex) { logger.Error($"[退出登录] 保存退出记录失败 - UserId: {userId}", ex); } } HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme); logger.Info($"[退出登录] SignOutAsync 已调用"); // 先将当前 JWT Token 加入黑名单(使其立即失效),然后再删除 Cookie string accessToken = null; if (Request.Cookies.TryGetValue("Access-Token", out accessToken) && !string.IsNullOrEmpty(accessToken)) { try { var tokenHandler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler(); var jwtToken = tokenHandler.ReadJwtToken(accessToken); var jti = jwtToken.Claims.FirstOrDefault(c => c.Type == "jti")?.Value ?? accessToken.Substring(0, 32); var expiry = jwtToken.ValidTo; var ttl = expiry - DateTime.UtcNow; if (ttl.TotalSeconds > 0) { // 将 Token 加入黑名单,过期时间与 Token 过期时间相同 Server.CacheProvider.Set($"token_blacklist:{jti}", userId.ToString(), ttl); logger.Info($"[退出登录] Token 已加入黑名单 - JTI: {jti}, 过期时间: {expiry:yyyy-MM-dd HH:mm:ss}"); } } catch (Exception ex) { logger.Error($"[退出登录] Token 加入黑名单失败", ex); } } // 清除所有 Cookie(确保旧Token不会带到登录页面) var keys = Request.Cookies.Keys.ToArray(); foreach (var key in keys) { Response.Cookies.Delete(key); } logger.Info($"[退出登录] 所有 Cookie 已清除,共 {keys.Length} 个"); if (SsoLoginConfig.SSO_Enable) { switch (loginName?.ToLowerInvariant()) { case "admin": case "supera": break; default: if (PS.Config.Company == CompanyEnum.天风) { return View("TianFengLogout"); } break; } } logger.Info($"[退出登录] 准备重定向到登录页,清除的Cookie数量: {keys.Length}"); // 添加响应头,确保浏览器清除Cookie Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); Response.Headers.Add("Pragma", "no-cache"); Response.Headers.Add("Expires", "0"); return RedirectToAction("Login"); } public ActionResult Captcha() { Request.Cookies.TryGetValue(_auth_login, out var guid); if (string.IsNullOrEmpty(guid) || guid.Length < 30) { return StatusCode((int)System.Net.HttpStatusCode.InternalServerError, "cookie无效"); } var key = _auth_login + guid; var value = Server.CacheProvider.Get(key)?.ToString(); if (value == null) { return StatusCode((int)System.Net.HttpStatusCode.InternalServerError, "页面停留超时"); } var info = _captcha.Generate(key); // 有多处验证码且过期时间不一样,可传第二个参数覆盖默认配置。 //var info = _captcha.Generate(id,120); var stream = new MemoryStream(info.Bytes); return File(stream, "image/jpeg"); } private static string Decode(string btoa) { var mod4 = btoa.Length % 4; if (mod4 > 0) { btoa += new string('=', 4 - mod4); } var data = Convert.FromBase64String(btoa); var strs = System.Text.Encoding.ASCII.GetString(data).Split(','); var arr = new char[strs.Length]; for (var i = 0; i < strs.Length; i++) { arr[i] = (char)int.Parse(strs[i]); } return new string(arr); } #region----天风证券单点登录---- /// /// 天风证券单点登录 /// public async Task TianFengSSO() { var serviceUrl = Url.Action("TianFengSSO", "account", null, Request.Scheme); var model = new SsoLogOnModel { SsoUrl = TianFengSSOHandler.GetSsoLoginUrl(serviceUrl) }; try { var ticket = Request.Query["ticket"]; if (string.IsNullOrWhiteSpace(ticket)) { return View(model.SetError("缺少单点登录令牌!")); } var userName = await new TianFengSSOHandler(_httpClientFactory).ValidateAsync(ticket, serviceUrl); var userToken = Guid.NewGuid().ToString("N"); SystemUser user = null; using (var db = Modules.DbContextFactory.GetErpBaseContext()) { user = db.SystemUsers.FirstOrDefault(n => n.LoginName == userName); if (user == null) { return View(model.SetError("找不到系统用户:" + userName)); } user.UserToken = userToken; db.SystemUserLoginfo.Add(new SystemUserLoginfo { IPAddress = HttpContext.Connection.RemoteIpAddress.ToString(), LogTime = DateTime.Now, LogType = "登入", UserId = user.Id, UserName = user.Name, LoginName = user.LoginName, MACAddress = "", LoadTime = DateTime.Now }); db.SaveChanges(); } //保存到cookie var identity = UserManager.CreateIdentity(user.Id.ToString(), user.Name, userToken); await HttpContext.SignInAsync(AuthHelper.CookieAuthType, new ClaimsPrincipal(identity)); var jwtToken = AuthHelper.CreateJwtToken(identity); Response.Cookies.Append("Access-Token", jwtToken, new CookieOptions { Domain = Request.Host.Host, MaxAge = TimeSpan.FromSeconds(AuthHelper.ExpireInSeconds) }); //保存到缓存 var userInfo = UserInfo.FromDbUser(user); Server.CacheProvider.Set("loginUser^" + user.Id, userInfo, TimeSpan.FromMinutes(30)); return RedirectToAction("Index", "Home"); } catch (Exception ex) { return View(model.SetError("单点登录出错:" + ex.ToString())); } } #endregion #region----通用单点登录回调---- /// /// 通用单点登录回调处理 /// public async Task Callback() { try { var code = Request.Query["code"]; var state = Request.Query["state"]; if (string.IsNullOrWhiteSpace(code)) { return Content("缺少授权码(code参数)"); } // 调用SSO服务器接口获取用户信息 var getUserUrl = GeneralSSOConfig.GetUserUrl; if (string.IsNullOrWhiteSpace(getUserUrl)) { return Content("获取用户信息接口地址未配置"); } var httpClient = _httpClientFactory.CreateClient(); var response = await httpClient.GetStringAsync($"{getUserUrl}?code={code}"); if (string.IsNullOrWhiteSpace(response)) { return Content("获取用户信息失败"); } // 解析返回的用户信息JSON var userInfoData = JsonConvert.DeserializeObject>(response); if (userInfoData.ContainsKey("data")) { var dataValue = userInfoData["data"]; Dictionary data = null; try { // 尝试直接解析为字典 if (dataValue is JToken jToken) { data = jToken.ToObject>(); } else if (dataValue is Dictionary dataDict) { data = dataDict; } else if (dataValue is string dataString) { // 如果是字符串,尝试解析为JSON data = JsonConvert.DeserializeObject>(dataString); } else { // 尝试将任何类型转换为字符串再解析 var stringValue = dataValue?.ToString(); if (!string.IsNullOrEmpty(stringValue)) { data = JsonConvert.DeserializeObject>(stringValue); } } } catch (Exception ex) { return Content("解析data字段失败: " + ex.Message); } if (data == null) { return Content("data字段解析失败"); } if (data.ContainsKey("user_id")) { var userId = data["user_id"]?.ToString()?.Trim(); if (string.IsNullOrEmpty(userId)) { return Content("userId字段为空"); } var userToken = Guid.NewGuid().ToString("N"); SystemUser user = null; using (var db = Modules.DbContextFactory.GetErpBaseContext()) { user = db.SystemUsers.FirstOrDefault(n => n.LoginName == userId); if (user == null) { return Content("找不到系统用户:" + userId); } if (user.State != (int)UserState.Enabled) { return Content("系统用户:" + userId + "处于禁用状态"); } user.UserToken = userToken; db.SystemUserLoginfo.Add(new SystemUserLoginfo { IPAddress = HttpContext.Connection.RemoteIpAddress.ToString(), LogTime = DateTime.Now, LogType = "登入", UserId = user.Id, UserName = user.Name, LoginName = user.LoginName, MACAddress = "", LoadTime = DateTime.Now }); db.SaveChanges(); } var identity = UserManager.CreateIdentity(user.Id.ToString(), user.Name, userToken); await HttpContext.SignInAsync(AuthHelper.CookieAuthType, new ClaimsPrincipal(identity)); var jwtToken = AuthHelper.CreateJwtToken(identity); Response.Cookies.Append("Access-Token", jwtToken, new CookieOptions { Domain = Request.Host.Host, MaxAge = TimeSpan.FromSeconds(AuthHelper.ExpireInSeconds) }); Response.Cookies.Append("Access-Token-Encrypt", DataProtectHelper.Encrypt(jwtToken), new CookieOptions { Domain = Request.Host.Host, MaxAge = TimeSpan.FromSeconds(AuthHelper.ExpireInSeconds) }); // 打印日志 var logger = LogFactory.GetLogger(); logger.Info($"用户 {user.LoginName} 登录成功,生成JWT令牌"); var userDbInfo = UserInfo.FromDbUser(user); Server.CacheProvider.Set("loginUser^" + user.Id, userDbInfo, TimeSpan.FromMinutes(30)); return RedirectToAction("Index", "Home"); } else { return Content("响应中缺少fullname字段"); } } else { return Content("响应中缺少data字段"); } } catch (Exception ex) { return Content("单点登录出错:" + ex.ToString()); } } #endregion } }