diff --git a/src/0-core/HuanMeng.DotNetCore/Base/ResonseCode.cs b/src/0-core/HuanMeng.DotNetCore/Base/ResonseCode.cs index d915c99..f5ffee2 100644 --- a/src/0-core/HuanMeng.DotNetCore/Base/ResonseCode.cs +++ b/src/0-core/HuanMeng.DotNetCore/Base/ResonseCode.cs @@ -1,4 +1,4 @@ -namespace HuanMeng.DotNetCore.Base +namespace HuanMeng.DotNetCore.Base { /// /// 响应编码参考,实际的项目使用可以自行定义 @@ -21,6 +21,10 @@ /// 用户验证失败 /// Unauthorized = 401, + /// + /// 重复请求 + /// + ManyRequests = 429, /// /// 正在处理中 diff --git a/src/0-core/HuanMeng.DotNetCore/HuanMeng.DotNetCore.csproj b/src/0-core/HuanMeng.DotNetCore/HuanMeng.DotNetCore.csproj index 78ada2f..cad8e59 100644 --- a/src/0-core/HuanMeng.DotNetCore/HuanMeng.DotNetCore.csproj +++ b/src/0-core/HuanMeng.DotNetCore/HuanMeng.DotNetCore.csproj @@ -9,7 +9,9 @@ + + diff --git a/src/0-core/HuanMeng.DotNetCore/JwtInfrastructure/Interface/IJwtAuthManager.cs b/src/0-core/HuanMeng.DotNetCore/JwtInfrastructure/Interface/IJwtAuthManager.cs new file mode 100644 index 0000000..23e2e1d --- /dev/null +++ b/src/0-core/HuanMeng.DotNetCore/JwtInfrastructure/Interface/IJwtAuthManager.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.DotNetCore.JwtInfrastructure.Interface +{ + /// + /// jwt帮助类 + /// + public interface IJwtAuthManager + { + /// + /// 用户刷新令牌只读词典 + /// + IImmutableDictionary UsersRefreshTokensReadOnlyDictionary { get; } + + /// + /// 生成令牌 + /// + /// 用户名 + /// 用户的有关信息 + /// + /// + JwtAuthResult GenerateTokens(string username, Claim[] claims, DateTime now); + /// + /// 刷新令牌 + /// + /// + /// + /// + /// + JwtAuthResult Refresh(string refreshToken, string accessToken, DateTime now); + /// + /// 删除过期的刷新令牌 + /// + /// + void RemoveExpiredRefreshTokens(DateTime now); + /// + /// 按用户名删除刷新令牌 + /// + /// + void RemoveRefreshTokenByUserName(string userName); + /// + /// 解码JwtToken + /// + /// + /// + (ClaimsPrincipal, JwtSecurityToken?) DecodeJwtToken(string token); + } +} diff --git a/src/0-core/HuanMeng.DotNetCore/JwtInfrastructure/JwtAuthManager.cs b/src/0-core/HuanMeng.DotNetCore/JwtInfrastructure/JwtAuthManager.cs new file mode 100644 index 0000000..780f9b0 --- /dev/null +++ b/src/0-core/HuanMeng.DotNetCore/JwtInfrastructure/JwtAuthManager.cs @@ -0,0 +1,168 @@ +using HuanMeng.DotNetCore.JwtInfrastructure.Interface; + +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + + +namespace HuanMeng.DotNetCore.JwtInfrastructure +{ + /// + /// jwt帮助类 + /// + /// + public class JwtAuthManager(JwtTokenConfig jwtTokenConfig) : IJwtAuthManager + { + /// + /// 保存刷新token + /// + public IImmutableDictionary UsersRefreshTokensReadOnlyDictionary => _usersRefreshTokens.ToImmutableDictionary(); + /// + /// 后面可以存储在数据库或分布式缓存中 + /// + private readonly ConcurrentDictionary _usersRefreshTokens = new(); + /// + /// 获取加密字段 + /// + private readonly byte[] _secret = Encoding.UTF8.GetBytes(jwtTokenConfig.Secret); + + /// + /// 删除过期token + /// + /// + public void RemoveExpiredRefreshTokens(DateTime now) + { + var expiredTokens = _usersRefreshTokens.Where(x => x.Value.ExpireAt < now).ToList(); + foreach (var expiredToken in expiredTokens) + { + _usersRefreshTokens.TryRemove(expiredToken.Key, out _); + } + } + + /// + /// 根据用户名删除token + /// + /// + public void RemoveRefreshTokenByUserName(string userName) + { + var refreshTokens = _usersRefreshTokens.Where(x => x.Value.UserName == userName).ToList(); + foreach (var refreshToken in refreshTokens) + { + _usersRefreshTokens.TryRemove(refreshToken.Key, out _); + } + } + + /// + /// 创建token + /// + /// 用户名 + /// 用户项 + /// 过期时间 + /// + public JwtAuthResult GenerateTokens(string username, Claim[] claims, DateTime now) + { + var shouldAddAudienceClaim = string.IsNullOrWhiteSpace(claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Aud)?.Value); + //创建token + var jwtToken = new JwtSecurityToken( + jwtTokenConfig.Issuer, + shouldAddAudienceClaim ? jwtTokenConfig.Audience : string.Empty, + claims, + expires: now.AddMinutes(jwtTokenConfig.AccessTokenExpiration), + signingCredentials: new SigningCredentials(new SymmetricSecurityKey(_secret), SecurityAlgorithms.HmacSha256Signature)); + var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtToken); + + //创建刷新token + var refreshToken = new JwtRefreshToken + { + UserName = username, + TokenString = GenerateRefreshTokenString(), + ExpireAt = now.AddMinutes(jwtTokenConfig.RefreshTokenExpiration) + }; + _usersRefreshTokens.AddOrUpdate(refreshToken.TokenString, refreshToken, (_, _) => refreshToken); + + return new JwtAuthResult + { + AccessToken = accessToken, + RefreshToken = refreshToken + }; + } + + /// + /// 刷新token + /// + /// + /// + /// + /// + /// + public JwtAuthResult Refresh(string refreshToken, string accessToken, DateTime now) + { + var (principal, jwtToken) = DecodeJwtToken(accessToken); + if (jwtToken == null || !jwtToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256Signature)) + { + throw new SecurityTokenException("无效的token"); + } + + var userName = principal.Identity?.Name; + if (!_usersRefreshTokens.TryGetValue(refreshToken, out var existingRefreshToken)) + { + throw new SecurityTokenException("token已失效"); + } + if (existingRefreshToken.UserName != userName || existingRefreshToken.ExpireAt < now) + { + throw new SecurityTokenException("token不匹配"); + } + //创建新的token + return GenerateTokens(userName, principal.Claims.ToArray(), now); + } + /// + /// 解析token + /// + /// + /// + /// + public (ClaimsPrincipal, JwtSecurityToken?) DecodeJwtToken(string token) + { + if (string.IsNullOrWhiteSpace(token)) + { + throw new SecurityTokenException("token不能为空"); + } + var principal = new JwtSecurityTokenHandler() + .ValidateToken(token, + new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = jwtTokenConfig.Issuer, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(_secret), + ValidAudience = jwtTokenConfig.Audience, + ValidateAudience = true, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(5) + }, + out var validatedToken); + return (principal, validatedToken as JwtSecurityToken); + } + + + /// + /// 获取刷新的token + /// + /// + private static string GenerateRefreshTokenString() + { + var randomNumber = new byte[32]; + using var randomNumberGenerator = RandomNumberGenerator.Create(); + randomNumberGenerator.GetBytes(randomNumber); + return Convert.ToBase64String(randomNumber); + } + } +} diff --git a/src/0-core/HuanMeng.DotNetCore/JwtInfrastructure/JwtAuthResult.cs b/src/0-core/HuanMeng.DotNetCore/JwtInfrastructure/JwtAuthResult.cs new file mode 100644 index 0000000..6bdbd7b --- /dev/null +++ b/src/0-core/HuanMeng.DotNetCore/JwtInfrastructure/JwtAuthResult.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace HuanMeng.DotNetCore.JwtInfrastructure +{ + /// + /// 令牌 + /// + public class JwtAuthResult + { + /// + /// 当前token + /// + [JsonPropertyName("accessToken")] + public string AccessToken { get; set; } = string.Empty; + + /// + /// 刷新token + /// + [JsonPropertyName("refreshToken")] + public JwtRefreshToken RefreshToken { get; set; } = new(); + } +} diff --git a/src/0-core/HuanMeng.DotNetCore/JwtInfrastructure/JwtRefreshToken.cs b/src/0-core/HuanMeng.DotNetCore/JwtInfrastructure/JwtRefreshToken.cs new file mode 100644 index 0000000..f68f04a --- /dev/null +++ b/src/0-core/HuanMeng.DotNetCore/JwtInfrastructure/JwtRefreshToken.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace HuanMeng.DotNetCore.JwtInfrastructure +{ + /// + /// 刷新token + /// + public class JwtRefreshToken + { + /// + /// 用户名 + /// + [JsonPropertyName("username")] + public string UserName { get; set; } = string.Empty; + + /// + /// token + /// + [JsonPropertyName("tokenString")] + public string TokenString { get; set; } = string.Empty; + + /// + /// 过期时间 + /// + [JsonPropertyName("expireAt")] + public DateTime ExpireAt { get; set; } + } +} diff --git a/src/0-core/HuanMeng.DotNetCore/JwtInfrastructure/JwtTokenConfig.cs b/src/0-core/HuanMeng.DotNetCore/JwtInfrastructure/JwtTokenConfig.cs new file mode 100644 index 0000000..a62d459 --- /dev/null +++ b/src/0-core/HuanMeng.DotNetCore/JwtInfrastructure/JwtTokenConfig.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace HuanMeng.DotNetCore.JwtInfrastructure +{ + /// + /// JwtToken 配置文件 + /// + public class JwtTokenConfig + { + + /// + /// 加密值 + /// + [JsonPropertyName("secret")] + public string Secret { get; set; } = string.Empty; + + /// + /// 颁发者 + /// + [JsonPropertyName("issuer")] + public string Issuer { get; set; } = string.Empty; + + /// + /// 受众 + /// + [JsonPropertyName("audience")] + public string Audience { get; set; } = string.Empty; + + /// + /// 令牌过期时间 + /// + [JsonPropertyName("accessTokenExpiration")] + public int AccessTokenExpiration { get; set; } + + /// + /// 刷新令牌过期时间(一般会比令牌过期时间长) + /// + [JsonPropertyName("refreshTokenExpiration")] + public int RefreshTokenExpiration { get; set; } + + } +} diff --git a/src/0-core/HuanMeng.DotNetCore/MultiTenant/MultiTenantDbContext.cs b/src/0-core/HuanMeng.DotNetCore/MultiTenant/MultiTenantDbContext.cs index ac2836f..9530f30 100644 --- a/src/0-core/HuanMeng.DotNetCore/MultiTenant/MultiTenantDbContext.cs +++ b/src/0-core/HuanMeng.DotNetCore/MultiTenant/MultiTenantDbContext.cs @@ -18,14 +18,14 @@ namespace HuanMeng.DotNetCore.MultiTenant /// /// 租户信息 /// - public ITenantInfo TenantInfo { get; set; } + public ITenantInfo? TenantInfo { get; set; } /// /// 构造函数 /// /// - public MultiTenantDbContext(ITenantInfo tenantInfo) + public MultiTenantDbContext(ITenantInfo? tenantInfo) { this.TenantInfo = tenantInfo; } @@ -35,16 +35,16 @@ namespace HuanMeng.DotNetCore.MultiTenant /// /// /// - public MultiTenantDbContext(ITenantInfo tenantInfo, DbContextOptions options) + public MultiTenantDbContext(ITenantInfo? tenantInfo, DbContextOptions options) : base(options) { if (tenantInfo == null) { - tenantInfo=new TenantInfo() + tenantInfo = new TenantInfo() { TenantId = Guid.NewGuid(), - Identifier= "default", - Name="default" + Identifier = "default", + Name = "default" }; } this.TenantInfo = tenantInfo; diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Base/MiaoYuBase.cs b/src/0-core/HuanMeng.MiaoYu.Code/Base/MiaoYuBase.cs index 390a2ac..b8fc698 100644 --- a/src/0-core/HuanMeng.MiaoYu.Code/Base/MiaoYuBase.cs +++ b/src/0-core/HuanMeng.MiaoYu.Code/Base/MiaoYuBase.cs @@ -1,14 +1,32 @@ +using AutoMapper; + using HuanMeng.DotNetCore.Base; +using HuanMeng.DotNetCore.JwtInfrastructure; +using HuanMeng.DotNetCore.JwtInfrastructure.Interface; +using HuanMeng.DotNetCore.MultiTenant; using HuanMeng.MiaoYu.Code.DataAccess; +using HuanMeng.MiaoYu.Code.TencentUtile; +using HuanMeng.MiaoYu.Code.Users.UserAccount; +using HuanMeng.MiaoYu.Code.Users.UserAccount.Contract; +using HuanMeng.MiaoYu.Model.Dto; + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; namespace HuanMeng.MiaoYu.Code.Base { + /// + /// 默认控制器类 + /// public class MiaoYuBase : BLLBase { public MiaoYuBase(IServiceProvider serviceProvider) : base(serviceProvider) @@ -31,6 +49,170 @@ namespace HuanMeng.MiaoYu.Code.Base } } + #region 租户信息 + /// + /// + /// + private TenantInfo _tenantInfo; + /// + /// 租户信息 + /// + public TenantInfo TenantInfo + { + get + { + if (_tenantInfo == null) + { + _tenantInfo = _serviceProvider.GetRequiredService(); + } + return _tenantInfo; + } + } + #endregion + #region 请求信息 + private IHttpContextAccessor _HttpContextAccessor; + /// + /// HttpContextAccessor + /// + public IHttpContextAccessor HttpContextAccessor + { + get + { + if (_mapper == null) + { + _HttpContextAccessor = _serviceProvider.GetRequiredService(); + } + return _HttpContextAccessor; + } + } + #endregion + + #region 映射 + private IMapper _mapper; + + /// + /// dto映射 + /// + //[FromServices] + public IMapper Mapper + { + get + { + if (_mapper == null) + { + _mapper = _serviceProvider.GetRequiredService(); + } + return _mapper; + } + set { _mapper = value; } + } + #endregion + + #region 腾讯云 + private TencentConfig _tencentConfig; + /// + /// 腾讯云配置 + /// + public TencentConfig TencentConfig + { + + get + { + if (_tencentConfig == null) + { + _tencentConfig = _serviceProvider.GetService() ?? new TencentConfig(); + } + return _tencentConfig; + } + } + #endregion + + #region 验证码管理 + private IVerificationCodeManager _verificationCodeManager; + /// + /// 验证码管理 + /// + public IVerificationCodeManager VerificationCodeManager + { + get + { + if (_verificationCodeManager == null) + { + _verificationCodeManager = _serviceProvider.GetService(); + } + return _verificationCodeManager; + } + } + #endregion + + #region JWT管理 + + private IJwtAuthManager _jwtAuthManager; + /// + /// jwt管理 + /// + public IJwtAuthManager JwtAuthManager + { + get + { + if (_jwtAuthManager == null) + { + _jwtAuthManager = _serviceProvider.GetService(); + } + return _jwtAuthManager; + } + } + #endregion + + #region 用户信息 + private RequestUserInfo _userInfo; + /// + /// 用户信息 + /// + public RequestUserInfo UserInfo + { + get + { + if (_userInfo == null) + { + var accessToken = HttpContextAccessor.HttpContext.GetTokenAsync("Bearer", "access_token").Result; + if (string.IsNullOrEmpty(accessToken)) + { + + } + var (principal, jwtToken) = JwtAuthManager.DecodeJwtToken(accessToken); + if (jwtToken == null || !jwtToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256Signature)) + { + throw new SecurityTokenException("无效的token"); + } + var userIdStr = principal.FindFirst("UserId")?.Value; + if (string.IsNullOrEmpty(userIdStr)) + { + throw new SecurityTokenException("无效的token"); + } + var nickName = principal.FindFirst("NickName")?.Value; + var userId = int.Parse(userIdStr); + _userInfo = new RequestUserInfo() + { + UserId = userId, + NickName = nickName + }; + } + return _userInfo; + } + } + + /// + /// 用户Id + /// + public int _UserId + { + get + { + return UserInfo?.UserId ?? 0; + } + } + #endregion } } diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Class1.cs b/src/0-core/HuanMeng.MiaoYu.Code/Class1.cs deleted file mode 100644 index 64bd4d9..0000000 --- a/src/0-core/HuanMeng.MiaoYu.Code/Class1.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace HuanMeng.MiaoYu.Code -{ - public class Class1 - { - - } -} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/DataAccess/DAO.cs b/src/0-core/HuanMeng.MiaoYu.Code/DataAccess/DAO.cs index 48da023..31e711a 100644 --- a/src/0-core/HuanMeng.MiaoYu.Code/DataAccess/DAO.cs +++ b/src/0-core/HuanMeng.MiaoYu.Code/DataAccess/DAO.cs @@ -1,4 +1,5 @@ using HuanMeng.DotNetCore.Base; +using HuanMeng.DotNetCore.MultiTenant; using HuanMeng.MiaoYu.Model.DbSqlServer.Db_MiaoYu; using Microsoft.Extensions.DependencyInjection; @@ -17,25 +18,23 @@ namespace HuanMeng.MiaoYu.Code.DataAccess public class DAO : DaoBase { //private IMultiTenantProvider _multiTenantProvider; - + private TenantInfo _tenantInfo; /// /// 构造函数 /// /// public DAO(IServiceProvider serviceProvider) : base(serviceProvider) { - //this._multiTenantProvider = serviceProvider.GetRequiredService(); - + this._tenantInfo = serviceProvider.GetRequiredService(); } /// /// 构造函数 /// /// - /// 渠道编号 - public DAO(IServiceProvider serviceProvider, string channelCode) : base(serviceProvider) + public DAO(IServiceProvider serviceProvider, TenantInfo tenantInfo) : base(serviceProvider) { - //this._multiTenantProvider + this._tenantInfo = tenantInfo; } @@ -52,15 +51,17 @@ namespace HuanMeng.MiaoYu.Code.DataAccess if (_daoDbMiaoYu == null) { var dbContext = _serviceProvider.GetRequiredService(); - - // 在这里进行数据库操作... + if (_tenantInfo == null) + { + this._tenantInfo = _serviceProvider.GetRequiredService(); + } + dbContext.SetTenantInfo(_tenantInfo); _daoDbMiaoYu = new EfCoreDaoBase(dbContext); } return _daoDbMiaoYu; } } - /// /// 租户 /// diff --git a/src/0-core/HuanMeng.MiaoYu.Code/GlobalUsings.cs b/src/0-core/HuanMeng.MiaoYu.Code/GlobalUsings.cs new file mode 100644 index 0000000..88e3fa4 --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/GlobalUsings.cs @@ -0,0 +1 @@ +global using HuanMeng.MiaoYu.Code.Base; \ No newline at end of file diff --git a/src/0-core/HuanMeng.MiaoYu.Code/HuanMeng.MiaoYu.Code.csproj b/src/0-core/HuanMeng.MiaoYu.Code/HuanMeng.MiaoYu.Code.csproj index 5b52481..e866572 100644 --- a/src/0-core/HuanMeng.MiaoYu.Code/HuanMeng.MiaoYu.Code.csproj +++ b/src/0-core/HuanMeng.MiaoYu.Code/HuanMeng.MiaoYu.Code.csproj @@ -8,8 +8,23 @@ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + @@ -17,8 +32,4 @@ - - - - diff --git a/src/0-core/HuanMeng.MiaoYu.Code/JwtUtil/JwtManager.cs b/src/0-core/HuanMeng.MiaoYu.Code/JwtUtil/JwtManager.cs new file mode 100644 index 0000000..bede411 --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/JwtUtil/JwtManager.cs @@ -0,0 +1,167 @@ +using HuanMeng.DotNetCore.JwtInfrastructure.Interface; +using HuanMeng.DotNetCore.JwtInfrastructure; +using Microsoft.IdentityModel.Tokens; + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.JwtUtil +{ + /// + /// jwt + /// + /// + public class JwtManager(JwtTokenConfig jwtTokenConfig) : IJwtAuthManager + { + + /// + /// 后面可以存储在数据库或分布式缓存中 + /// + private readonly ConcurrentDictionary _usersRefreshTokens = new(); + /// + /// 获取加密字段 + /// + private readonly byte[] _secret = Encoding.UTF8.GetBytes(jwtTokenConfig.Secret); + + /// + /// 删除过期token + /// + /// + public void RemoveExpiredRefreshTokens(DateTime now) + { + var expiredTokens = _usersRefreshTokens.Where(x => x.Value.ExpireAt < now).ToList(); + foreach (var expiredToken in expiredTokens) + { + _usersRefreshTokens.TryRemove(expiredToken.Key, out _); + } + } + + /// + /// 根据用户名删除token + /// + /// + public void RemoveRefreshTokenByUserName(string userName) + { + var refreshTokens = _usersRefreshTokens.Where(x => x.Value.UserName == userName).ToList(); + foreach (var refreshToken in refreshTokens) + { + _usersRefreshTokens.TryRemove(refreshToken.Key, out _); + } + } + + /// + /// 创建token + /// + /// 用户名 + /// 用户项 + /// 过期时间 + /// + public JwtAuthResult GenerateTokens(string username, Claim[] claims, DateTime now) + { + var shouldAddAudienceClaim = string.IsNullOrWhiteSpace(claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Aud)?.Value); + //创建token + var jwtToken = new JwtSecurityToken( + jwtTokenConfig.Issuer, + shouldAddAudienceClaim ? jwtTokenConfig.Audience : string.Empty, + claims, + expires: now.AddMinutes(jwtTokenConfig.AccessTokenExpiration), + signingCredentials: new SigningCredentials(new SymmetricSecurityKey(_secret), SecurityAlgorithms.HmacSha256Signature)); + var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtToken); + + //创建刷新token + var refreshToken = new JwtRefreshToken + { + UserName = username, + TokenString = GenerateRefreshTokenString(), + ExpireAt = now.AddMinutes(jwtTokenConfig.RefreshTokenExpiration) + }; + //_usersRefreshTokens.AddOrUpdate(refreshToken.TokenString, refreshToken, (_, _) => refreshToken); + + return new JwtAuthResult + { + AccessToken = accessToken, + RefreshToken = refreshToken + }; + } + + /// + /// 刷新token + /// + /// + /// + /// + /// + /// + public JwtAuthResult Refresh(string refreshToken, string accessToken, DateTime now) + { + var (principal, jwtToken) = DecodeJwtToken(accessToken); + if (jwtToken == null || !jwtToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256Signature)) + { + throw new SecurityTokenException("无效的token"); + } + + var userName = principal.Identity?.Name; + if (!_usersRefreshTokens.TryGetValue(refreshToken, out var existingRefreshToken)) + { + throw new SecurityTokenException("token已失效"); + } + if (existingRefreshToken.UserName != userName || existingRefreshToken.ExpireAt < now) + { + throw new SecurityTokenException("token不匹配"); + } + //创建新的token + return GenerateTokens(userName, principal.Claims.ToArray(), now); + } + /// + /// 解析token + /// + /// + /// + /// + public (ClaimsPrincipal, JwtSecurityToken?) DecodeJwtToken(string token) + { + if (string.IsNullOrWhiteSpace(token)) + { + throw new SecurityTokenException("token不能为空"); + } + var principal = new JwtSecurityTokenHandler() + .ValidateToken(token, + new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = jwtTokenConfig.Issuer, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(_secret), + ValidAudience = jwtTokenConfig.Audience, + ValidateAudience = true, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(5) + }, + out var validatedToken); + return (principal, validatedToken as JwtSecurityToken); + } + + + /// + /// 获取刷新的token + /// + /// + private static string GenerateRefreshTokenString() + { + var randomNumber = new byte[32]; + using var randomNumberGenerator = RandomNumberGenerator.Create(); + randomNumberGenerator.GetBytes(randomNumber); + return Convert.ToBase64String(randomNumber); + } + + public IImmutableDictionary UsersRefreshTokensReadOnlyDictionary => throw new NotImplementedException(); + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/JwtUtil/JwtTokenManageExtension.cs b/src/0-core/HuanMeng.MiaoYu.Code/JwtUtil/JwtTokenManageExtension.cs new file mode 100644 index 0000000..932ddba --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/JwtUtil/JwtTokenManageExtension.cs @@ -0,0 +1,69 @@ +using HuanMeng.DotNetCore.JwtInfrastructure.Interface; +using HuanMeng.DotNetCore.JwtInfrastructure; +using Microsoft.Extensions.Hosting; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; + +namespace HuanMeng.MiaoYu.Code.JwtUtil +{ + /// + /// + /// + public static class JwtTokenManageExtension + { + /// + /// 添加jwt安全验证,配置 + /// + /// + /// + /// + public static void AddJwtConfig(this IHostApplicationBuilder builder) + { + var jwtTokenConfig = builder.Configuration.GetSection("JwtTokenConfig").Get()!; + //注册一个jwtTokenConfig的单例服务 + builder.Services.AddSingleton(jwtTokenConfig); + // + builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(options => + { + options.RequireHttpsMetadata = true; + options.SaveToken = true; + //调试使用 + //options.Events = new JwtDebugBearerEvents().GetJwtBearerEvents(); + options.TokenValidationParameters = new TokenValidationParameters + { + //是否验证颁发者 + ValidateIssuer = true, + //是否验证受众 + ValidateAudience = true, + //指定是否验证令牌的生存期。设置为 true 表示要进行验证。 + ValidateLifetime = true, + //指定是否验证颁发者签名密钥。设置为 true 表示要进行验证。 + ValidateIssuerSigningKey = true, + //颁发者 + ValidIssuer = jwtTokenConfig.Issuer, + //受众 + ValidAudience = jwtTokenConfig.Audience, + //指定用于验证颁发者签名的密钥 + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtTokenConfig.Secret)), + //指定允许令牌的时钟偏移。允许令牌的过期时间与实际时间之间存在的时间差。在这里设置为 5 分钟,表示允许令牌的时钟偏移为 5 分钟。 + ClockSkew = TimeSpan.FromMinutes(5) + }; + }); + //注册一个JwtAuthManager的单例服务 + builder.Services.AddSingleton(); + } + + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/MultiTenantUtil/MiaoYuMultiTenantConfig.cs b/src/0-core/HuanMeng.MiaoYu.Code/MultiTenantUtil/MiaoYuMultiTenantConfig.cs new file mode 100644 index 0000000..9be00b5 --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/MultiTenantUtil/MiaoYuMultiTenantConfig.cs @@ -0,0 +1,49 @@ +using HuanMeng.DotNetCore.MultiTenant; +using HuanMeng.DotNetCore.MultiTenant.Contract; + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.MultiTenantUtil +{ + /// + /// 租户配置项 + /// + public class MiaoYuMultiTenantConfig + { + public MiaoYuMultiTenantConfig(string conn) + { + var tenantInfo = new TenantInfo() + { + + }; + + tenantInfo.ConnectionString = conn ?? "Server=192.168.195.2;Database=MiaoYu;User Id=zpc;Password=zpc;TrustServerCertificate=true;"; + tenantInfo.Identifier = "default"; + tenantInfo.TenantId = Guid.Empty; + tenantInfo.Name = "default"; + TenantInfos.Add(tenantInfo); + } + + /// + /// + /// + public List TenantInfos { get; set; } = new List(); + + + /// + /// 获取默认 + /// + /// + public TenantInfo GetMultiTenantCfgDefault() + { + var config = TenantInfos.FirstOrDefault(); + + return config; + } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/MultiTenantUtil/MiaoYuMultiTenantExtension.cs b/src/0-core/HuanMeng.MiaoYu.Code/MultiTenantUtil/MiaoYuMultiTenantExtension.cs new file mode 100644 index 0000000..3b0cee9 --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/MultiTenantUtil/MiaoYuMultiTenantExtension.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Builder; + +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Configuration; +using HuanMeng.DotNetCore.MultiTenant; +using HuanMeng.MiaoYu.Model.DbSqlServer.Db_MiaoYu; +using Microsoft.EntityFrameworkCore; + +namespace HuanMeng.MiaoYu.Code.MultiTenantUtil +{ + /// + /// 多租户扩展 + /// + public static class MiaoYuMultiTenantExtension + { + /// + /// 多租户IServiceCollection扩展 + /// + /// + public static void AddMultiTenantMiaoYu(this IHostApplicationBuilder builder) + { + + //初始学生数据库 + string MiaoYu_SqlServer_Db = builder.Configuration.GetConnectionString("MiaoYu_SqlServer_Db") ?? ""; + //添加配置项 + //string SunnySports_SqlServer_Db_SunnySport_Admin = builder.Configuration.GetConnectionString("MiaoYu_SqlServer_Db_Admin") ?? ""; + MiaoYuMultiTenantConfig miaoYuMultiTenantConfig = new MiaoYuMultiTenantConfig(MiaoYu_SqlServer_Db); builder.Services.AddSingleton(miaoYuMultiTenantConfig); + //添加注入全部的多租户配置项 + //builder.Services.AddSingleton(sunnySportsMultiTenantConfig); + ////添加单个租户的配置项 + builder.Services.AddScoped(); + ////添加教师端用户 + //builder.Services.AddScoped(); + //builder.Services.AddScoped(); + //添加DB + //var iDbLog = LoggerFactory.Create(b => b.AddConsole().AddFilter("", LogLevel.Information)); + //添加系统数据库 + builder.Services.AddDbContext((serviceProvider, options) => + { + var m = serviceProvider.GetRequiredService(); + string sunnySportConnectionString = ""; + if (m != null) + { + sunnySportConnectionString = m.ConnectionString ?? MiaoYu_SqlServer_Db; + } + if (string.IsNullOrEmpty(sunnySportConnectionString)) + { + sunnySportConnectionString = MiaoYu_SqlServer_Db; + } + options + .UseSqlServer(sunnySportConnectionString); + //options.UseSqlServer + }, ServiceLifetime.Scoped); + + + } + /// + /// 多租户IApplicationBuilder扩展 + /// + /// + /// + public static IApplicationBuilder UseMultiTenantMiaoYu(this IApplicationBuilder app) + { + return app.UseMiddleware(); + } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/MultiTenantUtil/MiaoYuMultiTenantTenantMiddleware.cs b/src/0-core/HuanMeng.MiaoYu.Code/MultiTenantUtil/MiaoYuMultiTenantTenantMiddleware.cs new file mode 100644 index 0000000..1deb72a --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/MultiTenantUtil/MiaoYuMultiTenantTenantMiddleware.cs @@ -0,0 +1,53 @@ +using HuanMeng.DotNetCore.MultiTenant; + +using Microsoft.AspNetCore.Http; + +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.MultiTenantUtil +{ + /// + /// 多租户中间件 + /// + public class MiaoYuMultiTenantTenantMiddleware + { + private readonly RequestDelegate _next; + public MiaoYuMultiTenantTenantMiddleware(RequestDelegate next) + { + _next = next; + } + + + /// + /// 根据HttpContext获取并设置当前租户ID + /// + /// + /// + /// + /// + public virtual async Task Invoke(HttpContext context, + IServiceProvider _serviceProvider, + TenantInfo tenantInfo, + MiaoYuMultiTenantConfig miaoYuMultiTenantConfig + ) + { + if (tenantInfo == null) + { + tenantInfo = new TenantInfo(); + } + var _ten = miaoYuMultiTenantConfig.GetMultiTenantCfgDefault(); + tenantInfo.ConnectionString = _ten.ConnectionString; + tenantInfo.Identifier = _ten.Identifier; + tenantInfo.TenantId = _ten.TenantId; + tenantInfo.Name = _ten.Name; + await _next.Invoke(context); + } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Other/JwtTokenManageExtension.cs b/src/0-core/HuanMeng.MiaoYu.Code/Other/JwtTokenManageExtension.cs new file mode 100644 index 0000000..abb1aac --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/Other/JwtTokenManageExtension.cs @@ -0,0 +1,23 @@ +using HuanMeng.DotNetCore.JwtInfrastructure; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using HuanMeng.DotNetCore.JwtInfrastructure.Interface; + +namespace HuanMeng.MiaoYu.Code.Other +{ + /// + /// jwt接口验证 + /// + public static class JwtTokenManageExtension + { + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Other/PhoneNumberValidator.cs b/src/0-core/HuanMeng.MiaoYu.Code/Other/PhoneNumberValidator.cs new file mode 100644 index 0000000..57611c9 --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/Other/PhoneNumberValidator.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.Other +{ + /// + /// 手机号 + /// + public class PhoneNumberValidator + { + // 正则表达式用于匹配手机号码。可以根据需要调整以适应不同的国家或地区。 + private static readonly Regex phoneNumberRegex = new Regex(@"^(1[3-9]\d{9})$"); + + public static bool IsPhoneNumber(string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + return false; + } + return phoneNumberRegex.IsMatch(input); + } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/TencentUtile/TencentBaseConfig.cs b/src/0-core/HuanMeng.MiaoYu.Code/TencentUtile/TencentBaseConfig.cs new file mode 100644 index 0000000..8b196e9 --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/TencentUtile/TencentBaseConfig.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.TencentUtile +{ + /// + /// 腾讯云配置 + /// + public class TencentBaseConfig + { + /// + /// 腾讯云id + /// + public string SecretId { get; set; } + /// + /// 密钥 + /// + public string SecretKey { get; set; } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/TencentUtile/TencentConfig.cs b/src/0-core/HuanMeng.MiaoYu.Code/TencentUtile/TencentConfig.cs new file mode 100644 index 0000000..04a471a --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/TencentUtile/TencentConfig.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.TencentUtile +{ + /// + /// + /// + public class TencentConfig : TencentBaseConfig + { + /// + /// 短信验证码接口 + /// + public TencentSMSConfig SMSCode { get; set; } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/TencentUtile/TencentExtension.cs b/src/0-core/HuanMeng.MiaoYu.Code/TencentUtile/TencentExtension.cs new file mode 100644 index 0000000..717557c --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/TencentUtile/TencentExtension.cs @@ -0,0 +1,48 @@ +using HuanMeng.MiaoYu.Code.Users.UserAccount.Contract; +using HuanMeng.MiaoYu.Code.Users.UserAccount.VerificationCodeManager; + +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace HuanMeng.MiaoYu.Code.TencentUtile +{ + /// + /// 腾讯云扩展 + /// + public static class TencentExtension + { + // + /// 腾讯云 + /// + /// + public static void AddTencent(this IHostApplicationBuilder builder) + { + var tencentConfig = builder.Configuration.GetSection("TencentCloud").Get(); + if (tencentConfig == null) + { + tencentConfig = new TencentConfig(); + } + if (tencentConfig.SMSCode == null) + { + tencentConfig.SMSCode = new TencentSMSConfig() { }; + } + if (string.IsNullOrEmpty(tencentConfig.SMSCode.SecretId)) + { + tencentConfig.SMSCode.SecretId = tencentConfig.SecretId; + } + if (string.IsNullOrEmpty(tencentConfig.SMSCode.SecretKey)) + { + tencentConfig.SMSCode.SecretKey = tencentConfig.SecretKey; + } + //注册一个验证码的服务 + builder.Services.AddSingleton(tencentConfig); + + } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/TencentUtile/TencentSMSConfig.cs b/src/0-core/HuanMeng.MiaoYu.Code/TencentUtile/TencentSMSConfig.cs new file mode 100644 index 0000000..99f7c1d --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/TencentUtile/TencentSMSConfig.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.TencentUtile +{ + /// + /// 模板短信 + /// + public class TencentSMSConfig : TencentBaseConfig + { + /// + /// 请求方式 + /// + public string ReqMethod { get; set; } + + /// + /// 超时时间,秒 + /// + public int Timeout { get; set; } + /// + /// 短信应用ID: + /// + public string SmsSdkAppId { get; set; } + + /// + /// 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名 + /// + public string SignName { get; set; } + + /// + /// 短信模板Id,必须填写已审核通过的模板 + /// + public string TemplateId { get; set; } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/Contract/BaseLoginParams.cs b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/Contract/BaseLoginParams.cs new file mode 100644 index 0000000..b6ee5ad --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/Contract/BaseLoginParams.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.Users.UserAccount.Contract +{ + /// + /// 登录参数 + /// + public abstract class BaseLoginParams + { + } + + /// + ///登录返回参数 + /// + public class LoginAccountInfo + { + /// + /// 用户Id + /// + public int UserId { get; set; } + + /// + /// 用户昵称 + /// + public string NickName { get; set; } + + /// + /// + /// + public string Token { get; set; } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/Contract/ISendVerificationCode.cs b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/Contract/ISendVerificationCode.cs new file mode 100644 index 0000000..791820e --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/Contract/ISendVerificationCode.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.Users.UserAccount.Contract +{ + /// + /// 发送验证码 + /// + public interface ISendVerificationCode + { + /// + /// 发送验证码 + /// + /// + public Task SendVerificationCode(BaseSendVerificationCode baseSendVerificationCode); + + } + + /// + /// 发送验证码需要的字段 + /// + public class BaseSendVerificationCode + { + + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/Contract/IUserAccount.cs b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/Contract/IUserAccount.cs index ab0d136..a92778d 100644 --- a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/Contract/IUserAccount.cs +++ b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/Contract/IUserAccount.cs @@ -14,7 +14,8 @@ namespace HuanMeng.MiaoYu.Code.Users.UserAccount.Contract /// /// 登录 /// + /// 登录参数 /// - public abstract bool Login(); + public abstract Task LoginAsync(BaseLoginParams loginParams); } } diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/Contract/IVerificationCodeManager.cs b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/Contract/IVerificationCodeManager.cs new file mode 100644 index 0000000..6728c2d --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/Contract/IVerificationCodeManager.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.Users.UserAccount.Contract +{ + /// + /// 验证码管理类 + /// + public interface IVerificationCodeManager + { + /// + /// 判断验证码是否已经过期 + /// + /// + /// + /// + bool IsVerificationCode(string key); + + /// + /// 判断验证码是否已经过期 + /// + /// + /// + /// + bool IsExpireVerificationCode(string key, string code); + + /// + /// 获取验证码 + /// + /// + /// + VerificationCodeResult GetVerificationCode(string key); + + /// + /// 生成验证码 + /// + /// + /// + /// + /// + VerificationCodeResult GenerateVerificationCode(string key, string code, DateTime now); + + /// + /// 刷新验证码 + /// + /// + /// + /// + /// + VerificationCodeResult Refresh(string key, string code, DateTime now); + + /// + /// 删除过期的刷新令牌 + /// + /// + void RemoveExpiredRefreshCodes(DateTime now); + + /// + /// 删除单个令牌 + /// + /// + void RemoveExpiredRefreshCodes(string key); + + /// + /// 程序结束时保存 + /// + void SaveVerificationCode(); + + /// + /// 程序运行时加载数据 + /// + void LoadVerificationCode(); + + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/Contract/VerificationCodeResult.cs b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/Contract/VerificationCodeResult.cs new file mode 100644 index 0000000..6e2df95 --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/Contract/VerificationCodeResult.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.Users.UserAccount.Contract +{ + /// + /// 验证码 + /// + public class VerificationCodeResult + { + /// + /// key + /// + public string Key { get; set; } + + /// + /// 验证码 + /// + public string Code { get; set; } + + /// + /// 过期时间 + /// + public DateTime ExpireAt { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreateAt { get; set; } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/PhoneAccount.cs b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/PhoneAccount.cs deleted file mode 100644 index 81fe380..0000000 --- a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/PhoneAccount.cs +++ /dev/null @@ -1,26 +0,0 @@ -using HuanMeng.MiaoYu.Code.Users.UserAccount.Contract; - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace HuanMeng.MiaoYu.Code.Users.UserAccount -{ - /// - /// 手机号登录 - /// - /// 手机号 - public class PhoneAccount(string phone) : IUserAccount - { - public Task SendPhone() - { - - } - public override bool Login() - { - return false; - } - } -} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/PhoneAccount/PhoneAccountLogin.cs b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/PhoneAccount/PhoneAccountLogin.cs new file mode 100644 index 0000000..a8a110b --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/PhoneAccount/PhoneAccountLogin.cs @@ -0,0 +1,184 @@ +using HuanMeng.DotNetCore.Base; +using HuanMeng.MiaoYu.Code.DataAccess; +using HuanMeng.MiaoYu.Code.TencentUtile; +using HuanMeng.MiaoYu.Code.Users.UserAccount.Contract; +using HuanMeng.MiaoYu.Code.Users.UserAccount.VerificationCodeManager; +using HuanMeng.MiaoYu.Model.DbSqlServer.Db_MiaoYu; + +using Microsoft.EntityFrameworkCore; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.Users.UserAccount.PhoneAccount +{ + + /// + /// 手机号登录 + /// + /// 手机号 + public class PhoneAccountLogin(IVerificationCodeManager memoryVerificationCodeManager, TencentConfig tencentConfig, DAO dao) : IUserAccount + { + /// + /// 发送手机验证码 + /// + /// + /// + public async Task> SendPhone(string phone) + { + if (!memoryVerificationCodeManager.IsVerificationCode(phone)) + { + return new BaseResponse(ResonseCode.ManyRequests, "发送验证码频繁,清稍后再试!", false); + } + Random random = new Random(); + var verificationCode = random.Next(100000, 1000000); + //先将验证码放到内存中,防止多次请求 + //将验证码放到内存中控制 + var verificationCodeModel = new VerificationCodeResult(); + try + { + verificationCodeModel = memoryVerificationCodeManager.GenerateVerificationCode(phone, verificationCode.ToString(), DateTime.Now.AddMinutes(5)); + } + catch (InvalidOperationException ex) + { + return new BaseResponse(ResonseCode.ManyRequests, "发送验证码频繁,清稍后再试!", false); + } + catch (Exception ex) + { + return new BaseResponse(ResonseCode.Error, "出现异常", false); + } + var day = int.Parse(DateTime.Now.ToString("yyyyMMdd")); + var phoneCount = dao.daoDbMiaoYu.context.T_Verification_Code.Where(it => it.Key == phone && it.CreateDay == day).Count(); + if (phoneCount >= 5) + { + memoryVerificationCodeManager.RemoveExpiredRefreshCodes(phone); + return new BaseResponse(ResonseCode.Error, "当日请求次数太多", false); + } + //使用腾讯云短信接口 + ISendVerificationCode sendVerificationCode = new TencentSMSSendVerificationCode(tencentConfig.SMSCode); + TencentSMSVerificationCode tencentSMSVerificationCode = new TencentSMSVerificationCode() + { + PhoneNum = phone, + VerificationCode = verificationCode.ToString(), + TimeOutInMinutes = 5.ToString() + }; + //发送验证码 + var isSend = await sendVerificationCode.SendVerificationCode(tencentSMSVerificationCode); + if (!isSend) + { + memoryVerificationCodeManager.RemoveExpiredRefreshCodes(phone); + return new BaseResponse(ResonseCode.Error, "验证码发送失败", false); + } + T_Verification_Code t_Verification_Code = new T_Verification_Code() + { + Code = verificationCodeModel.Code, + CreateAt = DateTime.Now, + ExpireAt = verificationCodeModel.ExpireAt, + CreateDay = day, + Key = phone, + Remarks = "登录验证码", + VerificationType = 0, + TenantId = dao.daoDbMiaoYu.context?.TenantInfo?.TenantId ?? Guid.Empty, + }; + dao.daoDbMiaoYu.Add(t_Verification_Code); + dao.daoDbMiaoYu.context.SaveChanges(); + return new BaseResponse(ResonseCode.Success, "验证码发送成功", true); + } + + public async override Task LoginAsync(BaseLoginParams loginParams) + { + var phoneLoginParams = loginParams as PhoneLoginParams; + if (phoneLoginParams == null) + { + throw new ArgumentNullException("登录参数异常"); + } + if (string.IsNullOrEmpty(phoneLoginParams.PhoneNumber)) + { + throw new ArgumentNullException("请输入手机号码"); + } + if (string.IsNullOrEmpty(phoneLoginParams.VerificationCode)) + { + throw new ArgumentNullException("请输入验证码"); + } + if (!memoryVerificationCodeManager.IsExpireVerificationCode(phoneLoginParams.PhoneNumber, phoneLoginParams.VerificationCode)) + { + throw new ArgumentNullException("验证码已失效"); + } + var userlogin = dao.daoDbMiaoYu.context.T_User_Phone_Account.Where(it => it.PhoneNum == phoneLoginParams.PhoneNumber).FirstOrDefault(); + T_User? user = null; + T_User_Data? userData = null; + if (userlogin != null) + { + user = await dao.daoDbMiaoYu.context.T_User.FirstOrDefaultAsync(it => it.Id == userlogin.UserId); + userData = await dao.daoDbMiaoYu.context.T_User_Data.FirstOrDefaultAsync(it => it.UserId == userlogin.UserId); + } + if (user == null) + { + user = new T_User() + { + UpdatedAt = DateTime.Now, + CreatedAt = DateTime.Now, + IsActive = true, + LastLoginAt = DateTime.Now, + LastLoginTypeAt = 1, + Email = "", + NickName = "新用户", + PhoneNum = phoneLoginParams.PhoneNumber, + RegisterType = 1, + TenantId = dao.daoDbMiaoYu.context.TenantInfo.TenantId, + UserName = phoneLoginParams.PhoneNumber, + + }; + dao.daoDbMiaoYu.context.T_User.Add(user); + dao.daoDbMiaoYu.context.SaveChanges(); + } + if (userData == null) + { + userData = new T_User_Data() + { + CreatedAt_ = DateTime.Now, + Currency = 0, + NickName = user.NickName, + UpdatedAt = DateTime.Now, + VipType = 0, + UserId = user.Id, + TenantId = dao.daoDbMiaoYu.context.TenantInfo.TenantId, + UserIconUrl = "", + + }; + dao.daoDbMiaoYu.context.Add(userData); + } + if (userlogin == null) + { + userlogin = new T_User_Phone_Account() + { + PhoneNum = phoneLoginParams.PhoneNumber, + UserId = user.Id, + VerificationCode = phoneLoginParams.VerificationCode, + CreatedAt = DateTime.Now, + LastLoginAt = DateTime.Now, + NikeName = user.NickName, + TenantId = dao.daoDbMiaoYu.context.TenantInfo.TenantId, + UpdatedAt = DateTime.Now, + }; + dao.daoDbMiaoYu.context.Add(userlogin); + } + user.LastLoginAt = DateTime.Now; + user.LastLoginTypeAt = 1; + user.IsActive = true; + user.Ip = phoneLoginParams.Ip; + dao.daoDbMiaoYu.context.SaveChanges(); + LoginAccountInfo loginAccountInfo = new LoginAccountInfo() + { + UserId = user.Id, + NickName = user.NickName, + }; + return loginAccountInfo; + } + + + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/PhoneAccount/PhoneLoginParams.cs b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/PhoneAccount/PhoneLoginParams.cs new file mode 100644 index 0000000..035effe --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/PhoneAccount/PhoneLoginParams.cs @@ -0,0 +1,31 @@ +using HuanMeng.MiaoYu.Code.Users.UserAccount.Contract; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.Users.UserAccount.PhoneAccount +{ + /// + /// 登录参数 + /// + public class PhoneLoginParams : BaseLoginParams + { + /// + /// 手机号码 + /// + public string PhoneNumber { get; set; } + + /// + /// 验证码 + /// + public string VerificationCode { get; set; } + + /// + /// Ip + /// + public string Ip { get; set; } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/TencentSMSSendVerificationCode.cs b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/TencentSMSSendVerificationCode.cs new file mode 100644 index 0000000..f065d03 --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/TencentSMSSendVerificationCode.cs @@ -0,0 +1,178 @@ +using HuanMeng.MiaoYu.Code.Users.UserAccount.Contract; + +using TencentCloud.Common; +using TencentCloud.Common.Profile; +using TencentCloud.Sms.V20210111; +using TencentCloud.Sms.V20210111.Models; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Mail; +using System.Text; +using System.Threading.Tasks; +using System.Reflection.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using HuanMeng.MiaoYu.Code.TencentUtile; + + +namespace HuanMeng.MiaoYu.Code.Users.UserAccount +{ + /// + /// 腾讯云发送短信 + /// + /// + public class TencentSMSSendVerificationCode(TencentSMSConfig tencentSMSConfig) : ISendVerificationCode + { + public async Task SendVerificationCode(BaseSendVerificationCode baseSendVerificationCode) + { + var code = baseSendVerificationCode as TencentSMSVerificationCode; + if (code == null) + { + throw new ArgumentNullException("参数错误"); + } + string phoneNum = code.PhoneNum; + string verificationCode = code.VerificationCode; + if (!phoneNum.StartsWith("+86")) + { + phoneNum = "+86" + phoneNum; + } + try + { + // 必要步骤: + // 实例化一个认证对象,入参需要传入腾讯云账户密钥对 SecretId,SecretKey。 + // 为了保护密钥安全,建议将密钥设置在环境变量中或者配置文件中。 + // 硬编码密钥到代码中有可能随代码泄露而暴露,有安全隐患,并不推荐。 + // 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。 + // SecretId、SecretKey 查询:https://console.cloud.tencent.com/cam/capi + Credential cred = new Credential + { + SecretId = tencentSMSConfig.SecretId, + SecretKey = tencentSMSConfig.SecretKey + }; + + + /* 非必要步骤: + * 实例化一个客户端配置对象,可以指定超时时间等配置 */ + ClientProfile clientProfile = new ClientProfile(); + /* SDK默认用TC3-HMAC-SHA256进行签名 + * 非必要请不要修改这个字段 */ + clientProfile.SignMethod = ClientProfile.SIGN_TC3SHA256; + /* 非必要步骤 + * 实例化一个客户端配置对象,可以指定超时时间等配置 */ + HttpProfile httpProfile = new HttpProfile(); + /* SDK默认使用POST方法。 + * 如果您一定要使用GET方法,可以在这里设置。GET方法无法处理一些较大的请求 */ + httpProfile.ReqMethod = tencentSMSConfig.ReqMethod; + httpProfile.Timeout = tencentSMSConfig.Timeout; // 请求连接超时时间,单位为秒(默认60秒) + /* 指定接入地域域名,默认就近地域接入域名为 sms.tencentcloudapi.com ,也支持指定地域域名访问,例如广州地域的域名为 sms.ap-guangzhou.tencentcloudapi.com */ + httpProfile.Endpoint = "sms.tencentcloudapi.com"; + // 代理服务器,当您的环境下有代理服务器时设定(无需要直接忽略) + // httpProfile.WebProxy = Environment.GetEnvironmentVariable("HTTPS_PROXY"); + + + clientProfile.HttpProfile = httpProfile; + /* 实例化要请求产品(以sms为例)的client对象 + * 第二个参数是地域信息,可以直接填写字符串ap-guangzhou,支持的地域列表参考 https://cloud.tencent.com/document/api/382/52071#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8 */ + SmsClient client = new SmsClient(cred, "ap-nanjing", clientProfile); + + + /* 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数 + * 您可以直接查询SDK源码确定SendSmsRequest有哪些属性可以设置 + * 属性可能是基本类型,也可能引用了另一个数据结构 + * 推荐使用IDE进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明 */ + SendSmsRequest req = new SendSmsRequest(); + + + /* 基本类型的设置: + * SDK采用的是指针风格指定参数,即使对于基本类型您也需要用指针来对参数赋值。 + * SDK提供对基本类型的指针引用封装函数 + * 帮助链接: + * 短信控制台: https://console.cloud.tencent.com/smsv2 + * 腾讯云短信小助手: https://cloud.tencent.com/document/product/382/3773#.E6.8A.80.E6.9C.AF.E4.BA.A4.E6.B5.81 */ + /* 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666 */ + // 应用 ID 可前往 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 查看 + req.SmsSdkAppId = tencentSMSConfig.SmsSdkAppId; + + + /* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名 */ + // 签名信息可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-sign) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-sign) 的签名管理查看 + req.SignName = tencentSMSConfig.SignName; + + + /* 模板 ID: 必须填写已审核通过的模板 ID */ + // 模板 ID 可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-template) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-template) 的正文模板管理查看 + req.TemplateId = tencentSMSConfig.TemplateId; + + + /* 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,若无模板参数,则设置为空 */ + req.TemplateParamSet = new String[] { verificationCode, code.TimeOutInMinutes }; + + + /* 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号] + * 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号*/ + req.PhoneNumberSet = new String[] { phoneNum }; + + + /* 用户的 session 内容(无需要可忽略): 可以携带用户侧 ID 等上下文信息,server 会原样返回 */ + req.SessionContext = ""; + + + /* 短信码号扩展号(无需要可忽略): 默认未开通,如需开通请联系 [腾讯云短信小助手] */ + req.ExtendCode = ""; + + + /* 国内短信无需填写该项;国际/港澳台短信已申请独立 SenderId 需要填写该字段,默认使用公共 SenderId,无需填写该字段。注:月度使用量达到指定量级可申请独立 SenderId 使用,详情请联系 [腾讯云短信小助手](https://cloud.tencent.com/document/product/382/3773#.E6.8A.80.E6.9C.AF.E4.BA.A4.E6.B5.81)。 */ + req.SenderId = ""; + + SendSmsResponse resp = await client.SendSms(req); + code.SendSmsResponse = resp; + // 输出json格式的字符串回包 + Console.WriteLine(AbstractModel.ToJsonString(resp)); + + } + catch (Exception e) + { + code.exception = e; + Console.WriteLine(e.ToString()); + return false; + } + //Console.Read(); + return true; + + + } + } + /// + /// 手机号 + /// + public class TencentSMSVerificationCode : BaseSendVerificationCode + { + /// + /// 手机号 + /// + public string PhoneNum { get; set; } + + /// + /// 验证码 + /// + public string VerificationCode { get; set; } + + /// + /// 超时时间 + /// + public string TimeOutInMinutes { get; set; } + + /// + /// 请求返回内容 + /// + public SendSmsResponse SendSmsResponse { get; set; } + + /// + /// 异常信息 + /// + public Exception exception { get; set; } + } + + +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/TokenAccountLogin.cs b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/TokenAccountLogin.cs new file mode 100644 index 0000000..480c9d6 --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/TokenAccountLogin.cs @@ -0,0 +1,100 @@ +using HuanMeng.DotNetCore.JwtInfrastructure.Interface; +using HuanMeng.MiaoYu.Code.DataAccess; +using HuanMeng.MiaoYu.Code.TencentUtile; +using HuanMeng.MiaoYu.Code.Users.UserAccount.Contract; +using HuanMeng.MiaoYu.Code.Users.UserAccount.PhoneAccount; + +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; + +using Newtonsoft.Json.Linq; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.Users.UserAccount +{ + /// + /// 使用token + /// + /// + /// + public class TokenAccountLogin(IJwtAuthManager jwtAuthManager, DAO dao) : IUserAccount + { + public override async Task LoginAsync(BaseLoginParams loginParams) + { + var tokenLoginParams = loginParams as TokenLoginParams; + if (tokenLoginParams == null) + { + throw new ArgumentNullException("登录参数异常"); + } + + var (principal, jwtToken) = jwtAuthManager.DecodeJwtToken(tokenLoginParams.Token); + if (jwtToken == null || !jwtToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256Signature)) + { + throw new SecurityTokenException("无效的token"); + } + + var exp = principal.FindFirst("exp")?.Value; + if (string.IsNullOrEmpty(exp)) + { + throw new SecurityTokenException("无效的token"); + } + var exptime = long.Parse(exp); + + long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + if (exptime < timestamp) + { + throw new SecurityTokenException("token已经过期"); + } + + var userIdStr = principal.FindFirst("UserId")?.Value; + if (string.IsNullOrEmpty(userIdStr)) + { + throw new SecurityTokenException("无效的token"); + } + var userId = int.Parse(userIdStr); + var user = await dao.daoDbMiaoYu.context.T_User.FirstOrDefaultAsync(it => it.Id == userId); + if (user == null) + { + throw new SecurityTokenException("用户未注册"); + } + user.LastLoginAt = DateTime.Now; + user.LastLoginTypeAt = 0; + user.IsActive = true; + user.Ip = tokenLoginParams.Ip; + dao.daoDbMiaoYu.context.SaveChanges(); + LoginAccountInfo loginAccountInfo = new LoginAccountInfo() + { + UserId = user.Id, + NickName = user.NickName, + }; + DateTime dateTime = DateTimeOffset.FromUnixTimeSeconds(exptime).DateTime; + if (dateTime.Subtract(DateTime.Now).TotalDays > 2) + { + loginAccountInfo.Token = tokenLoginParams.Token; + } + return loginAccountInfo; + } + + } + /// + /// 登录参数 + /// + public class TokenLoginParams : BaseLoginParams + { + /// + /// token + /// + public string Token { get; set; } + + + /// + /// Ip + /// + public string Ip { get; set; } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/VerificationCodeManager/MemoryVerificationCodeExtension.cs b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/VerificationCodeManager/MemoryVerificationCodeExtension.cs new file mode 100644 index 0000000..6516769 --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/VerificationCodeManager/MemoryVerificationCodeExtension.cs @@ -0,0 +1,32 @@ +using HuanMeng.MiaoYu.Code.Users.UserAccount.Contract; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.Protocols; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.Users.UserAccount.VerificationCodeManager +{ + /// + /// + /// + public static class MemoryVerificationCodeExtension + { + // + /// 验证码扩展 + /// + /// + public static void AddMemoryVerificationCode(this IHostApplicationBuilder builder) + { + //注册一个验证码的服务 + builder.Services.AddSingleton(); + //注册验证码过期的服务器 + builder.Services.AddHostedService(); + } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/VerificationCodeManager/MemoryVerificationCodeManager.cs b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/VerificationCodeManager/MemoryVerificationCodeManager.cs new file mode 100644 index 0000000..cc0a202 --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/VerificationCodeManager/MemoryVerificationCodeManager.cs @@ -0,0 +1,208 @@ +using HuanMeng.MiaoYu.Code.Users.UserAccount.Contract; + +using Newtonsoft.Json; + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.Users.UserAccount.VerificationCodeManager +{ + /// + /// 内存 验证码存放 MemoryVerificationCodeExtension + /// + public class MemoryVerificationCodeManager : IVerificationCodeManager + { + /// + /// 存放数据 + /// + public ConcurrentDictionary MemoryVerificationCode { get; set; } + = new ConcurrentDictionary(); + + public bool IsVerificationCode(string key) + { + //判断是否存在 + if (MemoryVerificationCode.ContainsKey(key)) + { + if (DateTime.Now.Subtract(MemoryVerificationCode[key].CreateAt).TotalSeconds < 60) + { + return false; + } + + } + return true; + } + /// + /// 判断验证码是否已经过期 + /// + /// 手机号 + /// 验证码 + /// + /// 未找到验证码 + public bool IsExpireVerificationCode(string key, string code) + { + if (!MemoryVerificationCode.TryGetValue(key, out var result)) + { + throw new ArgumentNullException("未找到验证码"); + } + if (result.Code == code && result.ExpireAt >= DateTime.Now) + { + return true; + } + return false; + } + + /// + /// 获取验证码 + /// + /// + /// + /// 未找到验证码 + public VerificationCodeResult GetVerificationCode(string key) + { + if (!MemoryVerificationCode.TryGetValue(key, out var result)) + { + throw new ArgumentNullException("未找到验证码"); + } + return result; + } + + + /// + /// 发送验证码 + /// + /// 手机号 + /// 验证码 + /// 过期时间 + /// + /// 验证码重复发送 + public VerificationCodeResult GenerateVerificationCode(string key, string code, DateTime now) + { + + if (!MemoryVerificationCode.TryGetValue(key, out var verificationCodeResult)) + { + verificationCodeResult = new VerificationCodeResult() + { + + }; + MemoryVerificationCode.TryAdd(key, verificationCodeResult); + } + else + { + if (DateTime.Now.Subtract(verificationCodeResult.CreateAt).TotalSeconds < 30) + { + throw new InvalidOperationException("请求发送验证码频繁"); + } + } + verificationCodeResult.Key = key; + verificationCodeResult.Code = code; + verificationCodeResult.ExpireAt = now; + verificationCodeResult.CreateAt = DateTime.Now; + return verificationCodeResult; + + } + + /// + /// 重新发送验证码 + /// + /// + /// + /// + /// + public VerificationCodeResult Refresh(string key, string code, DateTime now) + { + if (!MemoryVerificationCode.TryGetValue(key, out var verificationCodeResult)) + { + verificationCodeResult = new VerificationCodeResult() + { + + }; + MemoryVerificationCode.TryAdd(key, verificationCodeResult); + } + verificationCodeResult.Key = key; + verificationCodeResult.Code = code; + verificationCodeResult.ExpireAt = now; + return verificationCodeResult; + } + + public void RemoveExpiredRefreshCodes(DateTime now) + { + var expiredTokens = MemoryVerificationCode.Where(x => x.Value.ExpireAt < now).ToList(); + foreach (var expiredToken in expiredTokens) + { + MemoryVerificationCode.TryRemove(expiredToken.Key, out _); + } + + } + + public void RemoveExpiredRefreshCodes(string key) + { + MemoryVerificationCode.TryRemove(key, out _); + + } + + public void SaveVerificationCode() + { + try + { + string path = Path.GetFullPath("./output/verificationcode/"); + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + var json = JsonConvert.SerializeObject(MemoryVerificationCode, Formatting.Indented); + var fileName = path + "verificationcode.json"; + using (var file = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite)) + { + file.Position = 0; + file.SetLength(0); + file.Write(System.Text.Encoding.UTF8.GetBytes(json)); + file.Flush(); + } + } + catch (Exception ex) + { + + Console.WriteLine("程序结束时保存验证码出现错误", ex.Message); + } + + + } + + public void LoadVerificationCode() + { + try + { + string path = Path.GetFullPath("./output/verificationcode/"); + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + var fileName = path + "verificationcode.json"; + if (!File.Exists(fileName)) + { + return; + } + using StreamReader streamReader = new StreamReader(fileName); + var str = streamReader.ReadToEnd(); + if (!string.IsNullOrEmpty(str)) + { + var _memoryVerificationCode = JsonConvert.DeserializeObject>(str); + if (_memoryVerificationCode != null) + { + this.MemoryVerificationCode = _memoryVerificationCode; + } + } + } + catch (Exception ex) + { + + Console.WriteLine("加载验证码出现错误", ex.Message); + } + + } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/VerificationCodeManager/MemoryVerificationCodeServer.cs b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/VerificationCodeManager/MemoryVerificationCodeServer.cs new file mode 100644 index 0000000..5fae7c1 --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserAccount/VerificationCodeManager/MemoryVerificationCodeServer.cs @@ -0,0 +1,84 @@ +using HuanMeng.MiaoYu.Code.Users.UserAccount.Contract; + +using Microsoft.Extensions.Hosting; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Code.Users.UserAccount.VerificationCodeManager +{ + /// + /// 验证码服务 + /// + public class MemoryVerificationCodeServer(IVerificationCodeManager manager) : IHostedService, IDisposable + { + /// + /// 定时器 + /// + private Timer _timer = null!; + + /// + /// 开始服务 + /// + /// + /// + public Task StartAsync(CancellationToken cancellationToken) + { + manager.LoadVerificationCode(); + // 每分钟从缓存中删除过期的刷新令牌 + _timer = new Timer(DoWork!, null, TimeSpan.Zero, TimeSpan.FromMinutes(1)); + return Task.CompletedTask; + } + + /// + /// 停止服务 + /// + /// + /// + public Task StopAsync(CancellationToken cancellationToken) + { + manager.SaveVerificationCode(); + _timer.Change(Timeout.Infinite, 0); + return Task.CompletedTask; + } + /// + /// 删除过期token + /// + /// + /// + private void DoWork(object state) + { + + manager.RemoveExpiredRefreshCodes(DateTime.Now); + } + + private bool disposedValue; + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: 释放托管状态(托管对象) + } + _timer.Dispose(); + // TODO: 释放未托管的资源(未托管的对象)并重写终结器 + // TODO: 将大型字段设置为 null + disposedValue = true; + } + } + + + + public void Dispose() + { + // 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中 + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserBLL.cs b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserBLL.cs index 989b14f..86e5662 100644 --- a/src/0-core/HuanMeng.MiaoYu.Code/Users/UserBLL.cs +++ b/src/0-core/HuanMeng.MiaoYu.Code/Users/UserBLL.cs @@ -1,9 +1,19 @@ using HuanMeng.DotNetCore.Base; using HuanMeng.MiaoYu.Code.Base; +using HuanMeng.MiaoYu.Code.Users.UserAccount; +using HuanMeng.MiaoYu.Code.Users.UserAccount.Contract; +using HuanMeng.MiaoYu.Code.Users.UserAccount.PhoneAccount; +using HuanMeng.MiaoYu.Model.Dto; +using HuanMeng.MiaoYu.Model.Dto.Account; + +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.Linq; +using System.Security.Claims; using System.Text; using System.Threading.Tasks; @@ -18,9 +28,91 @@ namespace HuanMeng.MiaoYu.Code.Users { } - public BaseResponse PhoneLogIn() + /// + /// 发送手机号码 + /// + /// + /// + public async Task> SendPhoneNumber(string PhoneNumber) { + PhoneAccountLogin phoneAccountLogin = new PhoneAccountLogin(VerificationCodeManager, TencentConfig, Dao); + var msg = await phoneAccountLogin.SendPhone(PhoneNumber); + return msg; + } + + /// + /// 登录 + /// + /// + /// + /// + public async Task> AccountLogIn(RequestLoginModel requestLoginModel) + { + IUserAccount userAccount = null; + BaseLoginParams loginParams = null; + string ip = HttpContextAccessor.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""; + if (requestLoginModel.LoginType == 0) + { + userAccount = new TokenAccountLogin(JwtAuthManager, Dao); + loginParams = new TokenLoginParams() + { + Token = requestLoginModel.Token, + Ip = ip + }; + } + else if (requestLoginModel.LoginType == 1) + { + userAccount = new PhoneAccountLogin(VerificationCodeManager, TencentConfig, Dao); + loginParams = new PhoneLoginParams() + { + PhoneNumber = requestLoginModel.PhoneNumber, + VerificationCode = requestLoginModel.VerificationCode, + Ip = ip + }; + } + else + { + throw new Exception("不支持的登录方式"); + } + + var accountInfo = await userAccount.LoginAsync(loginParams); + if (string.IsNullOrEmpty(accountInfo.Token)) + { + var claims = new[] + { + new Claim(ClaimTypes.Name,accountInfo.NickName), + new Claim("Name",accountInfo.NickName), + new Claim("UserId",accountInfo.UserId.ToString()), + }; + var jwtAuthResulttre = JwtAuthManager.GenerateTokens(accountInfo.NickName, claims, DateTime.Now); + accountInfo.Token = jwtAuthResulttre.AccessToken; + } + ResponseAccountLogIn responseAccountLogIn = new ResponseAccountLogIn() + { + token = accountInfo.Token, + NickName = accountInfo.NickName, + UserId = accountInfo.UserId, + }; + + return new BaseResponse(ResonseCode.Success, "登录成功", responseAccountLogIn) { }; + } + + /// + /// 获取用户信息 + /// + /// + public async Task> GetUserInfo() + { + var user = await Dao.daoDbMiaoYu.context.T_User.FirstOrDefaultAsync(it => it.Id == _UserId); + var userData = await Dao.daoDbMiaoYu.context.T_User_Data.FirstOrDefaultAsync(it => it.Id == _UserId); + return new BaseResponse(ResonseCode.Success, "请求成功", new ResponseUserInfo + { + NickName = user.NickName, + UserId = user.Id, + Currency = userData.Currency, + UserIconUrl = userData.UserIconUrl + }); } } } diff --git a/src/0-core/HuanMeng.MiaoYu.Model/CodeTemplates/EFCore/DbContext.t4 b/src/0-core/HuanMeng.MiaoYu.Model/CodeTemplates/EFCore/DbContext.t4 index 060913f..0f7bb66 100644 --- a/src/0-core/HuanMeng.MiaoYu.Model/CodeTemplates/EFCore/DbContext.t4 +++ b/src/0-core/HuanMeng.MiaoYu.Model/CodeTemplates/EFCore/DbContext.t4 @@ -47,7 +47,7 @@ namespace <#= NamespaceHint #>; } #> /// -/// ʵ +/// 妙语实体类 /// public partial class <#= Options.ContextName #> : MultiTenantDbContext//DbContext { @@ -321,12 +321,8 @@ public partial class <#= Options.ContextName #> : MultiTenantDbContext//DbContex usings.AddRange(propertyFluentApiCalls.GetRequiredUsings()); #> - j.IndexerProperty<<#= code.Reference(property.ClrType) #>>(<#= code.Literal(property.Name) #>)<#= code.Fragment(propertyFluentApiCalls, indent: 7) #>; - //ȫɸѡ - if (this.TenantInfo != null) - { - entity.HasQueryFilter(it => it.TenantId == this.TenantInfo.TenantId); - } + j.IndexerProperty<<#= code.Reference(property.ClrType) #>>(<#= code.Literal(property.Name) #>)<#= code.Fragment(propertyFluentApiCalls, indent: 7) #>; + <# } #> @@ -334,7 +330,13 @@ public partial class <#= Options.ContextName #> : MultiTenantDbContext//DbContex <# anyEntityTypeConfiguration = true; } + #> + //添加全局筛选器 + if (this.TenantInfo != null) + { + entity.HasQueryFilter(it => it.TenantId == this.TenantInfo.TenantId); + } }); <# // If any signicant code was generated, append it to the main environment diff --git a/src/0-core/HuanMeng.MiaoYu.Model/DbSqlServer/Db_MiaoYu/MiaoYuContext.cs b/src/0-core/HuanMeng.MiaoYu.Model/DbSqlServer/Db_MiaoYu/MiaoYuContext.cs index aad9138..363d32d 100644 --- a/src/0-core/HuanMeng.MiaoYu.Model/DbSqlServer/Db_MiaoYu/MiaoYuContext.cs +++ b/src/0-core/HuanMeng.MiaoYu.Model/DbSqlServer/Db_MiaoYu/MiaoYuContext.cs @@ -1,14 +1,11 @@ -using System; +using System; using System.Collections.Generic; - -using HuanMeng.DotNetCore.MultiTenant.Contract; - using Microsoft.EntityFrameworkCore; namespace HuanMeng.MiaoYu.Model.DbSqlServer.Db_MiaoYu; /// -/// +/// 妙语实体类 /// public partial class MiaoYuContext : MultiTenantDbContext//DbContext { @@ -56,8 +53,12 @@ public partial class MiaoYuContext : MultiTenantDbContext//DbContext /// public virtual DbSet T_User_Phone_Account { get; set; } + /// + /// 验证码表 + /// + public virtual DbSet T_Verification_Code { get; set; } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - //#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263. => optionsBuilder.UseSqlServer("Server=192.168.195.2;Database=MiaoYu;User Id=zpc;Password=zpc;TrustServerCertificate=true;"); protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -75,6 +76,9 @@ public partial class MiaoYuContext : MultiTenantDbContext//DbContext entity.Property(e => e.Email) .HasMaxLength(255) .HasComment("绑定的邮箱"); + entity.Property(e => e.Ip) + .HasMaxLength(100) + .HasComment("Ip地址"); entity.Property(e => e.IsActive).HasComment("是否活跃"); entity.Property(e => e.LastLoginAt) .HasComment("最后一次登录时间") @@ -95,6 +99,11 @@ public partial class MiaoYuContext : MultiTenantDbContext//DbContext entity.Property(e => e.UserName) .HasMaxLength(100) .HasComment("用户姓名"); + //添加全局筛选器 + if (this.TenantInfo != null) + { + entity.HasQueryFilter(it => it.TenantId == this.TenantInfo.TenantId); + } }); modelBuilder.Entity(entity => @@ -112,12 +121,8 @@ public partial class MiaoYuContext : MultiTenantDbContext//DbContext .HasColumnType("datetime") .HasColumnName("CreatedAt "); entity.Property(e => e.Currency).HasComment("货币"); - entity.Property(e => e.IP) - .HasMaxLength(25) - .IsUnicode(false) - .HasComment("Ip"); entity.Property(e => e.NickName) - .HasMaxLength(50) + .HasMaxLength(100) .HasComment("用户昵称,需要和主表保持一致"); entity.Property(e => e.UpdatedAt) .HasComment("更新时间") @@ -126,6 +131,11 @@ public partial class MiaoYuContext : MultiTenantDbContext//DbContext .HasMaxLength(300) .HasComment("用户头像"); entity.Property(e => e.VipType).HasComment("vip类型"); + //添加全局筛选器 + if (this.TenantInfo != null) + { + entity.HasQueryFilter(it => it.TenantId == this.TenantInfo.TenantId); + } }); modelBuilder.Entity(entity => @@ -142,7 +152,7 @@ public partial class MiaoYuContext : MultiTenantDbContext//DbContext .HasComment("最后一次登录时间") .HasColumnType("datetime"); entity.Property(e => e.NikeName) - .HasMaxLength(1) + .HasMaxLength(100) .HasComment("用户昵称"); entity.Property(e => e.PhoneNum) .HasMaxLength(50) @@ -157,6 +167,43 @@ public partial class MiaoYuContext : MultiTenantDbContext//DbContext .HasMaxLength(10) .IsUnicode(false) .HasComment("验证码"); + //添加全局筛选器 + if (this.TenantInfo != null) + { + entity.HasQueryFilter(it => it.TenantId == this.TenantInfo.TenantId); + } + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("PK__T_Verifi__3214EC074DE3F41A"); + + entity.ToTable(tb => tb.HasComment("验证码表")); + + entity.Property(e => e.Id).HasComment("主键"); + entity.Property(e => e.Code) + .HasMaxLength(10) + .HasComment("验证码"); + entity.Property(e => e.CreateAt) + .HasComment("创建时间") + .HasColumnType("datetime"); + entity.Property(e => e.CreateDay).HasComment("创建天"); + entity.Property(e => e.ExpireAt) + .HasComment("过期时间") + .HasColumnType("datetime"); + entity.Property(e => e.Key) + .HasMaxLength(100) + .HasComment("手机号或者邮箱"); + entity.Property(e => e.Remarks) + .HasMaxLength(100) + .HasComment("备注"); + entity.Property(e => e.TenantId).HasComment("租户"); + entity.Property(e => e.VerificationType).HasComment("0手机,1邮箱"); + //添加全局筛选器 + if (this.TenantInfo != null) + { + entity.HasQueryFilter(it => it.TenantId == this.TenantInfo.TenantId); + } }); OnModelCreatingPartial(modelBuilder); diff --git a/src/0-core/HuanMeng.MiaoYu.Model/DbSqlServer/Db_MiaoYu/T_User.cs b/src/0-core/HuanMeng.MiaoYu.Model/DbSqlServer/Db_MiaoYu/T_User.cs index 63160fd..8e97d1e 100644 --- a/src/0-core/HuanMeng.MiaoYu.Model/DbSqlServer/Db_MiaoYu/T_User.cs +++ b/src/0-core/HuanMeng.MiaoYu.Model/DbSqlServer/Db_MiaoYu/T_User.cs @@ -62,4 +62,9 @@ public partial class T_User: MultiTenantEntity /// 首次注册方式 /// public int RegisterType { get; set; } + + /// + /// Ip地址 + /// + public string? Ip { get; set; } } diff --git a/src/0-core/HuanMeng.MiaoYu.Model/DbSqlServer/Db_MiaoYu/T_User_Data.cs b/src/0-core/HuanMeng.MiaoYu.Model/DbSqlServer/Db_MiaoYu/T_User_Data.cs index 057e8b9..17b33ad 100644 --- a/src/0-core/HuanMeng.MiaoYu.Model/DbSqlServer/Db_MiaoYu/T_User_Data.cs +++ b/src/0-core/HuanMeng.MiaoYu.Model/DbSqlServer/Db_MiaoYu/T_User_Data.cs @@ -47,9 +47,4 @@ public partial class T_User_Data: MultiTenantEntity /// 更新时间 /// public DateTime UpdatedAt { get; set; } - - /// - /// Ip - /// - public string? IP { get; set; } } diff --git a/src/0-core/HuanMeng.MiaoYu.Model/DbSqlServer/Db_MiaoYu/T_Verification_Code.cs b/src/0-core/HuanMeng.MiaoYu.Model/DbSqlServer/Db_MiaoYu/T_Verification_Code.cs new file mode 100644 index 0000000..76e28f2 --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Model/DbSqlServer/Db_MiaoYu/T_Verification_Code.cs @@ -0,0 +1,50 @@ +using System; + +namespace HuanMeng.MiaoYu.Model.DbSqlServer.Db_MiaoYu; + +/// +/// 验证码表 +/// +public partial class T_Verification_Code: MultiTenantEntity +{ + /// + /// 主键 + /// + public int Id { get; set; } + + /// + /// 手机号或者邮箱 + /// + public string Key { get; set; } = null!; + + /// + /// 验证码 + /// + public string Code { get; set; } = null!; + + /// + /// 创建天 + /// + public int CreateDay { get; set; } + + /// + /// 过期时间 + /// + public DateTime ExpireAt { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreateAt { get; set; } + + /// + /// 备注 + /// + public string? Remarks { get; set; } + + + /// + /// 0手机,1邮箱 + /// + public int VerificationType { get; set; } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Model/Dto/Account/RequestLoginModel.cs b/src/0-core/HuanMeng.MiaoYu.Model/Dto/Account/RequestLoginModel.cs new file mode 100644 index 0000000..208fc4d --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Model/Dto/Account/RequestLoginModel.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Model.Dto.Account +{ + /// + /// 登录接口 + /// + public class RequestLoginModel : RequestPhoneNumberModel + { + /// + /// 验证码 + /// + public string? VerificationCode { get; set; } + + /// + /// 登录类型,0token登录 1手机号,2邮箱 + /// + public int LoginType { get; set; } + + /// + /// token登录 + /// + public string? Token { get; set; } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Model/Dto/Account/RequestPhoneNumberModel.cs b/src/0-core/HuanMeng.MiaoYu.Model/Dto/Account/RequestPhoneNumberModel.cs new file mode 100644 index 0000000..ad0b92d --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Model/Dto/Account/RequestPhoneNumberModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Model.Dto.Account +{ + /// + /// 发送验证码 + /// + public class RequestPhoneNumberModel + { + /// + /// 手机号码 + /// + public string? PhoneNumber { get; set; } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Model/Dto/Account/ResponseAccountLogIn.cs b/src/0-core/HuanMeng.MiaoYu.Model/Dto/Account/ResponseAccountLogIn.cs new file mode 100644 index 0000000..0078c52 --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Model/Dto/Account/ResponseAccountLogIn.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Model.Dto.Account +{ + /// + /// 登录请求返回参数 + /// + public class ResponseAccountLogIn + { + /// + /// token + /// + public string token { get; set; } + + /// + /// 昵称 + /// + public string NickName { get; set; } + + /// + /// 用户id + /// + public int UserId { get; set; } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Model/Dto/RequestUserInfo.cs b/src/0-core/HuanMeng.MiaoYu.Model/Dto/RequestUserInfo.cs new file mode 100644 index 0000000..2660e4b --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Model/Dto/RequestUserInfo.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Model.Dto +{ + /// + /// 用户信息 + /// + public class RequestUserInfo + { + /// + /// 昵称 + /// + public string NickName { get; set; } + + /// + /// 用户id + /// + public int UserId { get; set; } + } +} diff --git a/src/0-core/HuanMeng.MiaoYu.Model/Dto/ResponseUserInfo.cs b/src/0-core/HuanMeng.MiaoYu.Model/Dto/ResponseUserInfo.cs new file mode 100644 index 0000000..2d1353b --- /dev/null +++ b/src/0-core/HuanMeng.MiaoYu.Model/Dto/ResponseUserInfo.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.MiaoYu.Model.Dto +{ + /// + /// 返回用户信息 + /// + public class ResponseUserInfo + { + /// + /// 货币 + /// + public int Currency { get; set; } + + /// + /// 用户头像 + /// + public string? UserIconUrl { get; set; } + + /// + /// 用户Id + /// + public int UserId { get; set; } + + + /// + /// 用户昵称,需要和主表保持一致 + /// + public string? NickName { get; set; } + } +} diff --git a/src/2-api/HuanMeng.MiaoYu.WebApi/Base/LowercaseParameterFilter.cs b/src/2-api/HuanMeng.MiaoYu.WebApi/Base/LowercaseParameterFilter.cs new file mode 100644 index 0000000..6c99eb9 --- /dev/null +++ b/src/2-api/HuanMeng.MiaoYu.WebApi/Base/LowercaseParameterFilter.cs @@ -0,0 +1,52 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace HuanMeng.MiaoYu.WebApi.Base +{ + /// + /// 自定义参数过滤器 + /// + public class LowercaseParameterFilter : IParameterFilter + { + /// + /// + /// + /// + /// + public void Apply(OpenApiParameter parameter, ParameterFilterContext context) + { + // 将参数名称改为小写开头 + parameter.Name = Char.ToLower(parameter.Name[0]) + parameter.Name.Substring(1); + } + } + + /// + /// 自定义参数过滤器 + /// + public class LowercaseRequestFilter : IRequestBodyFilter + { + public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context) + { + + if (requestBody.Content != null) + { + foreach (var mediaType in requestBody.Content.Values) + { + if (mediaType.Schema?.Properties != null) + { + var propertiesToRename = new Dictionary(mediaType.Schema.Properties); + // 清空旧的属性 + mediaType.Schema.Properties.Clear(); + + foreach (var property in propertiesToRename) + { + // 创建新的属性,并将名称改为小写开头 + var newPropertyName = Char.ToLower(property.Key[0]) + property.Key.Substring(1); + mediaType.Schema.Properties.Add(newPropertyName, property.Value); + } + } + } + } + } + } +} diff --git a/src/2-api/HuanMeng.MiaoYu.WebApi/Base/MiaoYuControllerBase.cs b/src/2-api/HuanMeng.MiaoYu.WebApi/Base/MiaoYuControllerBase.cs new file mode 100644 index 0000000..44eef23 --- /dev/null +++ b/src/2-api/HuanMeng.MiaoYu.WebApi/Base/MiaoYuControllerBase.cs @@ -0,0 +1,51 @@ +using AutoMapper; + +using HuanMeng.MiaoYu.Code.DataAccess; + +using Microsoft.AspNetCore.Mvc; + +namespace HuanMeng.MiaoYu.WebApi.Base +{ + [ApiController] + public class MiaoYuControllerBase(IServiceProvider _serviceProvider) : ControllerBase + { + /// + /// 数据库使用 + /// + //[FromServices] + public IServiceProvider ServiceProvider { get; set; } = _serviceProvider; + + + /// + /// HttpContextAccessor + /// + [FromServices] + public required IHttpContextAccessor HttpContextAccessor { get; set; } + + /// + /// + /// + [FromServices] + public required IMapper Mapper { get; set; } + + /// + /// + /// + private DAO? _dao; + /// + /// 数据库访问类 + /// + public DAO Dao + { + get + { + if (_dao == null) + { + _dao = new DAO(ServiceProvider); + } + return _dao; + } + } + } + +} diff --git a/src/2-api/HuanMeng.MiaoYu.WebApi/Controllers/AccountController.cs b/src/2-api/HuanMeng.MiaoYu.WebApi/Controllers/AccountController.cs new file mode 100644 index 0000000..efad636 --- /dev/null +++ b/src/2-api/HuanMeng.MiaoYu.WebApi/Controllers/AccountController.cs @@ -0,0 +1,72 @@ +using Azure; + +using HuanMeng.DotNetCore.Base; +using HuanMeng.MiaoYu.Code.Other; +using HuanMeng.MiaoYu.Code.Users; +using HuanMeng.MiaoYu.Model.Dto; +using HuanMeng.MiaoYu.Model.Dto.Account; +using HuanMeng.MiaoYu.WebApi.Base; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +using System.Numerics; +using System.Text.RegularExpressions; + +namespace HuanMeng.MiaoYu.WebApi.Controllers +{ + /// + /// 账号控制器 + /// + [Route("api/[controller]/[action]")] + [ApiController] + public class AccountController : MiaoYuControllerBase + { + public AccountController(IServiceProvider _serviceProvider) : base(_serviceProvider) + { + } + + /// + /// 发送手机号验证码 + /// + /// 手机号 + /// + [HttpPost] + public async Task> SendPhoneNumber([FromBody] RequestPhoneNumberModel phone) + { + if (!PhoneNumberValidator.IsPhoneNumber(phone.PhoneNumber)) + { + throw new ArgumentException("请输入正确的手机号"); + } + UserBLL userBLL = new UserBLL(ServiceProvider); + return await userBLL.SendPhoneNumber(phone.PhoneNumber); + } + /// + /// 登录 + /// + /// + /// + [HttpPost] + [AllowAnonymous] + public async Task> AccountLogIn([FromBody] RequestLoginModel requestLoginModel) + { + UserBLL userBLL = new UserBLL(ServiceProvider); + return await userBLL.AccountLogIn(requestLoginModel); + } + + /// + /// + /// + /// + [Authorize] + [HttpGet] + public async Task> GetUserInfo() + { + UserBLL userBLL = new UserBLL(ServiceProvider); + return await userBLL.GetUserInfo(); + } + + } +} diff --git a/src/2-api/HuanMeng.MiaoYu.WebApi/Controllers/WeatherForecastController.cs b/src/2-api/HuanMeng.MiaoYu.WebApi/Controllers/WeatherForecastController.cs deleted file mode 100644 index 9cf5522..0000000 --- a/src/2-api/HuanMeng.MiaoYu.WebApi/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace HuanMeng.MiaoYu.WebApi.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} diff --git a/src/2-api/HuanMeng.MiaoYu.WebApi/HuanMeng.MiaoYu.WebApi.csproj b/src/2-api/HuanMeng.MiaoYu.WebApi/HuanMeng.MiaoYu.WebApi.csproj index 9f551b4..c374a35 100644 --- a/src/2-api/HuanMeng.MiaoYu.WebApi/HuanMeng.MiaoYu.WebApi.csproj +++ b/src/2-api/HuanMeng.MiaoYu.WebApi/HuanMeng.MiaoYu.WebApi.csproj @@ -4,9 +4,13 @@ net8.0 enable enable + True + + + diff --git a/src/2-api/HuanMeng.MiaoYu.WebApi/Program.cs b/src/2-api/HuanMeng.MiaoYu.WebApi/Program.cs index df2434c..5507433 100644 --- a/src/2-api/HuanMeng.MiaoYu.WebApi/Program.cs +++ b/src/2-api/HuanMeng.MiaoYu.WebApi/Program.cs @@ -1,23 +1,133 @@ +using System.Diagnostics; +using System.Reflection; +using HuanMeng.MiaoYu.Code.MultiTenantUtil; +using HuanMeng.DotNetCore.MiddlewareExtend; +using HuanMeng.MiaoYu.WebApi.Base; +using Microsoft.OpenApi.Models; +using HuanMeng.MiaoYu.Code.TencentUtile; +using HuanMeng.MiaoYu.Code.Users.UserAccount.VerificationCodeManager; +using HuanMeng.MiaoYu.Code.JwtUtil; +using Microsoft.AspNetCore.Authentication.JwtBearer; var builder = WebApplication.CreateBuilder(args); // Add services to the container. +builder.Services.AddHttpContextAccessor(); //添加httpContext注入访问 +#region 添加跨域 +var _myAllowSpecificOrigins = "_myAllowSpecificOrigins"; +builder.Services.AddCors(options => +options.AddPolicy(_myAllowSpecificOrigins, +builder => +{ + //builder.AllowAnyHeader().AllowAnyMethod(). + //AllowAnyOrigin(); + //builder.AllowAnyHeader() + // .AllowAnyMethod() + // .SetIsOriginAllowed((host) => true)//限制允许跨域请求的特定源 + // .AllowCredentials();//允许跨域请求发送凭据,如 Cookies、HTTP 认证信息等。启用该配置可能会增加安全风险,因为浏览器会在请求头中包含凭据信息。因此,在设置 AllowCredentials 时,应该确保服务器端的安全性,并且仅允许受信任的源发送包含凭据的请求。 + builder.AllowAnyHeader() + .AllowAnyMethod() + .AllowAnyOrigin();// 许来自任意源的跨域请求 +})); +#endregion +builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies().Where(it => it.FullName.Contains("HuanMeng") || it.FullName.Contains("XLib.")).ToList()); builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(c => +{ + var securityScheme = new OpenApiSecurityScheme + { + Name = "JWT 身份验证(Authentication)", + Description = "请输入登录后获取JWT的**token**", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = "bearer", //必须小写 + BearerFormat = "JWT", + Reference = new OpenApiReference + { + Id = JwtBearerDefaults.AuthenticationScheme, + Type = ReferenceType.SecurityScheme + } + }; + c.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme); + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + {securityScheme, Array.Empty()} + }); + + c.SwaggerDoc("v1", new OpenApiInfo { Title = "妙语", Version = "v1" }); + foreach (var assemblies in AppDomain.CurrentDomain.GetAssemblies()) + { + // 添加 XML 注释文件路径 + var xmlFile = $"{assemblies.GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + if (File.Exists(xmlPath)) + { + c.IncludeXmlComments(xmlPath); + + } + } + c.ParameterFilter(); + c.RequestBodyFilter(); +}); +builder.AddMultiTenantMiaoYu(); +builder.AddTencent(); +builder.AddMemoryVerificationCode(); +builder.AddJwtConfig(); var app = builder.Build(); // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) +//if (app.Environment.IsDevelopment()) +//{ +app.UseSwagger(); +app.UseSwaggerUI(c => { - app.UseSwagger(); - app.UseSwaggerUI(); -} + + c.EnableDeepLinking(); + c.DefaultModelsExpandDepth(3); + c.DefaultModelExpandDepth(3); + c.EnableFilter("true"); + //c.RoutePrefix = "swagger"; + //c.SwaggerEndpoint("/swagger/v1/swagger.json", "Your API V1"); +}); +//} app.UseAuthorization(); - +//自定义初始化 +//使用跨域 +app.UseCors(_myAllowSpecificOrigins); app.MapControllers(); +//数据库中间件 +app.UseMultiTenantMiaoYu(); +//异常中间件 +app.UseExecutionTimeMiddleware(); +//请求耗时中间件 +app.UseExceptionMiddleware(); +#region 默认请求 +app.MapGet("/", () => "请求成功").WithName("默认请求"); +var startDateTime = DateTime.Now; +var InformationalVersion = Assembly.GetEntryAssembly().GetCustomAttribute().InformationalVersion; +//Console.WriteLine($"version:{InformationalVersion}"); +app.MapGet("/system", () => +{ + + using Process currentProcess = Process.GetCurrentProcess(); + // CPU使用率 (一般是一个0-100之间的值,但实际是时间占比,需要转换) + double cpuUsage = currentProcess.TotalProcessorTime.TotalMilliseconds / Environment.TickCount * 100; + // 已用内存 (字节) + long memoryUsage = currentProcess.WorkingSet64; + return new + { + msg = $"系统版本:{InformationalVersion},启动时间:{startDateTime.ToString("yyyy-MM-dd HH:mm:ss")},已安全运行时间:{DateTime.Now.Subtract(startDateTime).TotalMinutes.ToString("#.##")}分钟", + InformationalVersion, + startDateTime, + MemoryUsage = $"{memoryUsage / (1024.0 * 1024.0):F2}MB", + CPUUsage = $"{cpuUsage:F2}%" + + }; +}).WithName("获取系统数据"); +#endregion app.Run(); diff --git a/src/2-api/HuanMeng.MiaoYu.WebApi/WeatherForecast.cs b/src/2-api/HuanMeng.MiaoYu.WebApi/WeatherForecast.cs deleted file mode 100644 index d551935..0000000 --- a/src/2-api/HuanMeng.MiaoYu.WebApi/WeatherForecast.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace HuanMeng.MiaoYu.WebApi -{ - public class WeatherForecast - { - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } - } -} diff --git a/src/2-api/HuanMeng.MiaoYu.WebApi/appsettings.json b/src/2-api/HuanMeng.MiaoYu.WebApi/appsettings.json index 10f68b8..1c2dac2 100644 --- a/src/2-api/HuanMeng.MiaoYu.WebApi/appsettings.json +++ b/src/2-api/HuanMeng.MiaoYu.WebApi/appsettings.json @@ -1,9 +1,50 @@ { + "ConnectionStrings": { + "MiaoYu_SqlServer_Db": "Server=192.168.195.2;Database=MiaoYu;User Id=zpc;Password=zpc;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=True;" + }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + //腾讯云配置 + "TencentCloud": { + "SecretId": "AKIDLbhdP0Vs57yd7QZWu8A2jFbno8JKBUp6", + "SecretKey": "MlP5tcUG6mdj7TwOpDWnZNFGIrJY8eH4", + "SMSCode": { + //请求方式 + "ReqMethod": "POST", + //超时时间 + "Timeout": 30, + //短信应用ID: + "SmsSdkAppId": "1400923253", + //签名 + "SignName": "上海寰梦科技发展", + //模板编号 + "TemplateId": "2209122" + } + }, + "JwtTokenConfig": { + //加密字段 + "secret": "XtrtwJIcxRHWInEMsCyUdwcRKLNHHAcQ", + //发行人 + "issuer": "HuanMeng", + //受众 + "audience": "HuanMengApp", + //token时间,分钟 + "accessTokenExpiration": 10080, + //刷新token时间.分钟 + "refreshTokenExpiration": 10100 + + }, + "AllowedHosts": "*", + //服务器配置 + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://*:89" + } + } + } }