增加MFA认证
continuous-integration/drone/push Build is passing

This commit is contained in:
2024-06-11 17:58:06 +08:00
parent eb0f13c075
commit 25efb34f1f
9 changed files with 401 additions and 172 deletions
@@ -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,12 +10,16 @@ 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);
Task<IResponseOutput> UpdateUser(UserCommand model);
Task<IResponseOutput> UpdateUserState(Guid userId, UserStateEnum state);
//Task<IResponseOutput> SendVerificationCode(string emailOrPhone, VerifyType verificationType, bool isReviewer = false);
//Task<IResponseOutput> SetNewPassword(ResetPasswordCommand resetPwdModel);
}
@@ -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
{
@@ -22,17 +25,17 @@ namespace IRaCIS.Application.Services
public class UserService : BaseService, IUserService
{
private readonly IRepository<User> _userRepository;
private readonly IMailVerificationService _mailVerificationService;
private readonly IMailVerificationService _mailVerificationService;
private readonly IRepository<VerificationCode> _verificationCodeRepository;
private readonly IRepository<Doctor> _doctorRepository;
private readonly IRepository<TrialUser> _userTrialRepository;
private readonly IRepository<UserLog> _userLogRepository;
private readonly IRepository<UserPassWordLog> _userPassWordLogRepository;
private readonly IDistributedLockProvider _distributedLockProvider;
private readonly IRepository<UserPassWordLog> _userPassWordLogRepository;
private readonly IDistributedLockProvider _distributedLockProvider;
private readonly IEasyCachingProvider _cache;
private readonly IReadingImageTaskService _readingImageTaskService;
private readonly IOptionsMonitor<ServiceVerifyConfigOption> _verifyConfig;
private readonly IReadingImageTaskService _readingImageTaskService;
private readonly IOptionsMonitor<ServiceVerifyConfigOption> _verifyConfig;
public UserService(IRepository<User> userRepository,
@@ -41,20 +44,20 @@ namespace IRaCIS.Application.Services
IRepository<VerificationCode> verificationCodeRepository,
IRepository<Doctor> doctorRepository,
IEasyCachingProvider cache,
IReadingImageTaskService readingImageTaskService,
IRepository<TrialUser> userTrialRepository,
IReadingImageTaskService readingImageTaskService,
IRepository<TrialUser> userTrialRepository,
IOptionsMonitor<ServiceVerifyConfigOption> verifyConfig,
IRepository<UserLog> userLogRepository,
IRepository<UserPassWordLog> userPassWordLogRepository
IRepository<UserPassWordLog> userPassWordLogRepository
,
IDistributedLockProvider distributedLockProvider)
{
_userLogRepository = userLogRepository;
this._userPassWordLogRepository = userPassWordLogRepository;
_verifyConfig = verifyConfig;
this._userPassWordLogRepository = userPassWordLogRepository;
_verifyConfig = verifyConfig;
_cache = cache;
this._readingImageTaskService = readingImageTaskService;
_userRepository = userRepository;
this._readingImageTaskService = readingImageTaskService;
_userRepository = userRepository;
_mailVerificationService = mailVerificationService;
_verificationCodeRepository = verificationCodeRepository;
_doctorRepository = doctorRepository;
@@ -95,45 +98,45 @@ namespace IRaCIS.Application.Services
private async Task VerifyUserPwdAsync(Guid userId, string newPwd, string? oldPwd = null)
{
//var dbUser = (await _userRepository.FirstOrDefaultAsync(t => t.Id == userId)).IfNullThrowException();
//var dbUser = (await _userRepository.FirstOrDefaultAsync(t => t.Id == userId)).IfNullThrowException();
if (oldPwd != null && oldPwd == newPwd)
{
//---新密码与旧密码相同。
throw new BusinessValidationFailedException(_localizer["User_NewOldPwdSame"]);
}
if (oldPwd != null && oldPwd == newPwd)
{
//---新密码与旧密码相同。
throw new BusinessValidationFailedException(_localizer["User_NewOldPwdSame"]);
}
var dbUser = (await _userRepository.Where(t => t.Id == userId).FirstOrDefaultAsync()).IfNullThrowException();
var dbUser = (await _userRepository.Where(t => t.Id == userId).FirstOrDefaultAsync()).IfNullThrowException();
if (oldPwd != null && dbUser.Password != oldPwd)
{
//---旧密码验证失败。
throw new BusinessValidationFailedException(_localizer["User_OldPwdInvalid"]);
}
if (oldPwd != null && dbUser.Password != oldPwd)
{
//---旧密码验证失败。
throw new BusinessValidationFailedException(_localizer["User_OldPwdInvalid"]);
}
if (dbUser.Password == newPwd)
{
//---新密码与旧密码相同。
throw new BusinessValidationFailedException(_localizer["User_NewOldPwdSame"]);
}
if (dbUser.Password == newPwd)
{
//---新密码与旧密码相同。
throw new BusinessValidationFailedException(_localizer["User_NewOldPwdSame"]);
}
var passWordList = await _userPassWordLogRepository.Where(x => x.UserId == userId).OrderByDescending(x => x.CreateTime).Take(2).ToListAsync();
if (passWordList.Any(x => x.PassWord == newPwd))
{
throw new BusinessValidationFailedException(_localizer["User_PassWordRepeat"]);
}
throw new BusinessValidationFailedException(_localizer["User_PassWordRepeat"]);
}
if (oldPwd != null)
{
await _userPassWordLogRepository.AddAsync(new UserPassWordLog()
{
await _userPassWordLogRepository.AddAsync(new UserPassWordLog()
{
CreateTime = DateTime.Now,
PassWord = oldPwd,
UserId = userId,
});
}
CreateTime = DateTime.Now,
PassWord = oldPwd,
UserId = userId,
});
}
await _userRepository.BatchUpdateNoTrackingAsync(x => x.Id == userId, x => new User()
{
@@ -142,7 +145,7 @@ namespace IRaCIS.Application.Services
await _userPassWordLogRepository.SaveChangesAsync();
await Task.CompletedTask;
await Task.CompletedTask;
}
@@ -306,7 +309,7 @@ namespace IRaCIS.Application.Services
{
await _mailVerificationService.AdminResetPwdSendEmailAsync(userId, pwd);
}
catch (Exception )
catch (Exception)
{
//---请检查邮箱地址或者联系维护人员, 邮件发送失败, 未能创建账户成功
throw new BusinessValidationFailedException(_localizer["User_CreateFailed"]);
@@ -319,7 +322,7 @@ namespace IRaCIS.Application.Services
IsFirstAdd = true
});
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId=userId, OptType = UserOptType.ResetPassword }, true);
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId = userId, OptType = UserOptType.ResetPassword }, true);
return ResponseOutput.Ok();
}
@@ -403,7 +406,7 @@ namespace IRaCIS.Application.Services
}
}
var list = await _userRepository.Where(t => t.EMail == email && t.Status== UserStateEnum.Enable).Select(t => new UserAccountDto() { UserId = t.Id, UserName = t.UserName, UserRealName = t.FullName, UserType = t.UserTypeRole.UserTypeShortName }).ToListAsync();
var list = await _userRepository.Where(t => t.EMail == email && t.Status == UserStateEnum.Enable).Select(t => new UserAccountDto() { UserId = t.Id, UserName = t.UserName, UserRealName = t.FullName, UserType = t.UserTypeRole.UserTypeShortName }).ToListAsync();
@@ -431,7 +434,7 @@ namespace IRaCIS.Application.Services
IsFirstAdd = false
});
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = userId, OptUserId = userId,LoginPassword=newPwd, OptType = UserOptType.UnloginModifyPasswoed }, true);
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = userId, OptUserId = userId, LoginPassword = newPwd, OptType = UserOptType.UnloginModifyPasswoed }, true);
return ResponseOutput.Result(success);
@@ -467,7 +470,7 @@ namespace IRaCIS.Application.Services
IsFirstAdd = false
});
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId= _userInfo.Id, OptType = UserOptType.LoginModifyPassword }, true);
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId = _userInfo.Id, OptType = UserOptType.LoginModifyPassword }, true);
return ResponseOutput.Result(success);
@@ -555,11 +558,11 @@ namespace IRaCIS.Application.Services
var success = await _userRepository.SaveChangesAsync();
}
await _mailVerificationService.AddUserSendEmailAsync(saveItem.Id, userAddModel.BaseUrl, userAddModel.RouteUrl);
return ResponseOutput.Ok( new UserAddedReturnDTO { Id = saveItem.Id, UserCode = saveItem.UserCode });
return ResponseOutput.Ok(new UserAddedReturnDTO { Id = saveItem.Id, UserCode = saveItem.UserCode });
}
@@ -589,7 +592,7 @@ namespace IRaCIS.Application.Services
user.OrganizationName = AppSettings.DefaultInternalOrganizationName;
}
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId= model.Id , OptType = UserOptType.UpdateUser }, true);
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId = model.Id, OptType = UserOptType.UpdateUser }, true);
var success = await _userRepository.SaveChangesAsync();
@@ -611,7 +614,7 @@ namespace IRaCIS.Application.Services
return ResponseOutput.NotOk(_localizer["User_InProject"]);
}
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId= userId, OptType = UserOptType.DeleteUser }, true);
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId = userId, OptType = UserOptType.DeleteUser }, true);
var success = await _userRepository.BatchDeleteNoTrackingAsync(t => t.Id == userId);
@@ -629,7 +632,7 @@ namespace IRaCIS.Application.Services
public async Task<IResponseOutput> UpdateUserState(Guid userId, UserStateEnum state)
{
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId = userId, OptType = state==UserStateEnum.Enable? UserOptType.AccountEnable: UserOptType.AccountLocked }, true);
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId = userId, OptType = state == UserStateEnum.Enable ? UserOptType.AccountEnable : UserOptType.AccountLocked }, true);
var success = await _userRepository.BatchUpdateNoTrackingAsync(u => u.Id == userId, t => new User
{
@@ -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>
/// 用户登陆
@@ -690,14 +756,12 @@ namespace IRaCIS.Application.Services
failCount++;
_cache.Set(cacheKey, failCount, TimeSpan.FromMinutes(lockoutMinutes));
var errorPwdUserId = await _userRepository.Where(u => u.UserName==userName).Select(t=>t.Id).FirstOrDefaultAsync();
var errorPwdUserId = await _userRepository.Where(u => u.UserName == userName).Select(t => t.Id).FirstOrDefaultAsync();
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = errorPwdUserId, OptUserId = errorPwdUserId, LoginFaildName = userName, LoginPassword = password, OptType = UserOptType.AccountOrPasswordError }, true);
return ResponseOutput.NotOk(_localizer["User_CheckNameOrPw"], new LoginReturnDTO());
}
if (loginUser.Status == 0)
@@ -712,26 +776,26 @@ namespace IRaCIS.Application.Services
if (loginUser.LastChangePassWordTime != null && DateTime.Now.AddDays(-90) > loginUser.LastChangePassWordTime.Value)
{
loginUser.LoginState = 1;
}
}
//登录成功 清除缓存
_cache.Set(cacheKey, 0, TimeSpan.FromMinutes(lockoutMinutes));
if (loginUser.LastLoginIP != string.Empty)
if (loginUser.LastLoginIP != string.Empty)
{
// 与上一次IP不一致
if (loginUser.LastLoginIP != _userInfo.IP)
{
loginUser.LoginState = 2;
}
loginUser.LoginState = 2;
}
}
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = loginUser.Id, OptUserId = loginUser.Id, OptType = UserOptType.Login }, true);
}
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = loginUser.Id, OptUserId = loginUser.Id, OptType = UserOptType.Login }, true);
userLoginReturnModel.BasicInfo = loginUser;
@@ -743,21 +807,13 @@ namespace IRaCIS.Application.Services
});
}
}
await _userRepository.BatchUpdateNoTrackingAsync(x => x.Id == loginUser.Id, x => new User()
{
LastLoginIP = _userInfo.IP
});
// 登录 清除缓存
//_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));
await _userRepository.BatchUpdateNoTrackingAsync(x => x.Id == loginUser.Id, x => new User()
{
LastLoginIP = _userInfo.IP
});
return ResponseOutput.Ok(userLoginReturnModel);
@@ -766,12 +822,12 @@ namespace IRaCIS.Application.Services
[HttpPost]
public async Task<PageOutput<UserLogView>> GetUserLogList(UserLogQuery inQuery)
{
DateTime? trialCreateTime = inQuery.TrialId != null ?_repository.Where<Trial>(t=>t.Id==inQuery.TrialId).Select(t=>t.CreateTime).FirstOrDefault() : null;
DateTime? trialCreateTime = inQuery.TrialId != null ? _repository.Where<Trial>(t => t.Id == inQuery.TrialId).Select(t => t.CreateTime).FirstOrDefault() : null;
var userLogQueryable =
_userLogRepository
.WhereIf(inQuery.TrialId != null, t => t.LoginUser.UserTrials.Any(c => c.TrialId == inQuery.TrialId && (c.UserId == t.LoginUserId || c.UserId == t.OptUserId)))
.WhereIf(trialCreateTime != null, t => t.CreateTime>= trialCreateTime)
.WhereIf(trialCreateTime != null, t => t.CreateTime >= trialCreateTime)
.WhereIf(inQuery.OptType != null, t => t.OptType == inQuery.OptType)
.WhereIf(inQuery.UserId != null, t => t.LoginUserId == inQuery.UserId)
.WhereIf(inQuery.BeginDate != null, t => t.CreateTime >= inQuery.BeginDate)