增加MFA认证
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
eb0f13c075
commit
25efb34f1f
|
@ -30,6 +30,8 @@ using IRaCIS.Core.Application.Helper;
|
|||
using Microsoft.Extensions.Options;
|
||||
using IRaCIS.Core.Application.Contracts;
|
||||
using LoginReturnDTO = IRaCIS.Application.Contracts.LoginReturnDTO;
|
||||
using DocumentFormat.OpenXml.Spreadsheet;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
|
||||
namespace IRaCIS.Api.Controllers
|
||||
{
|
||||
|
@ -96,14 +98,54 @@ namespace IRaCIS.Api.Controllers
|
|||
/// <summary> 系统用户登录接口[New] </summary>
|
||||
[HttpPost, Route("user/login")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IResponseOutput<LoginReturnDTO>> Login(UserLoginDTO loginUser, [FromServices] IEasyCachingProvider provider, [FromServices] IUserService _userService,
|
||||
public async Task<IResponseOutput> Login(UserLoginDTO loginUser,
|
||||
[FromServices] IEasyCachingProvider provider,
|
||||
[FromServices] IUserService _userService,
|
||||
[FromServices] ITokenService _tokenService,
|
||||
|
||||
[FromServices] IReadingImageTaskService readingImageTaskService,
|
||||
[FromServices] IConfiguration configuration)
|
||||
IOptionsMonitor<ServiceVerifyConfigOption> _verifyConfig,
|
||||
IMailVerificationService _mailVerificationService)
|
||||
{
|
||||
|
||||
//MFA 邮箱验证 前端传递用户Id 和MFACode
|
||||
if (loginUser.UserId != null && !string.IsNullOrEmpty(loginUser.MFACode) && _verifyConfig.CurrentValue.OpenLoginMFA)
|
||||
{
|
||||
Guid userId = (Guid)loginUser.UserId;
|
||||
|
||||
//验证MFA 编码是否有问题
|
||||
|
||||
await _userService.VerifyMFACodeAsync(userId, loginUser.MFACode);
|
||||
|
||||
var basicInfo = await _userService.GetUserBasicInfo(userId);
|
||||
|
||||
var loginReturn = new LoginReturnDTO() { BasicInfo = basicInfo };
|
||||
|
||||
loginReturn.JWTStr = _tokenService.GetToken(IRaCISClaims.Create(loginReturn.BasicInfo));
|
||||
|
||||
|
||||
// 创建一个 CookieOptions 对象,用于设置 Cookie 的属性
|
||||
var option = new CookieOptions
|
||||
{
|
||||
Expires = DateTime.Now.AddMonths(1), // 设置过期时间为 30 分钟之后
|
||||
HttpOnly = false, // 确保 cookie 只能通过 HTTP 访问
|
||||
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None, // 设置 SameSite 属性
|
||||
Secure = false // 确保 cookie 只能通过 HTTPS 访问
|
||||
};
|
||||
|
||||
HttpContext.Response.Cookies.Append("access_token", loginReturn.JWTStr, option);
|
||||
|
||||
// 验证阅片休息时间
|
||||
await readingImageTaskService.ResetReadingRestTime(userId);
|
||||
|
||||
await provider.SetAsync(userId.ToString(), loginReturn.JWTStr, TimeSpan.FromDays(7));
|
||||
|
||||
await provider.SetAsync($"{userId.ToString()}_Online", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), TimeSpan.FromMinutes(_verifyConfig.CurrentValue.AutoLoginOutMinutes));
|
||||
|
||||
return ResponseOutput.Ok(loginReturn);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
var returnModel = await _userService.Login(loginUser.UserName, loginUser.Password);
|
||||
|
||||
if (returnModel.IsSuccess)
|
||||
|
@ -169,6 +211,45 @@ namespace IRaCIS.Api.Controllers
|
|||
|
||||
#endregion
|
||||
|
||||
var userId = returnModel.Data.BasicInfo.Id;
|
||||
|
||||
if (_verifyConfig.CurrentValue.OpenLoginMFA)
|
||||
{
|
||||
//发版屏蔽
|
||||
|
||||
returnModel.Data.JWTStr = _tokenService.GetToken(IRaCISClaims.Create(returnModel.Data.BasicInfo));
|
||||
|
||||
//MFA 发送邮件
|
||||
|
||||
returnModel.Data.IsMFA = true;
|
||||
|
||||
var email = returnModel.Data.BasicInfo.EMail;
|
||||
|
||||
#region 隐藏Email
|
||||
// 找到 "@" 符号的位置
|
||||
int atIndex = email.IndexOf('@');
|
||||
|
||||
// 替换 "@" 符号前的中间两位为星号
|
||||
string visiblePart = email.Substring(0, atIndex);
|
||||
|
||||
int startIndex = (visiblePart.Length - 2) / 2;
|
||||
|
||||
// 替换中间两位字符为星号
|
||||
string hiddenPartBeforeAt = visiblePart.Substring(0, startIndex) + "**" + visiblePart.Substring(startIndex + 2);
|
||||
|
||||
string afterAt = email.Substring(atIndex + 1);
|
||||
|
||||
// 组合隐藏和可见部分
|
||||
string hiddenEmail = hiddenPartBeforeAt + "@" + afterAt;
|
||||
#endregion
|
||||
|
||||
returnModel.Data.BasicInfo.EMail = hiddenEmail;
|
||||
|
||||
await _userService.SendMFAEmail(userId);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
returnModel.Data.JWTStr = _tokenService.GetToken(IRaCISClaims.Create(returnModel.Data.BasicInfo));
|
||||
|
||||
// 创建一个 CookieOptions 对象,用于设置 Cookie 的属性
|
||||
|
@ -182,16 +263,28 @@ namespace IRaCIS.Api.Controllers
|
|||
|
||||
HttpContext.Response.Cookies.Append("access_token", returnModel.Data.JWTStr, option);
|
||||
|
||||
}
|
||||
|
||||
var userId = returnModel.Data.BasicInfo.Id.ToString();
|
||||
//provider.Set(userId, userId, TimeSpan.FromMinutes(AppSettings.LoginExpiredTimeSpan));
|
||||
|
||||
// 验证阅片休息时间
|
||||
await readingImageTaskService.ResetReadingRestTime(returnModel.Data.BasicInfo.Id);
|
||||
|
||||
await provider.SetAsync(userId.ToString(), returnModel.Data.JWTStr, TimeSpan.FromDays(7));
|
||||
return returnModel;
|
||||
|
||||
await provider.SetAsync($"{userId.ToString()}_Online", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), TimeSpan.FromMinutes(_verifyConfig.CurrentValue.AutoLoginOutMinutes));
|
||||
}
|
||||
|
||||
}
|
||||
return returnModel;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[HttpGet, Route("imageShare/ShareImage")]
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<param name="doctorId"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:IRaCIS.Api.Controllers.ExtraController.Login(IRaCIS.Application.Contracts.UserLoginDTO,EasyCaching.Core.IEasyCachingProvider,IRaCIS.Application.Services.IUserService,IRaCIS.Core.Application.Auth.ITokenService,IRaCIS.Core.Application.Contracts.IReadingImageTaskService,Microsoft.Extensions.Configuration.IConfiguration)">
|
||||
<member name="M:IRaCIS.Api.Controllers.ExtraController.Login(IRaCIS.Application.Contracts.UserLoginDTO,EasyCaching.Core.IEasyCachingProvider,IRaCIS.Application.Services.IUserService,IRaCIS.Core.Application.Auth.ITokenService,IRaCIS.Core.Application.Contracts.IReadingImageTaskService,Microsoft.Extensions.Options.IOptionsMonitor{IRaCIS.Core.Domain.Share.ServiceVerifyConfigOption},IRaCIS.Application.Services.IMailVerificationService)">
|
||||
<summary> 系统用户登录接口[New] </summary>
|
||||
</member>
|
||||
<member name="M:IRaCIS.Core.API.Controllers.Special.FinancialChangeController.AddOrUpdateTrialInspection(IRaCIS.Core.Application.Service.Inspection.DTO.DataInspectionDto{IRaCIS.Application.Contracts.TrialCommand})">
|
||||
|
|
|
@ -60,7 +60,10 @@
|
|||
"LoginMaxFailCount": 5,
|
||||
|
||||
"LoginFailLockMinutes": 1,
|
||||
"AutoLoginOutMinutes": 1
|
||||
|
||||
"AutoLoginOutMinutes": 1,
|
||||
|
||||
"OpenLoginMFA": true
|
||||
},
|
||||
|
||||
"SystemEmailSendConfig": {
|
||||
|
|
|
@ -21,6 +21,8 @@ namespace IRaCIS.Application.Services
|
|||
|
||||
Task SiteSurveyRejectEmail(MimeMessage messageToSend);
|
||||
|
||||
Task SenMFAVerifyEmail(Guid userId, string userName, string emailAddress, int verificationCode);
|
||||
|
||||
Task SendMailEditEmail(Guid userId, string userName, string emailAddress, int verificationCode);
|
||||
|
||||
Task AnolymousSendEmailForResetAccount(string emailAddress, int verificationCode);
|
||||
|
@ -91,6 +93,66 @@ namespace IRaCIS.Application.Services
|
|||
return str;
|
||||
}
|
||||
|
||||
//MFA
|
||||
public async Task SenMFAVerifyEmail(Guid userId, string userName, string emailAddress, int verificationCode)
|
||||
{
|
||||
var messageToSend = new MimeMessage();
|
||||
//发件地址
|
||||
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
|
||||
//收件地址
|
||||
messageToSend.To.Add(new MailboxAddress(userName, emailAddress));
|
||||
//主题
|
||||
//---[来自{0}] 关于MFA邮箱验证的提醒
|
||||
messageToSend.Subject = _localizer["Mail_EmailMFATopic", _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN];
|
||||
|
||||
var builder = new BodyBuilder();
|
||||
|
||||
|
||||
var pathToFile = _hostEnvironment.WebRootPath
|
||||
+ Path.DirectorySeparatorChar.ToString()
|
||||
+ "EmailTemplate"
|
||||
+ Path.DirectorySeparatorChar.ToString()
|
||||
//+ "UserOptCommon.html";
|
||||
+ (_userInfo.IsEn_Us ? "UserOptCommon_US.html" : "UserOptCommon.html");
|
||||
|
||||
using (StreamReader SourceReader = System.IO.File.OpenText(pathToFile))
|
||||
{
|
||||
var templateInfo = SourceReader.ReadToEnd();
|
||||
|
||||
|
||||
builder.HtmlBody = string.Format(ReplaceCompanyName(templateInfo),
|
||||
|
||||
userName,
|
||||
_localizer["Mail_MFAEmail"],
|
||||
verificationCode
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
messageToSend.Body = builder.ToMessageBody();
|
||||
|
||||
|
||||
|
||||
EventHandler<MessageSentEventArgs> sucessHandle = (sender, args) =>
|
||||
{
|
||||
// args.Response
|
||||
var code = verificationCode.ToString();
|
||||
_ = _verificationCodeRepository.AddAsync(new VerificationCode()
|
||||
{
|
||||
CodeType = 0,
|
||||
HasSend = true,
|
||||
Code = code,
|
||||
UserId = userId,
|
||||
ExpirationTime = DateTime.Now.AddMinutes(3)
|
||||
}).Result;
|
||||
_ = _verificationCodeRepository.SaveChangesAsync().Result;
|
||||
|
||||
};
|
||||
|
||||
|
||||
await SendEmailHelper.SendEmailAsync(messageToSend, _systemEmailConfig, sucessHandle);
|
||||
}
|
||||
|
||||
//重置邮箱
|
||||
public async Task SendMailEditEmail(Guid userId, string userName, string emailAddress, int verificationCode)
|
||||
{
|
||||
|
|
|
@ -21,6 +21,9 @@ namespace IRaCIS.Application.Contracts
|
|||
{
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
public Guid? UserId { get; set; }
|
||||
public string MFACode { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class LoginReturnDTO
|
||||
|
@ -28,6 +31,8 @@ namespace IRaCIS.Application.Contracts
|
|||
public UserBasicInfo BasicInfo { get; set; } = new UserBasicInfo();
|
||||
public string JWTStr { get; set; }=string.Empty;
|
||||
|
||||
public bool IsMFA { get; set; } = false;
|
||||
|
||||
}
|
||||
|
||||
public class UserBasicInfo
|
||||
|
@ -59,7 +64,7 @@ namespace IRaCIS.Application.Contracts
|
|||
|
||||
public string PermissionStr { get; set; } = String.Empty;
|
||||
|
||||
|
||||
public string EMail { get; set; } = string.Empty;
|
||||
public bool IsFirstAdd { get; set; }
|
||||
public bool IsReviewer { get; set; } = false;
|
||||
|
||||
|
|
|
@ -10,6 +10,10 @@ namespace IRaCIS.Application.Services
|
|||
Task<UserDetailDTO> GetUser(Guid id);
|
||||
Task<PageOutput<UserListDTO>> GetUserList(UserListQueryDTO param);
|
||||
Task<IResponseOutput<LoginReturnDTO>> Login(string userName, string password);
|
||||
Task<IResponseOutput> VerifyMFACodeAsync(Guid userId, string Code);
|
||||
|
||||
Task<IResponseOutput> SendMFAEmail(Guid userId);
|
||||
Task<UserBasicInfo> GetUserBasicInfo(Guid userId);
|
||||
Task<IResponseOutput> ModifyPassword(EditPasswordCommand editPwModel);
|
||||
Task<IResponseOutput> ResetPassword(Guid userId);
|
||||
|
||||
|
|
|
@ -15,6 +15,9 @@ using Medallion.Threading;
|
|||
using EasyCaching.Core;
|
||||
using IRaCIS.Core.Application.Contracts;
|
||||
using LoginReturnDTO = IRaCIS.Application.Contracts.LoginReturnDTO;
|
||||
using IRaCIS.Core.Application.Auth;
|
||||
using BeetleX.Redis.Commands;
|
||||
using IRaCIS.Core.Domain.Models;
|
||||
|
||||
namespace IRaCIS.Application.Services
|
||||
{
|
||||
|
@ -639,7 +642,70 @@ namespace IRaCIS.Application.Services
|
|||
}
|
||||
|
||||
|
||||
public async Task<UserBasicInfo> GetUserBasicInfo(Guid userId)
|
||||
{
|
||||
var info = await _userRepository.Where(u => u.Id == userId).ProjectTo<UserBasicInfo>(_mapper.ConfigurationProvider).FirstNotNullAsync();
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送MFA 验证邮件
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
public async Task<IResponseOutput> SendMFAEmail(Guid userId)
|
||||
{
|
||||
var userInfo = await _userRepository.Where(u => u.Id == userId).Select(t => new { t.FullName, t.EMail }).FirstOrDefaultAsync();
|
||||
|
||||
int verificationCode = new Random().Next(100000, 1000000);
|
||||
|
||||
await _mailVerificationService.SenMFAVerifyEmail(userId, userInfo.FullName, userInfo.EMail, verificationCode);
|
||||
|
||||
return ResponseOutput.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证MFA 邮件
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="Code"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="BusinessValidationFailedException"></exception>
|
||||
[AllowAnonymous]
|
||||
public async Task<IResponseOutput> VerifyMFACodeAsync(Guid userId, string Code)
|
||||
{
|
||||
var verificationRecord = await _repository.GetQueryable<VerificationCode>().OrderByDescending(x => x.ExpirationTime).Where(t => t.UserId == userId && t.Code == Code && t.CodeType == VerifyType.Email).FirstOrDefaultAsync();
|
||||
VerifyEmialGetDoctorInfoOutDto result = new VerifyEmialGetDoctorInfoOutDto();
|
||||
|
||||
//检查数据库是否存在该验证码
|
||||
if (verificationRecord == null)
|
||||
{
|
||||
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = userId, OptUserId = userId, OptType = UserOptType.MFALoginFail }, true);
|
||||
//---验证码错误。
|
||||
throw new BusinessValidationFailedException(_localizer["TrialSiteSurvey_WrongVerificationCode"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
//检查验证码是否失效
|
||||
if (verificationRecord.ExpirationTime < DateTime.Now)
|
||||
{
|
||||
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = userId, OptUserId = userId, OptType = UserOptType.MFALoginFail }, true);
|
||||
//---验证码已经过期。
|
||||
throw new BusinessValidationFailedException(_localizer["TrialSiteSurvey_ExpiredVerificationCode"]);
|
||||
|
||||
|
||||
}
|
||||
else //验证码正确 并且 没有超时
|
||||
{
|
||||
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = userId, OptUserId = userId, OptType = UserOptType.MFALogin }, true);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseOutput.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户登陆
|
||||
|
@ -696,8 +762,6 @@ namespace IRaCIS.Application.Services
|
|||
|
||||
return ResponseOutput.NotOk(_localizer["User_CheckNameOrPw"], new LoginReturnDTO());
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (loginUser.Status == 0)
|
||||
|
@ -751,14 +815,6 @@ namespace IRaCIS.Application.Services
|
|||
|
||||
});
|
||||
|
||||
|
||||
// 登录 清除缓存
|
||||
//_cache.Remove(userLoginReturnModel.BasicInfo.Id.ToString());
|
||||
|
||||
var userId = loginUser.Id;
|
||||
await _cache.SetAsync($"{userId.ToString()}_Online", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), TimeSpan.FromMinutes(_verifyConfig.CurrentValue.AutoLoginOutMinutes));
|
||||
|
||||
|
||||
return ResponseOutput.Ok(userLoginReturnModel);
|
||||
|
||||
}
|
||||
|
|
|
@ -93,7 +93,11 @@ namespace IRaCIS.Core.Domain.Models
|
|||
|
||||
DeleteUser=10,
|
||||
|
||||
UpdateUser=11
|
||||
UpdateUser=11,
|
||||
|
||||
MFALogin=12,
|
||||
|
||||
MFALoginFail=13,
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ namespace IRaCIS.Core.Domain.Share
|
|||
public int LoginFailLockMinutes { get; set; }
|
||||
|
||||
public int AutoLoginOutMinutes { get; set; }
|
||||
|
||||
public bool OpenLoginMFA { get; set; }
|
||||
}
|
||||
|
||||
public class SystemEmailSendConfig
|
||||
|
|
Loading…
Reference in New Issue