CloudBuilder/CloudBuilder.Security.Service/AuthService.cs
owenchen 1c0c83af5f ow
2026-05-12 10:09:31 +08:00

489 lines
24 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 Azure.Core;
using CloudBuilder.Core.Authorization;
using CloudBuilder.Core.DatabaseAccessor;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.EventBuses;
using CloudBuilder.Core.DependencyInjection.Request;
using CloudBuilder.Core.Policy;
using CloudBuilder.Core.Service;
using CloudBuilder.Security.Data;
using CloudBuilder.Security.Entity;
using CloudBuilder.Security.Policy;
using CloudBuilder.Security.Publisher;
using CloudBuilder.Security.Service.Authorization;
using CloudBuilder.Security.Service.Publisher;
using DocumentFormat.OpenXml.Spreadsheet;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Runtime;
using System.Text;
using System.Text.RegularExpressions;
namespace CloudBuilder.Security.Service
{
public class AuthService : IAuthService
{
private readonly IApplicationService service;
private readonly IHttpContextAccessor httpContextAccessor;
private readonly ILocaleMessageManager localeMessageManager;
private readonly IContext context;
private IRepository<SecurityUserEntity> repository;
public AuthService(IApplicationService service, IHttpContextAccessor httpContextAccessor, ILocaleMessageManager localeMessageManager, IContext context)
{
this.service = service;
this.httpContextAccessor = httpContextAccessor;
this.localeMessageManager = localeMessageManager;
this.context = context;
repository = service.GetRepository<IRepository<SecurityUserEntity>>();
}
[AllowAnonymous]
[UnitOfWork]
public LoginProfile Login(LoginData data)
{
context.SetCulture(data.Culture);
string certification = localeMessageManager.ComposeMessage(typeof(AuthService).Name, "certification");
SecurityUserEntity securityUserEntity = repository.DetachedEntities.Where(x =>
(x.UserName == data.UserMobile || x.UserMobile == data.UserMobile)
&& x.UserIsValidIndc == YesNoPolicy.YES).FirstOrDefault()!;
if (securityUserEntity == null)
{
throw new ValidatedException(certification);
}
var descKey = service.Configuration[SecuritySettingsPolicy.SecuritySettings_DescKey];
var privateKey = service.Configuration[SecuritySettingsPolicy.SecuritySettings_RsaPrivateKey];
var rsa = new RSA(privateKey, true);
var psd = rsa.DecodeOrNull(data.RsaUserPassword);
if (string.IsNullOrEmpty(psd)) throw new ValidatedException(certification);
//052636a8-b146-9efd-f5dc-0034a3c5bb34+password
var guid = psd.Substring(0, 36);
if (guid != data.Guid)
{
throw new ValidatedException(certification);
}
if (securityUserEntity.IsAccountsLockedIndc == YesNoPolicy.YES && securityUserEntity.AccountsLockedDatetime.HasValue
&& securityUserEntity.AccountsLockedDatetime.Value.Subtract(DateTime.Now).TotalMinutes > 0)
{
throw new ValidatedException(localeMessageManager.ComposeMessage("AuthService", "locked"));
}
data.UserPassword = psd.Substring(36, psd.Length - 36);
if (securityUserEntity.UserPassword != data.UserPassword)
{
securityUserEntity.ErrorLoginCount += 1;
//输错多少次后,账号锁定
if (securityUserEntity.ErrorLoginCount > SecurityUserPolicy.ERROR_LOGIN_COUNT)
{
securityUserEntity.IsAccountsLockedIndc = YesNoPolicy.YES;
securityUserEntity.AccountsLockedDatetime = DateTime.Now.AddDays(1);
}
repository.UpdateNow(securityUserEntity);
throw new ValidatedException(certification);
}
JwtGenerate jwtGenerate = service.ServiceProvider.GetService<JwtGenerate>()!;
string clainmName = Guid.NewGuid().ToString();
Dictionary<string, string> claims = new Dictionary<string, string>();
claims.Add(ClaimPolicy.CLAINM_NAME, clainmName);
claims.Add(ClaimPolicy.CLAINM_ACCOUNT, securityUserEntity.UserMobile!);
claims.Add(ClaimPolicy.CLAINM_USERID, securityUserEntity.UserName);
claims.Add(ClaimPolicy.CLAINM_LOCALE, data.Culture);
var accessToken = jwtGenerate.Generate(claims);
if (httpContextAccessor.HttpContext.Request.Headers.ContainsKey("Authorization"))
httpContextAccessor.HttpContext.Request.Headers.Remove("Authorization");
httpContextAccessor.HttpContext.Request.Headers.Add("Authorization", "Bearer " + accessToken);
if (httpContextAccessor.HttpContext.Request.Headers.ContainsKey(ClaimPolicy.HEADER_LOCALE))
httpContextAccessor.HttpContext.Request.Headers.Remove(ClaimPolicy.HEADER_LOCALE);
httpContextAccessor.HttpContext.Request.Headers.Add(ClaimPolicy.HEADER_LOCALE, data.Culture);
service.UserId = securityUserEntity.UserName;
securityUserEntity.ErrorLoginCount = 0;
securityUserEntity.IsAccountsLockedIndc = YesNoPolicy.NO;
securityUserEntity.AccountsLockedDatetime = null;
//更新最后登录日期
securityUserEntity.LastLoginDatetime = DateTime.Now;
repository.UpdateNow(securityUserEntity);
IRepository<SecurityUserWebLoginEntity> repositoryLog = service.GetRepository<IRepository<SecurityUserWebLoginEntity>>();
SecurityUserWebLoginEntity webLogin = new SecurityUserWebLoginEntity
{
Username = securityUserEntity.UserName,
SessionId = clainmName,
LoginTime = DateTime.Now,
HostIp = httpContextAccessor.HttpContext.Connection.RemoteIpAddress?.MapToIPv4()?.ToString(),
HostUserAgent = this.httpContextAccessor?.HttpContext?.Request.Headers.UserAgent.ToString(),
RefreshToken = jwtGenerate.GenerateRefreshToken(),
RefreshTokenRefreshedTime = DateTime.Now,
};
repositoryLog.InsertNow(webLogin);
PermissionContext permissionContext = new PermissionContext(service);
return new LoginProfile
{
Token = new JwtInfo() { AccessToken = accessToken, RefreshToken = webLogin.RefreshToken, CreatedTime = webLogin.RefreshTokenRefreshedTime },
Username = securityUserEntity.UserName,
UserType = securityUserEntity.UserType,
UserLocalName = securityUserEntity.UserLocalName,
UserEngName = securityUserEntity.UserEngName,
UserMobile = securityUserEntity.UserMobile!,
PermissionInfo = permissionContext.GetUserPermissionInfo()
};
}
[PermissionOperate(name: SecurityPermissionPolicy.SecurityUser, operate: "LoginAgent")]
[UnitOfWork]
public LoginProfile LoginAgent(LoginData data)
{
LoginProfile loginProfile = Login(data);
SecurityUserEntity agent = repository.DetachedEntities.Where(x =>
(x.UserMobile == data.AgentUserName && x.UserIsValidIndc == YesNoPolicy.YES)).FirstOrDefault()!;
if (agent == null)
{
throw new ValidatedException("代理用户不存在.");
}
JwtGenerate jwtGenerate = service.ServiceProvider.GetService<JwtGenerate>()!;
Dictionary<string, string> claims = new Dictionary<string, string>();
claims.Add(ClaimPolicy.CLAINM_NAME, agent.UserName);
claims.Add(ClaimPolicy.CLAINM_ACCOUNT, agent.UserMobile!);
claims.Add(ClaimPolicy.CLAINM_USERID, agent.UserName);
claims.Add(ClaimPolicy.CLAINM_LOCALE, data.Culture);
var accessToken = jwtGenerate.Generate(claims);
if (httpContextAccessor.HttpContext.Request.Headers.ContainsKey("Authorization"))
httpContextAccessor.HttpContext.Request.Headers.Remove("Authorization");
httpContextAccessor.HttpContext.Request.Headers.Add("Authorization", "Bearer " + accessToken);
if (httpContextAccessor.HttpContext.Request.Headers.ContainsKey(ClaimPolicy.HEADER_LOCALE))
httpContextAccessor.HttpContext.Request.Headers.Remove(ClaimPolicy.HEADER_LOCALE);
httpContextAccessor.HttpContext.Request.Headers.Add(ClaimPolicy.HEADER_LOCALE, data.Culture);
SecurityUserWebLoginEntity webLogin = new SecurityUserWebLoginEntity
{
Username = agent.UserName,
SessionId = Guid.NewGuid().ToString(),
LoginTime = DateTime.Now,
HostIp = loginProfile.Username,
HostUserAgent = loginProfile.UserMobile!,
RefreshToken = jwtGenerate.GenerateRefreshToken(),
RefreshTokenRefreshedTime = DateTime.Now,
};
IRepository<SecurityUserWebLoginEntity> repositoryLog = service.GetRepository<IRepository<SecurityUserWebLoginEntity>>();
repositoryLog.InsertNow(webLogin);
service.UserId = agent.UserName;
PermissionContext permissionContext = new PermissionContext(service);
return new LoginProfile
{
Token = new JwtInfo() { AccessToken = accessToken, RefreshToken = webLogin.RefreshToken, CreatedTime = webLogin.RefreshTokenRefreshedTime },
Username = agent.UserName,
UserType = agent.UserType,
UserLocalName = agent.UserLocalName,
UserEngName = agent.UserEngName,
UserMobile = agent.UserMobile!,
PermissionInfo = permissionContext.GetUserPermissionInfo()
};
}
[AllowAnonymous]
[UnitOfWork]
public string Register(RegisterData data)
{
SecurityUserEntity securityUser = repository.DetachedEntities.Where(x => x.UserMobile == data.UserMobile).FirstOrDefault()!;
if (securityUser != null)
{
throw new ValidatedException("手机号已注册.");
}
securityUser = new SecurityUserEntity();
if (string.IsNullOrEmpty(data.UserMobile) || string.IsNullOrEmpty(data.RsaUserPassword)) return null;
ISecurityGeneratorNumberService securityGeneratorNumberService = service.ServiceProvider.GetService<ISecurityGeneratorNumberService>()!;
string userName = securityGeneratorNumberService.GetNo(SecurityPrefixPolicy.SECURITY_USER, SecurityPrefixPolicy.SECURITY_USER_USER_NAME, null, YesNoPolicy.NO, SecurityPrefixPolicy.SECURITY_USER_FORMAT);
var privateKey = service.Configuration[SecuritySettingsPolicy.SecuritySettings_RsaPrivateKey];
var rsa = new System.RSA(privateKey, true);
securityUser.UserPassword = rsa.DecodeOrNull(data.RsaUserPassword);
//052636a8-b146-9efd-f5dc-0034a3c5bb34+password(MD5)
string guid = securityUser.UserPassword.Substring(0, 36);
securityUser.UserPassword = securityUser.UserPassword.Substring(36, securityUser.UserPassword.Length - 36);
securityUser.UserName = userName;
securityUser.UserMobile = data.UserMobile;
securityUser.UserLocalName = "用户" + securityUser.UserName;
securityUser.UserIsValidIndc = YesNoPolicy.YES;
JwtGenerate jwtGenerate = service.ServiceProvider.GetService<JwtGenerate>()!;
Dictionary<string, string> claims = new Dictionary<string, string>();
claims.Add(ClaimPolicy.CLAINM_NAME, securityUser.UserName);
claims.Add(ClaimPolicy.CLAINM_ACCOUNT, securityUser.UserName);
claims.Add(ClaimPolicy.CLAINM_USERID, securityUser.UserName);
claims.Add(ClaimPolicy.CLAINM_LOCALE, CulturePolicy.ZH_CN);
service.UserId = securityUser.UserName;
var accessToken = jwtGenerate.Generate(claims);
if (httpContextAccessor.HttpContext.Request.Headers.ContainsKey("Authorization"))
httpContextAccessor.HttpContext.Request.Headers.Remove("Authorization");
httpContextAccessor.HttpContext.Request.Headers.Add("Authorization", "Bearer " + accessToken);
repository.InsertNow(securityUser);
//发布注册的事务
EventData eventData = new EventData();
ISecurityUserRegisterPublisher publisher = service.ServiceProvider.GetService<ISecurityUserRegisterPublisher>()!;
eventData.EventSource = securityUser;
eventData.EventId = typeof(SecurityUserRegisterPublisher).Name;
publisher.Publish(eventData);
return securityUser.UserMobile;
}
[PermissionOperate(name: SecurityPermissionPolicy.SecurityUser, operate: Operate.Update)]
[UnitOfWork]
public LoginProfile AmendPassword(LoginData data)
{
SecurityUserEntity securityUserEntity = repository.DetachedEntities.Where(x =>
x.UserName == service.UserId
&& x.UserIsValidIndc == YesNoPolicy.YES).FirstOrDefault()!;
if (securityUserEntity == null)
{
throw new ValidatedException("认证不通过.");
}
var descKey = service.Configuration[SecuritySettingsPolicy.SecuritySettings_DescKey];
var privateKey = service.Configuration[SecuritySettingsPolicy.SecuritySettings_RsaPrivateKey];
var rsa = new RSA(privateKey, true);
var psd = rsa.DecodeOrNull(data.RsaUserPassword);
if (string.IsNullOrEmpty(psd)) throw new ValidatedException("认证不通过.");
//052636a8-b146-9efd-f5dc-0034a3c5bb34+password
var guid = psd.Substring(0, 36);
if (guid != data.UserName)
{
throw new ValidatedException("认证不通过.");
}
if (securityUserEntity.IsAccountsLockedIndc == YesNoPolicy.YES && securityUserEntity.AccountsLockedDatetime.HasValue
&& securityUserEntity.AccountsLockedDatetime.Value.Subtract(DateTime.Now).TotalMinutes > 0)
{
throw new ValidatedException("用户账号已锁定.");
}
if (securityUserEntity.UserPassword != psd.Substring(36, psd.Length - 36))
{
securityUserEntity.ErrorLoginCount += 1;
//输错多少次后,账号锁定
if (securityUserEntity.ErrorLoginCount > SecurityUserPolicy.ERROR_LOGIN_COUNT)
{
securityUserEntity.IsAccountsLockedIndc = YesNoPolicy.YES;
securityUserEntity.AccountsLockedDatetime = DateTime.Now.AddDays(1);
}
repository.UpdateNow(securityUserEntity);
throw new ValidatedException("认证不通过.");
}
JwtGenerate jwtGenerate = service.ServiceProvider.GetService<JwtGenerate>()!;
string clainmName = Guid.NewGuid().ToString();
Dictionary<string, string> claims = new Dictionary<string, string>();
claims.Add(ClaimPolicy.CLAINM_NAME, clainmName);
claims.Add(ClaimPolicy.CLAINM_ACCOUNT, securityUserEntity.UserMobile!);
claims.Add(ClaimPolicy.CLAINM_USERID, securityUserEntity.UserName);
claims.Add(ClaimPolicy.CLAINM_LOCALE, data.Culture ?? CulturePolicy.ZH_CN);
var accessToken = jwtGenerate.Generate(claims);
if (httpContextAccessor.HttpContext.Request.Headers.ContainsKey("Authorization"))
httpContextAccessor.HttpContext.Request.Headers.Remove("Authorization");
httpContextAccessor.HttpContext.Request.Headers.Add("Authorization", "Bearer " + accessToken);
securityUserEntity.ErrorLoginCount = 0;
securityUserEntity.IsAccountsLockedIndc = YesNoPolicy.NO;
securityUserEntity.AccountsLockedDatetime = null;
//更新最后登录日期
securityUserEntity.LastLoginDatetime = DateTime.Now;
psd = rsa.DecodeOrNull(data.UserPassword);
if (string.IsNullOrEmpty(psd)) throw new ValidatedException("认证不通过.");
securityUserEntity.UserPassword = psd.Substring(36, psd.Length - 36);
repository.UpdateNow(securityUserEntity);
IRepository<SecurityUserWebLoginEntity> repositoryLog = service.GetRepository<IRepository<SecurityUserWebLoginEntity>>();
SecurityUserWebLoginEntity webLogin = new SecurityUserWebLoginEntity
{
Username = securityUserEntity.UserName,
SessionId = clainmName,
LoginTime = DateTime.Now,
HostIp = httpContextAccessor.HttpContext.Connection.RemoteIpAddress?.MapToIPv4()?.ToString(),
HostUserAgent = this.httpContextAccessor?.HttpContext?.Request.Headers.UserAgent.ToString(),
RefreshToken = jwtGenerate.GenerateRefreshToken(),
RefreshTokenRefreshedTime = DateTime.Now,
};
repositoryLog.InsertNow(webLogin);
PermissionContext permissionContext = new PermissionContext(service);
return new LoginProfile
{
Token = new JwtInfo() { AccessToken = accessToken, RefreshToken = webLogin.RefreshToken, CreatedTime = webLogin.RefreshTokenRefreshedTime },
Username = securityUserEntity.UserName,
UserType = securityUserEntity.UserType,
UserLocalName = securityUserEntity.UserLocalName,
UserEngName = securityUserEntity.UserEngName,
UserMobile = securityUserEntity.UserMobile!,
PermissionInfo = permissionContext.GetUserPermissionInfo()
};
}
[AllowAnonymous]
[HttpGet]
public string GetRsaPublicKey()
{
if (string.IsNullOrEmpty(service.Configuration["SecuritySettings:RsaPublicKey"]))
throw new ValidatedException("[SecuritySettings:RsaPublicKey]");
return service.Configuration["SecuritySettings:RsaPublicKey"]!;
}
[AllowAnonymous]
public JwtInfo RefreshToken(JwtInfo jwtInfo)
{
if (jwtInfo == null || string.IsNullOrEmpty(jwtInfo.AccessToken) || string.IsNullOrEmpty(jwtInfo.RefreshToken))
throw new ServiceErrorException("无效的令牌,请重新登录.");
TokenValidationParameters parameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
//获取或设置要使用的Microsoft.IdentityModel.Tokens.SecurityKey用于签名验证。
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(service.CloudBuilder.CloudBuilderOptions.Jwt.Secret)),
//获取或设置一个System.String它表示将使用的有效发行者检查代币的发行者。
ValidIssuer = service.CloudBuilder.CloudBuilderOptions.Jwt.Issuer,
//获取或设置一个字符串,该字符串表示将用于检查的有效受众反对令牌的观众。
ValidAudience = service.CloudBuilder.CloudBuilderOptions.Jwt.Audience,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = false,//不验证过期时间
};
// 验证 Token
var tokenHandler = new JwtSecurityTokenHandler();
SecurityToken vTocken;
string token = jwtInfo.AccessToken.Replace("Bearer ", "");
try
{
var tokenValidationResult = tokenHandler.ValidateToken(token, parameters, out vTocken);
if (tokenValidationResult == null || !tokenValidationResult.Identities.Any() || !tokenValidationResult.Identities.First().IsAuthenticated)
throw new ServiceErrorException("无效的令牌,请重新登录.");
}
catch (Exception)
{
throw new ServiceErrorException("无效的令牌,请重新登录.");
}
string clainmName = ClaimPolicy.GetClaimValue(token, ClaimPolicy.CLAINM_NAME);
string username = ClaimPolicy.GetClaimValue(token, ClaimPolicy.CLAINM_USERID);
string userMobile = ClaimPolicy.GetClaimValue(token, ClaimPolicy.CLAINM_ACCOUNT);
string locale = ClaimPolicy.GetClaimValue(token, ClaimPolicy.CLAINM_LOCALE);
if (string.IsNullOrEmpty(clainmName) || string.IsNullOrEmpty(username))
throw new ServiceErrorException("无效的令牌,请重新登录.");
IRepository<SecurityUserEntity> repositorySecurityUser = service.GetRepository<IRepository<SecurityUserEntity>>();
SecurityUserEntity user = repositorySecurityUser.DetachedEntities.Where(x => x.UserName == username && x.UserIsValidIndc == YesNoPolicy.YES).FirstOrDefault();
if (user == null) throw new ServiceErrorException("无效的令牌,请重新登录.");
IRepository<SecurityUserWebLoginEntity> repositoryLog = service.GetRepository<IRepository<SecurityUserWebLoginEntity>>();
SecurityUserWebLoginEntity log = repositoryLog.DetachedEntities.Where(x => x.Username == username && x.SessionId == clainmName).FirstOrDefault()!;
//不是最后登录的令牌失效
DateTime? LoginTime = repositoryLog.DetachedEntities.Where(x => x.Username == username).Max(x => x.LoginTime);
if (LoginTime.HasValue && log.LoginTime < LoginTime.Value) throw new ServiceErrorException("无效的令牌,请重新登录.");
if (log == null || log.RefreshToken != jwtInfo.RefreshToken || log.LogoutTime.HasValue ||
log.RefreshToken != jwtInfo.RefreshToken || log.RefreshTokenRefreshedTime.AddMinutes(Convert.ToInt32(service.CloudBuilder.CloudBuilderOptions.Jwt.RefreshExpireMins)) < DateTime.Now)
throw new ServiceErrorException("无效的令牌,请重新登录.");
JwtGenerate jwtGenerate = service.ServiceProvider.GetService<JwtGenerate>()!;
Dictionary<string, string> claims = new Dictionary<string, string>();
claims.Add(ClaimPolicy.CLAINM_NAME, clainmName);
claims.Add(ClaimPolicy.CLAINM_ACCOUNT, userMobile);
claims.Add(ClaimPolicy.CLAINM_USERID, username);
claims.Add(ClaimPolicy.CLAINM_LOCALE, locale??CulturePolicy.ZH_CN);
var accessToken = jwtGenerate.Generate(claims);
log.RefreshToken = jwtGenerate.GenerateRefreshToken();
log.RefreshTokenRefreshedTime = DateTime.Now;
if (httpContextAccessor.HttpContext.Request.Headers.ContainsKey("Authorization"))
httpContextAccessor.HttpContext.Request.Headers.Remove("Authorization");
httpContextAccessor.HttpContext.Request.Headers.Add("Authorization", "Bearer " + accessToken);
repositoryLog.UpdateNow(log);
jwtInfo.AccessToken = accessToken;
jwtInfo.RefreshToken = log.RefreshToken;
jwtInfo.CreatedTime = log.RefreshTokenRefreshedTime;
return jwtInfo;
}
[AllowAnonymous]
public void Logout()
{
string username = ClaimPolicy.GetClaimValue(httpContextAccessor, ClaimPolicy.CLAINM_USERID);
string clainmName = ClaimPolicy.GetClaimValue(httpContextAccessor, ClaimPolicy.CLAINM_NAME);
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(clainmName)) { return; }
IRepository<SecurityUserWebLoginEntity> repositoryLog = service.GetRepository<IRepository<SecurityUserWebLoginEntity>>();
SecurityUserWebLoginEntity log = repositoryLog.DetachedEntities.Where(x => x.Username == username && x.SessionId == clainmName).FirstOrDefault()!;
if (log == null || log.LogoutTime.HasValue) { return; }
log.LogoutTime = DateTime.Now;
repositoryLog.UpdateNow(log);
}
}
}