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