Files
zszq-trs/YLErpWeb/Controllers/AccountController.cs
T
hjhan 7cd937518f refactor(auth): 优化JWT令牌创建方法
- 移除CreateJwtToken2方法,统一使用CreateJwtToken方法
- 修改密钥处理方式,从ASCII字符串改为Base64解码
- 删除IssuedAt声明以简化令牌结构
- 更新所有相关调用点使用统一的令牌创建方法
2026-04-27 15:34:12 +08:00

663 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,则根据服务端缓存判断此浏览器是否上次登录失败
// 如果上次登录失败,则显示验证码输入,否则不需要
//---------------------------------------
public ActionResult Login(bool skipSso = false)
{
var logger = LogFactory.GetLogger<AccountController>();
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 LoginError(null);
}
public ActionResult Login2()
{
return Login(true);
}
/// <summary>
/// 登录错误
/// </summary>
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(Login), model);
}
[HttpPost, ResponseCache(NoStore = true)]
public async Task<IActionResult> LoginHandle(LogOnModel model)
{
var logger = LogFactory.GetLogger<AccountController>();
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<AccountController>();
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----天风证券单点登录----
/// <summary>
/// 天风证券单点登录
/// </summary>
public async Task<ActionResult> 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----通用单点登录回调----
/// <summary>
/// 通用单点登录回调处理
/// </summary>
public async Task<ActionResult> 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<Dictionary<string, object>>(response);
if (userInfoData.ContainsKey("data"))
{
var dataValue = userInfoData["data"];
Dictionary<string, object> data = null;
try
{
// 尝试直接解析为字典
if (dataValue is JToken jToken)
{
data = jToken.ToObject<Dictionary<string, object>>();
}
else if (dataValue is Dictionary<string, object> dataDict)
{
data = dataDict;
}
else if (dataValue is string dataString)
{
// 如果是字符串,尝试解析为JSON
data = JsonConvert.DeserializeObject<Dictionary<string, object>>(dataString);
}
else
{
// 尝试将任何类型转换为字符串再解析
var stringValue = dataValue?.ToString();
if (!string.IsNullOrEmpty(stringValue))
{
data = JsonConvert.DeserializeObject<Dictionary<string, object>>(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<AccountController>();
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
}
}