- 移除 skipSso 参数并优化天风证券SSO登录逻辑 - 添加 ErpElement.DisablePasswordLogin 配置项控制密码登录 - 增加 SSO重定向功能当密码登录被禁用时 - 在appconfig.xml中添加 Erp.DisablePasswordLogin 配置参数 - 在AppUpgrader中初始化DisablePasswordLogin配置数据 - 扩展IErpConfig接口添加DisablePasswordLogin属性定义 - 在ProjectSettings类中实现DisablePasswordLogin属性
592 lines
22 KiB
C#
592 lines
22 KiB
C#
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 keys = Request.Cookies.Keys.ToArray();
|
|
|
|
foreach (var key in keys)
|
|
{
|
|
if (key != _auth_login)
|
|
{
|
|
Response.Cookies.Delete(key);
|
|
}
|
|
}
|
|
|
|
if (SsoLoginConfig.SSO_Enable && PS.Config.Company == CompanyEnum.天风)
|
|
{
|
|
return View("TianFengLogin");
|
|
}
|
|
|
|
if (PS.Config.ErpElement.DisablePasswordLogin)
|
|
{
|
|
var ssoUrl = YLErp.Web.Models.GeneralSSOConfig.AuthUrl;
|
|
if (!string.IsNullOrEmpty(ssoUrl))
|
|
{
|
|
return Redirect(ssoUrl);
|
|
}
|
|
return Content("SSO登录未配置,请联系管理员");
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(model?.UserName) || string.IsNullOrWhiteSpace(model?.Password))
|
|
{
|
|
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));
|
|
|
|
var jwtToken = AuthHelper.CreateJwtToken2(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 userId = User.GetUserId();
|
|
var loginName = User.GetUserName();
|
|
|
|
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 = HttpContext.Connection.RemoteIpAddress?.ToString(),
|
|
LogTime = DateTime.Now,
|
|
LogType = "登出",
|
|
UserId = userId,
|
|
UserName = user?.Name ?? User.GetUserName(),
|
|
MACAddress = string.Empty
|
|
});
|
|
db.SaveChanges();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogFactory.GetLogger("用户登出").Error(ex);
|
|
}
|
|
}
|
|
|
|
HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme);
|
|
|
|
if (SsoLoginConfig.SSO_Enable)
|
|
{
|
|
switch (loginName?.ToLowerInvariant())
|
|
{
|
|
case "admin":
|
|
case "supera": break;
|
|
default:
|
|
if (PS.Config.Company == CompanyEnum.天风)
|
|
{
|
|
return View("TianFengLogout");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
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
|
|
}
|
|
} |