using Application.Domain.Entities; using Application.Models; using Infrastructure.Application.Services.Settings; using Infrastructure.Data; using Infrastructure.Email; using Infrastructure.Extensions; using Infrastructure.Security; using Infrastructure.Sms; using Infrastructure.Web; using Infrastructure.Web.DataAnnotations; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; namespace UserCenter.Controllers { [Authorize] public class AccountController : BaseController { private readonly IConfiguration _cfg; private readonly ISettingService _settingService; private readonly IRepository _userRepo; private readonly IRepository _siteRepo; private readonly IEncryptionService _encryptionService; private readonly IEmailSender _emailSender; private readonly ISmsSender _smsSender; //private readonly FaceRecognitionService _frs; public AccountController(IConfiguration cfg, ISettingService settingService, IRepository userRepo, IRepository siteRepo, IEncryptionService encryptionService, IEmailSender emaliSender, ISmsSender smsSender //FaceRecognitionService frs ) { this._cfg = cfg; this._settingService = settingService; this._userRepo = userRepo; this._siteRepo = siteRepo; this._encryptionService = encryptionService; this._emailSender = emaliSender; this._smsSender = smsSender; //this._frs = frs; } #region 用户中心 public IActionResult Index() { return View(this._siteRepo.ReadOnlyTable().ToList()); } #endregion 用户中心 #region 注销 public IActionResult Logout(string returnUrl = null) { var username = User.Identity.Name; var urls = new List(); var list = this._siteRepo.ReadOnlyTable().ToList(); foreach (var site in list) { var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(); var sign = string.Concat(username, timestamp, site.Key).Md5(); var url = site.Logout .SetParam(nameof(username), username) .SetParam(nameof(timestamp), timestamp) .SetParam(nameof(sign), sign); urls.Add(url); } if (string.IsNullOrEmpty(returnUrl)) { returnUrl = Url.Action("Index", "Home"); } ViewBag.Url = returnUrl; HttpContext.JwtSignOut(); return View("JsonpLogout", urls); } #endregion 注销 #region 登录 [AllowAnonymous] [HttpGet] public IActionResult Login(string returnUrl = null) { ViewData["ReturnUrl"] = returnUrl; return View(); } [AllowAnonymous] [HttpPost] public IActionResult Login(LoginModel model, string returnUrl = null) { var userName = model.UserName; var password = model.Password; var key = ""; var message = ""; if (ModelState.IsValid) { try { var user = this._userRepo.Table().FirstOrDefault(o => o.UserName == userName); if (user == null) { key = nameof(model.UserName); message = "用户不存在"; } else { var maxAccessFailedCount = Convert.ToInt32(this._settingService.GetSetting("MaxFailedAccessAttemptsBeforeLockout").Value); var lockoutEndMinutes = Convert.ToInt32(this._settingService.GetSetting("DefaultAccountLockoutMinutes").Value); if (user.LockoutEnabled)//对已启用登录锁定的用户,如果当前登录时间超出锁定时间,先解除锁定状态 { if (user.LockoutEnd.HasValue && DateTime.UtcNow > user.LockoutEnd) { user.LockoutEnd = null; user.AccessFailedCount = 0; this._userRepo.SaveChanges(); } } var success = false; if (user.LockoutEnabled)//对启用登录锁定的用户进行验证 { if (user.LockoutEnd.HasValue == false) { if (user.PasswordHash == this._encryptionService.CreatePasswordHash(password, user.SecurityStamp)) { user.LockoutEnd = null; user.AccessFailedCount = 0; success = true; } else { user.AccessFailedCount += 1; if (user.AccessFailedCount >= maxAccessFailedCount) { user.LockoutEnd = DateTime.UtcNow.AddMinutes(lockoutEndMinutes); } } this._userRepo.SaveChanges(); } } else//对未启用登录锁定的用户进行验证 { if (user.PasswordHash == this._encryptionService.CreatePasswordHash(password, user.SecurityStamp)) { success = true; } } if (success) { HttpContext.JwtSignIn(model.UserName, model.RememberMe, _cfg); if (string.IsNullOrEmpty(returnUrl)) { returnUrl = Url.Action("Index", "Home"); } ViewBag.Url = returnUrl; var urls = new List(); var list = this._siteRepo.ReadOnlyTable().ToList(); foreach (var site in list) { var username = user.UserName; var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(); var sign = string.Concat(userName, timestamp, site.Key).Md5(); var url = site.Login .SetParam(nameof(username), username) .SetParam(nameof(user.NickName), user.NickName) .SetParam(nameof(user.Avatar), user.Avatar) .SetParam(nameof(timestamp), timestamp) .SetParam("rememberme", model.RememberMe) .SetParam(nameof(sign), sign); urls.Add(url); } Response.Headers.Remove("Location"); return View("JsonpLogin", urls); } else { if (user.LockoutEnabled && user.LockoutEnd.HasValue) { key = nameof(model.UserName); message = $"用户被锁定,请于{user.LockoutEnd.Value.ToLocalTime():HH:mm}后重试"; } else { key = nameof(model.Password); message = "密码错误"; } } } } catch (DbUpdateException ex) { ex.PrintStack(); message = ex.Message; } } ModelState.AddModelError(key, message); ViewData["ReturnUrl"] = returnUrl; return View(model); } #endregion 登录 #region 注册 [AllowAnonymous] public JsonResult UserNameNotUsed([Required] string userName) { if (ModelState.IsValid) { return Json(!this._userRepo.ReadOnlyTable().Any(o => o.UserName == userName)); } return Json("用户名不能为空"); } #endregion 注册 #region 邮箱注册 [AllowAnonymous] public IActionResult Register(string returnUrl = null) { if (this.RegisterDisabled()) { return RedirectTo("Index", "Home", "当前未开放注册"); } if (User.Identity.IsAuthenticated) { return RedirectTo("Index", "Home", "当前已登录,请退出"); } ViewData["ReturnUrl"] = returnUrl; return View(); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public IActionResult Register(RegisterModel model, string returnUrl = null) { if (this._cfg.GetValue("RegisterDisabled")) { return RedirectTo("Index", "Home", "当前未开放注册"); } if (ModelState.IsValid) { try { var user = new User().From(model); user.SecurityStamp = this._encryptionService.CreateSalt(); user.PasswordHash = this._encryptionService.CreatePasswordHash(model.Password, user.SecurityStamp); user.PasswordConfirmed = true; user.EmailConfirmed = true; this._userRepo.Add(user); this._userRepo.SaveChanges(); if (returnUrl == null) { returnUrl = Url.Action("Index", "Home"); } return RedirectTo("Login", "Account", "注册成功", new { returnUrl }); } catch (Exception ex) { ex.PrintStack(); ModelState.AddModelError("", ex.Message); } } return View(model); } [AllowAnonymous] public JsonResult EmailNotUsed([Required] string email) { if (ModelState.IsValid) { return Json(!this._userRepo.ReadOnlyTable().Any(o => o.Email == email)); } return Json("邮箱不能为空"); } [AllowAnonymous] public JsonResult SendCodeToEmail([Required] string email) { if (ModelState.IsValid) { try { string code = this.GetRandomCode(); this.SendCodeToEmail(email, code); var seconds = this.GetCaptchaSeconds(); this.HttpContext.Session.Set(CodeCaptchaModel.Key, new CodeCaptchaModel { ExpireDateUtc = DateTime.UtcNow.AddSeconds(seconds), MaxErrorLimit = 5, Captcha = code }); return Json(new { success = true, data = seconds }); } catch (Exception ex) { ex.PrintStack(); return Json(new { success = false, data = ex.Message }); } } return Json(new { success = false, data = "邮箱不能为空" }); } #endregion 邮箱注册 #region 手机号注册 [AllowAnonymous] public IActionResult RegisterByPhoneNumber(string returnUrl = null) { if (this._cfg.GetValue("RegisterDisabled")) { return RedirectTo("Index", "Home", "当前未开放注册"); } if (User.Identity.IsAuthenticated) { return RedirectTo("Index", "Home", "当前已登录,请退出后再注册"); } ViewData["ReturnUrl"] = returnUrl; return View(); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public IActionResult RegisterByPhoneNumber(RegisterPhoneNumberModel model, string returnUrl = null) { if (this._cfg.GetValue("RegisterDisabled")) { return RedirectTo("Index", "Home", "当前未开放注册"); } if (ModelState.IsValid) { try { var user = new User().From(model); user.SecurityStamp = this._encryptionService.CreateSalt(); user.PasswordHash = this._encryptionService.CreatePasswordHash(model.Password, user.SecurityStamp); user.PasswordConfirmed = true; user.PhoneNumberConfirmed = true; this._userRepo.Add(user); this._userRepo.SaveChanges(); if (returnUrl == null) { returnUrl = Url.Action("Index", "Home"); } return RedirectTo("Login", "Account", "注册成功", new { returnUrl }); } catch (DbUpdateException ex) { ex.PrintStack(); ModelState.AddModelError("", ex.Message); } } return View(model); } [AllowAnonymous] public JsonResult PhoneNumberNotUsed([Required] string phoneNumber) { if (ModelState.IsValid) { return Json(!this._userRepo.ReadOnlyTable().Any(o => o.PhoneNumber == phoneNumber)); } return Json("邮箱不能为空"); } [AllowAnonymous] public JsonResult SendCodeToPhoneNumber([Required] string phoneNumber) { if (ModelState.IsValid) { try { this._smsSender.Send(phoneNumber, out string code); var seconds = this.GetCaptchaSeconds(); this.HttpContext.Session.Set(CodeCaptchaModel.Key, new CodeCaptchaModel { ExpireDateUtc = DateTime.UtcNow.AddSeconds(seconds), MaxErrorLimit = 5, Captcha = code }); return Json(new { success = true, data = seconds }); } catch (Exception ex) { ex.PrintStack(); return Json(new { success = false, data = ex.Message }); } } return Json(new { success = false, data = "手机号不能为空" }); } #endregion 手机号注册 #region 重设密码 [AllowAnonymous] public IActionResult ForgotPassword() { if (User.Identity.IsAuthenticated) { return RedirectTo("Index", "Home", "当前已登录,请退出后重试"); } return View(); } [HttpPost] [AllowAnonymous] public IActionResult ForgotPassword(ResetPasswordModel model) { if (ModelState.IsValid) { if (this._userRepo.ReadOnlyTable().Any(o => o.UserName == model.UserName)) { this.HttpContext.Session.Remove(ResetPasswordModel.Key); return RedirectTo(nameof(Login), rawMesage: "密码修改成功,请重新登录"); } ModelState.AddModelError(nameof(model.UserName), "用户名不存在"); } this.HttpContext.Session.Remove(ResetPasswordModel.Key); return RedirectTo(nameof(ForgotPassword), rawMesage: "请重试"); } [AllowAnonymous] public JsonResult HasUser([Required] string userName) { if (ModelState.IsValid) { var user = this._userRepo.ReadOnlyTable().FirstOrDefault(o => o.UserName == userName); if (user != null) { var list = new List(); if (user.EmailConfirmed) { list.Add(new SelectListItem { Text = Regex.Replace(user.Email, "...@", "***@"), Value = Url.Action(nameof(SendCodeToEmailForResetPassword)) }); } var smsEnabled = Convert.ToBoolean(this._settingService.GetSetting("sms").Value); if (smsEnabled && user.PhoneNumberConfirmed) { list.Add(new SelectListItem { Text = Regex.Replace(user.PhoneNumber, "...$", "***"), Value = Url.Action(nameof(SendCodeToPhoneNumberForResetPassword)) }); } var selectList = new SelectList(list, "Value", "Text"); HttpContext.Response.Headers.Add("x-model", JsonConvert.SerializeObject(selectList)); HttpContext.Session.Set(ResetPasswordModel.Key, new ResetPasswordModel { UserName = userName }); HttpContext.Session.Remove(CodeCaptchaModel.Key); return Json(true); } return Json("用户名不存在"); } return Json("用户名不能为空"); } [AllowAnonymous] public JsonResult SendCodeToEmailForResetPassword() { var model = HttpContext.Session.Get(ResetPasswordModel.Key); if (model != null) { try { var user = this._userRepo.ReadOnlyTable().FirstOrDefault(o => o.UserName == model.UserName); var email = user.Email; string code = this.GetRandomCode(); this.SendCodeToEmail(email, code); var seconds = this.GetCaptchaSeconds(); this.HttpContext.Session.Set(CodeCaptchaModel.Key, new CodeCaptchaModel { ExpireDateUtc = DateTime.UtcNow.AddSeconds(seconds), MaxErrorLimit = 5, Captcha = code }); return Json(new { success = true, data = seconds }); } catch (Exception ex) { ex.PrintStack(); return Json(new { success = false, data = ex.Message }); } } return Json(new { success = false, data = "会话已过期,请刷新页面" }); } [AllowAnonymous] public JsonResult SendCodeToPhoneNumberForResetPassword() { var model = HttpContext.Session.Get(ResetPasswordModel.Key); if (model == null) { try { var user = this._userRepo.ReadOnlyTable().FirstOrDefault(o => o.UserName == model.UserName); var phoneNumber = user.PhoneNumber; this._smsSender.Send(phoneNumber, out string code); var seconds = this.GetCaptchaSeconds(); this.HttpContext.Session.Set(CodeCaptchaModel.Key, new CodeCaptchaModel { ExpireDateUtc = DateTime.UtcNow.AddSeconds(seconds), MaxErrorLimit = 5, Captcha = code }); return Json(new { success = true, data = seconds }); } catch (Exception ex) { ex.PrintStack(); return Json(new { success = false, data = ex.Message }); } } return Json(new { success = false, data = "会话已过期,请刷新页面" }); } #endregion 重设密码 #region 修改密码 public IActionResult ChangePassword() { return View(); } [HttpPost] [ValidateAntiForgeryToken] public IActionResult ChangePassword(ChangePasswordModel model) { if (ModelState.IsValid) { try { var user = this._userRepo.Table().FirstOrDefault(o => o.UserName == User.Identity.Name); if (user.PasswordHash == this._encryptionService.CreatePasswordHash(model.OldPassword, user.SecurityStamp)) { user.PasswordHash = this._encryptionService.CreatePasswordHash(model.NewPassword, user.SecurityStamp); this._userRepo.SaveChanges(); return RedirectTo(rawMesage: "密码修改成功"); } else { ModelState.AddModelError(nameof(model.OldPassword), "当前密码输入错误"); } } catch (DbUpdateException ex) { ex.PrintStack(); ModelState.AddModelError("", ex.Message); } } return View(model); } #endregion 修改密码 #region 用户信息 public IActionResult UserInfo() { var user = this._userRepo.ReadOnlyTable().FirstOrDefault(o => o.UserName == User.Identity.Name); var model = user.To(); return View(model); } [HttpPost] public IActionResult UserInfo(EditUserInfoModel model) { try { var usreName = User.Identity.Name; var user = this._userRepo.Table().FirstOrDefault(o => o.UserName == usreName); user.From(model); this._userRepo.SaveChanges(); return RedirectTo(rawMesage: "基本信息修改成功"); } catch (Exception ex) { ex.PrintStack(); ModelState.AddModelError("", ex.Message); } return View(model); } #endregion 用户信息 #region 账户安全 public IActionResult Security() { return View(this._userRepo.Table().FirstOrDefault(o => o.UserName == User.Identity.Name)); } #endregion 账户安全 #region 设置修改邮箱 public IActionResult SetEmail() { var userName = User.Identity.Name; var user = this._userRepo.ReadOnlyTable().FirstOrDefault(o => o.UserName == userName); return Validate(user); } [HttpPost] public IActionResult SetEmail(ValidateModel model) { if (ModelState.IsValid) { ModelState.Clear(); ViewData["HtmlAction"] = Url.Action("SetEmail2"); return View(); } return RedirectTo("ChangeEmail", rawMesage: "请重试"); } [HttpPost] public IActionResult SetEmail2(ChangeEmailModel model) { if (ModelState.IsValid) { var userName = User.Identity.Name; var user = this._userRepo.Table().FirstOrDefault(o => o.UserName == userName); user.Email = model.Email; user.EmailConfirmed = true; this._userRepo.SaveChanges(); return RedirectTo("Security", rawMesage: "设置邮箱成功"); } return RedirectTo("ChangeEmail", rawMesage: "请重试"); } public IActionResult ChangeEmail() { var userName = User.Identity.Name; var user = this._userRepo.ReadOnlyTable().FirstOrDefault(o => o.UserName == userName); if (!user.EmailConfirmed) { return RedirectTo("SetEmail", rawMesage: "尚未设置邮箱"); } return Validate(user); } [HttpPost] public IActionResult ChangeEmail(ValidateModel model) { if (ModelState.IsValid) { ModelState.Clear(); ViewData["HtmlAction"] = Url.Action("ChangeEmail2"); return View(); } return RedirectTo("ChangeEmail", rawMesage: "请重试"); } [HttpPost] public IActionResult ChangeEmail2(ChangeEmailModel model) { if (ModelState.IsValid) { var userName = User.Identity.Name; var user = this._userRepo.Table().FirstOrDefault(o => o.UserName == userName); user.Email = model.Email; this._userRepo.SaveChanges(); return RedirectTo("Security", rawMesage: "更换邮箱成功"); } return RedirectTo("ChangeEmail", rawMesage: "请重试"); } #endregion 设置修改邮箱 #region 设置修改手机号 public IActionResult SetPhoneNumber() { var userName = User.Identity.Name; var user = this._userRepo.ReadOnlyTable().FirstOrDefault(o => o.UserName == userName); return Validate(user); } [HttpPost] public IActionResult SetPhoneNumber(ValidateModel model) { if (ModelState.IsValid) { ModelState.Clear(); ViewData["HtmlAction"] = Url.Action("SetPhoneNumber2"); return View(); } return RedirectTo("ChangePhoneNumber", rawMesage: "请重试"); } [HttpPost] public IActionResult SetPhoneNumber2(ChangePhoneNumberModel model) { if (ModelState.IsValid) { var userName = User.Identity.Name; var user = this._userRepo.Table().FirstOrDefault(o => o.UserName == userName); user.PhoneNumber = model.PhoneNumber; user.PhoneNumberConfirmed = true; this._userRepo.SaveChanges(); return RedirectTo("Security", rawMesage: "设置手机号成功"); } return RedirectTo("ChangePhoneNumber", rawMesage: "请重试"); } public IActionResult ChangePhoneNumber() { var userName = User.Identity.Name; var user = this._userRepo.ReadOnlyTable().FirstOrDefault(o => o.UserName == userName); if (!user.PhoneNumberConfirmed) { return RedirectTo("SetPhoneNumber", rawMesage: "尚未设置手机号"); } return Validate(user); } [HttpPost] public IActionResult ChangePhoneNumber(ValidateModel model) { if (ModelState.IsValid) { ModelState.Clear(); ViewData["HtmlAction"] = Url.Action("ChangePhoneNumber2"); return View(); } return RedirectTo("ChangePhoneNumber", rawMesage: "请重试"); } [HttpPost] public IActionResult ChangePhoneNumber2(ChangePhoneNumberModel model) { if (ModelState.IsValid) { var userName = User.Identity.Name; var user = this._userRepo.Table().FirstOrDefault(o => o.UserName == userName); user.PhoneNumber = model.PhoneNumber; this._userRepo.SaveChanges(); return RedirectTo("Security", rawMesage: "更换手机号成功"); } return RedirectTo("ChangePhoneNumber", rawMesage: "请重试"); } #endregion 设置修改手机号 #region 安全验证 public JsonResult SendCodeToEmailForValid() { try { var userName = User.Identity.Name; var user = this._userRepo.ReadOnlyTable().FirstOrDefault(o => o.UserName == userName); var email = user.Email; string code = this.GetRandomCode(); this.SendCodeToEmail(email, code); var seconds = this.GetCaptchaSeconds(); this.HttpContext.Session.Set(CodeCaptchaModel.Key, new CodeCaptchaModel { ExpireDateUtc = DateTime.UtcNow.AddSeconds(seconds), MaxErrorLimit = 5, Captcha = code }); return Json(new { success = true, data = seconds }); } catch (Exception ex) { ex.PrintStack(); return Json(new { success = false, data = ex.Message }); } } public JsonResult SendCodeToPhoneNumberForValid() { try { var userName = User.Identity.Name; var user = this._userRepo.ReadOnlyTable().FirstOrDefault(o => o.UserName == userName); var phoneNumber = user.PhoneNumber; this._smsSender.Send(phoneNumber, out string code); var seconds = this.GetCaptchaSeconds(); this.HttpContext.Session.Set(CodeCaptchaModel.Key, new CodeCaptchaModel { ExpireDateUtc = DateTime.UtcNow.AddSeconds(seconds), MaxErrorLimit = 5, Captcha = code }); return Json(new { success = true, data = seconds }); } catch (Exception ex) { ex.PrintStack(); return Json(new { success = false, data = ex.Message }); } } private IActionResult Validate(User user) { var list = new List(); if (user.EmailConfirmed) { list.Add(new SelectListItem { Text = Regex.Replace(user.Email, "...@", "***@"), Value = Url.Action(nameof(SendCodeToEmailForValid)) }); } var smsEnabled = Convert.ToBoolean(this._settingService.GetSetting("sms").Value); if (smsEnabled && user.PhoneNumberConfirmed) { list.Add(new SelectListItem { Text = Regex.Replace(user.PhoneNumber, "...$", "***"), Value = Url.Action(nameof(SendCodeToPhoneNumberForValid)) }); } var selectList = new SelectList(list, "Value", "Text"); ViewData["TypeSelectList"] = selectList; return View("Validate"); } #endregion 安全验证 #region 权限不足 [AllowAnonymous] public IActionResult AccessDenied(string returnUrl) { return View(model: returnUrl); } #endregion 权限不足 [AllowAnonymous] public IActionResult ServerValid() { return Json(new { Name = this._cfg["name"], Version = this._cfg["version"] }); } //[AllowAnonymous] //[HttpPost] //public IActionResult FaceLogin(IFormFile face) //{ // using var stream = face.OpenReadStream(); // using var bitmap = new System.Drawing.Bitmap(stream); // Console.WriteLine($"face length:{face.Length}"); // var userName = this._frs.FindFace(bitmap); // if (!string.IsNullOrEmpty(userName)) // { // var user = this._userRepo.ReadOnlyTable().FirstOrDefault(o => o.UserName == userName); // if (user != null) // { // var list = this._siteRepo.ReadOnlyTable().ToList(); // var result = new // { // Code = 0, // Token = this._jwtHelper.GetToken(new Dictionary() { { nameof(user.UserName), user.UserName } }) // }; // return Json(result); // } // } // return Json(new { Code = 1 }); //} [AllowAnonymous] public IActionResult HasLogin() { Response.Headers["Content-Type"] = "application/javascript"; return Content($"var hasLogin={(User.Identity.IsAuthenticated ? "true" : "false")}"); } #region tools private void SendCodeToEmail(string email, string code) { var name = this._settingService.GetSetting("name").Value; this._emailSender.SendMail(name, this._settingService.GetSetting("email:user").Value, email, $"{name}注册验证码", $"{name} 验证码:{code}", this._settingService.GetSetting("email:host").Value, Convert.ToInt32(this._settingService.GetSetting("email:port").Value), this._settingService.GetSetting("email:user").Value, this._settingService.GetSetting("email:password").Value); } private string GetRandomCode(int length = 4) { var builder = new StringBuilder(); for (int i = 0; i < length; i++) { var random = new byte[1]; using var rg = RandomNumberGenerator.Create(); rg.GetBytes(random); builder.Append(new Random(Convert.ToInt32(random[0])).Next(0, 9)); } return builder.ToString(); } private bool RegisterDisabled() { return this._cfg.GetValue("RegisterDisabled"); } private int GetCaptchaSeconds() { return this._cfg.GetValue("CaptchaSeconds"); } #endregion tools } }