HaniBlindBox/server/HoneyBox/src/HoneyBox.Core/Services/AuthService.cs
2026-01-25 19:10:31 +08:00

996 lines
36 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Security.Cryptography;
using System.Text;
using HoneyBox.Core.Interfaces;
using HoneyBox.Model.Data;
using HoneyBox.Model.Entities;
using HoneyBox.Model.Models.Auth;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace HoneyBox.Core.Services;
/// <summary>
/// 认证服务实现
/// </summary>
public class AuthService : IAuthService
{
private readonly HoneyBoxDbContext _dbContext;
private readonly IUserService _userService;
private readonly IJwtService _jwtService;
private readonly IWechatService _wechatService;
private readonly IIpLocationService _ipLocationService;
private readonly IRedisService _redisService;
private readonly JwtSettings _jwtSettings;
private readonly ILogger<AuthService> _logger;
// Redis key prefixes
private const string LoginDebounceKeyPrefix = "login:debounce:";
private const string SmsCodeKeyPrefix = "sms:code:";
private const int DebounceSeconds = 3;
// Refresh Token 配置
private const int RefreshTokenLength = 64;
public AuthService(
HoneyBoxDbContext dbContext,
IUserService userService,
IJwtService jwtService,
IWechatService wechatService,
IIpLocationService ipLocationService,
IRedisService redisService,
JwtSettings jwtSettings,
ILogger<AuthService> logger)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
_jwtService = jwtService ?? throw new ArgumentNullException(nameof(jwtService));
_wechatService = wechatService ?? throw new ArgumentNullException(nameof(wechatService));
_ipLocationService = ipLocationService ?? throw new ArgumentNullException(nameof(ipLocationService));
_redisService = redisService ?? throw new ArgumentNullException(nameof(redisService));
_jwtSettings = jwtSettings ?? throw new ArgumentNullException(nameof(jwtSettings));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// 微信小程序登录
/// Requirements: 1.1-1.8
/// </summary>
public async Task<LoginResult> WechatMiniProgramLoginAsync(string code, int? pid, string? clickId)
{
_logger.LogInformation("[AuthService] 微信登录开始code={Code}, pid={Pid}, clickId={ClickId}", code, pid, clickId);
if (string.IsNullOrWhiteSpace(code))
{
_logger.LogWarning("[AuthService] 微信登录失败code为空");
return new LoginResult
{
Success = false,
ErrorMessage = "授权code不能为空"
};
}
try
{
// 1.6 防抖机制 - 3秒内不允许重复登录
var debounceKey = $"{LoginDebounceKeyPrefix}wechat:{code}";
_logger.LogInformation("[AuthService] 检查防抖锁: {Key}", debounceKey);
var lockAcquired = await _redisService.TryAcquireLockAsync(debounceKey, "1", TimeSpan.FromSeconds(DebounceSeconds));
if (!lockAcquired)
{
_logger.LogWarning("[AuthService] 防抖触发,拒绝重复登录请求: {Code}", code);
return new LoginResult
{
Success = false,
ErrorMessage = "请勿频繁登录"
};
}
_logger.LogInformation("[AuthService] 防抖锁获取成功");
// 1.1 调用微信API获取openid和unionid
_logger.LogInformation("[AuthService] 开始调用微信API获取openid...");
var wechatResult = await _wechatService.GetOpenIdAsync(code);
_logger.LogInformation("[AuthService] 微信API调用完成Success={Success}, OpenId={OpenId}, UnionId={UnionId}, Error={Error}",
wechatResult.Success,
wechatResult.OpenId ?? "null",
wechatResult.UnionId ?? "null",
wechatResult.ErrorMessage ?? "null");
if (!wechatResult.Success)
{
_logger.LogWarning("[AuthService] 微信API调用失败: {Error}", wechatResult.ErrorMessage);
return new LoginResult
{
Success = false,
ErrorMessage = wechatResult.ErrorMessage ?? "登录失败,请稍后重试"
};
}
var openId = wechatResult.OpenId!;
var unionId = wechatResult.UnionId;
// 1.2 查找用户 - 优先通过unionid查找其次通过openid查找
User? user = null;
if (!string.IsNullOrWhiteSpace(unionId))
{
_logger.LogInformation("[AuthService] 尝试通过unionid查找用户: {UnionId}", unionId);
user = await _userService.GetUserByUnionIdAsync(unionId);
_logger.LogInformation("[AuthService] unionid查找结果: {Found}", user != null ? $"找到用户ID={user.Id}" : "未找到");
}
if (user == null)
{
_logger.LogInformation("[AuthService] 尝试通过openid查找用户: {OpenId}", openId);
user = await _userService.GetUserByOpenIdAsync(openId);
_logger.LogInformation("[AuthService] openid查找结果: {Found}", user != null ? $"找到用户ID={user.Id}" : "未找到");
}
if (user == null)
{
// 1.3 用户不存在,创建新用户
_logger.LogInformation("[AuthService] 用户不存在,开始创建新用户...");
var createDto = new CreateUserDto
{
OpenId = openId,
UnionId = unionId,
Nickname = $"用户{Random.Shared.Next(100000, 999999)}",
Headimg = GenerateDefaultAvatar(openId),
Pid = pid ?? 0,
ClickId = ParseClickId(clickId)
};
user = await _userService.CreateUserAsync(createDto);
_logger.LogInformation("[AuthService] 新用户创建成功: UserId={UserId}, OpenId={OpenId}", user.Id, openId);
}
else
{
// 1.4 用户存在更新unionid如果之前为空
if (string.IsNullOrWhiteSpace(user.UnionId) && !string.IsNullOrWhiteSpace(unionId))
{
_logger.LogInformation("[AuthService] 更新用户unionid: UserId={UserId}", user.Id);
await _userService.UpdateUserAsync(user.Id, new UpdateUserDto { UnionId = unionId });
_logger.LogInformation("[AuthService] unionid更新成功");
}
}
// 1.5 生成双 TokenAccess Token + Refresh Token
_logger.LogInformation("[AuthService] 开始生成双 Token: UserId={UserId}", user.Id);
var loginResponse = await GenerateLoginResponseAsync(user, null);
_logger.LogInformation("[AuthService] 双 Token 生成成功AccessToken长度={Length}", loginResponse.AccessToken?.Length ?? 0);
// 3.6 同时在数据库UserAccount表中存储account_token用于兼容旧系统
_logger.LogInformation("[AuthService] 更新UserAccount表...");
await CreateOrUpdateAccountTokenAsync(user.Id, loginResponse.AccessToken);
_logger.LogInformation("[AuthService] UserAccount更新成功");
_logger.LogInformation("[AuthService] 微信登录成功: UserId={UserId}", user.Id);
return new LoginResult
{
Success = true,
Token = loginResponse.AccessToken, // 兼容旧版
UserId = user.Id,
LoginResponse = loginResponse
};
}
catch (Exception ex)
{
_logger.LogError(ex, "[AuthService] 微信登录异常: code={Code}, Message={Message}, StackTrace={StackTrace}",
code, ex.Message, ex.StackTrace);
return new LoginResult
{
Success = false,
ErrorMessage = "网络故障,请稍后再试"
};
}
}
/// <summary>
/// 手机号验证码登录
/// Requirements: 2.1-2.7
/// </summary>
public async Task<LoginResult> MobileLoginAsync(string mobile, string code, int? pid, string? clickId)
{
if (string.IsNullOrWhiteSpace(mobile))
{
return new LoginResult
{
Success = false,
ErrorMessage = "手机号不能为空"
};
}
if (string.IsNullOrWhiteSpace(code))
{
return new LoginResult
{
Success = false,
ErrorMessage = "验证码不能为空"
};
}
try
{
// 2.6 防抖机制 - 3秒内不允许重复登录
var debounceKey = $"{LoginDebounceKeyPrefix}mobile:{mobile}";
var lockAcquired = await _redisService.TryAcquireLockAsync(debounceKey, "1", TimeSpan.FromSeconds(DebounceSeconds));
if (!lockAcquired)
{
_logger.LogWarning("Login debounce triggered for mobile: {Mobile}", MaskMobile(mobile));
return new LoginResult
{
Success = false,
ErrorMessage = "请勿频繁登录"
};
}
// 2.1 从Redis获取并验证验证码
var smsCodeKey = $"{SmsCodeKeyPrefix}{mobile}";
var storedCode = await _redisService.GetStringAsync(smsCodeKey);
if (string.IsNullOrWhiteSpace(storedCode) || storedCode != code)
{
_logger.LogWarning("SMS code verification failed for mobile: {Mobile}", MaskMobile(mobile));
return new LoginResult
{
Success = false,
ErrorMessage = "验证码错误"
};
}
// 2.2 验证码验证通过后删除Redis中的验证码
await _redisService.DeleteAsync(smsCodeKey);
// 查找用户
var user = await _userService.GetUserByMobileAsync(mobile);
if (user == null)
{
// 2.3 用户不存在,创建新用户
var createDto = new CreateUserDto
{
Mobile = mobile,
Nickname = $"用户{Random.Shared.Next(100000, 999999)}",
Headimg = GenerateDefaultAvatar(mobile),
Pid = pid ?? 0,
ClickId = ParseClickId(clickId)
};
user = await _userService.CreateUserAsync(createDto);
_logger.LogInformation("New user created via mobile login: UserId={UserId}, Mobile={Mobile}", user.Id, MaskMobile(mobile));
}
// 2.4 生成双 TokenAccess Token + Refresh Token
var loginResponse = await GenerateLoginResponseAsync(user, null);
// 3.6 同时在数据库UserAccount表中存储account_token用于兼容旧系统
await CreateOrUpdateAccountTokenAsync(user.Id, loginResponse.AccessToken);
_logger.LogInformation("Mobile login successful: UserId={UserId}", user.Id);
return new LoginResult
{
Success = true,
Token = loginResponse.AccessToken, // 兼容旧版
UserId = user.Id,
LoginResponse = loginResponse
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Mobile login failed for mobile: {Mobile}", MaskMobile(mobile));
return new LoginResult
{
Success = false,
ErrorMessage = "网络故障,请稍后再试"
};
}
}
/// <summary>
/// 验证码绑定手机号
/// Requirements: 5.1-5.5
/// </summary>
public async Task<BindMobileResponse> BindMobileAsync(int userId, string mobile, string code)
{
if (string.IsNullOrWhiteSpace(mobile))
{
throw new ArgumentException("手机号不能为空", nameof(mobile));
}
if (string.IsNullOrWhiteSpace(code))
{
throw new ArgumentException("验证码不能为空", nameof(code));
}
// 5.1 验证短信验证码
var smsCodeKey = $"{SmsCodeKeyPrefix}{mobile}";
var storedCode = await _redisService.GetStringAsync(smsCodeKey);
if (string.IsNullOrWhiteSpace(storedCode) || storedCode != code)
{
_logger.LogWarning("SMS code verification failed for bind mobile: UserId={UserId}, Mobile={Mobile}", userId, MaskMobile(mobile));
throw new InvalidOperationException("验证码错误");
}
// 验证码验证通过后删除
await _redisService.DeleteAsync(smsCodeKey);
// 获取当前用户
var currentUser = await _userService.GetUserByIdAsync(userId);
if (currentUser == null)
{
throw new InvalidOperationException("用户不存在");
}
// 检查手机号是否已被其他用户绑定
var existingUser = await _userService.GetUserByMobileAsync(mobile);
if (existingUser != null && existingUser.Id != userId)
{
// 5.2 手机号已被其他用户绑定,需要合并账户
return await MergeAccountsAsync(currentUser, existingUser);
}
// 5.4 手机号未被绑定,直接更新当前用户的手机号
await _userService.UpdateUserAsync(userId, new UpdateUserDto { Mobile = mobile });
_logger.LogInformation("Mobile bound successfully: UserId={UserId}, Mobile={Mobile}", userId, MaskMobile(mobile));
return new BindMobileResponse { Token = null };
}
/// <summary>
/// 微信授权绑定手机号
/// Requirements: 5.1-5.5
/// </summary>
public async Task<BindMobileResponse> WechatBindMobileAsync(int userId, string wechatCode)
{
if (string.IsNullOrWhiteSpace(wechatCode))
{
throw new ArgumentException("微信授权code不能为空", nameof(wechatCode));
}
// 调用微信API获取手机号
var mobileResult = await _wechatService.GetMobileAsync(wechatCode);
if (!mobileResult.Success || string.IsNullOrWhiteSpace(mobileResult.Mobile))
{
_logger.LogWarning("WeChat get mobile failed: UserId={UserId}, Error={Error}", userId, mobileResult.ErrorMessage);
throw new InvalidOperationException(mobileResult.ErrorMessage ?? "获取手机号失败");
}
var mobile = mobileResult.Mobile;
// 获取当前用户
var currentUser = await _userService.GetUserByIdAsync(userId);
if (currentUser == null)
{
throw new InvalidOperationException("用户不存在");
}
// 检查手机号是否已被其他用户绑定
var existingUser = await _userService.GetUserByMobileAsync(mobile);
if (existingUser != null && existingUser.Id != userId)
{
// 5.2 手机号已被其他用户绑定,需要合并账户
return await MergeAccountsAsync(currentUser, existingUser);
}
// 5.4 手机号未被绑定,直接更新当前用户的手机号
await _userService.UpdateUserAsync(userId, new UpdateUserDto { Mobile = mobile });
_logger.LogInformation("Mobile bound via WeChat successfully: UserId={UserId}, Mobile={Mobile}", userId, MaskMobile(mobile));
return new BindMobileResponse { Token = null };
}
/// <summary>
/// 记录登录信息
/// Requirements: 6.1, 6.3, 6.4
/// </summary>
public async Task<RecordLoginResponse> RecordLoginAsync(int userId, string? device, string? deviceInfo)
{
var user = await _userService.GetUserByIdAsync(userId);
if (user == null)
{
throw new InvalidOperationException("用户不存在");
}
try
{
// 获取客户端IP这里使用空字符串作为占位符实际IP应从Controller传入
var clientIp = deviceInfo ?? string.Empty;
// 6.2 解析IP地址获取地理位置
IpLocationResult? locationResult = null;
if (!string.IsNullOrWhiteSpace(clientIp))
{
locationResult = await _ipLocationService.GetLocationAsync(clientIp);
}
var now = DateTime.UtcNow;
var today = DateOnly.FromDateTime(now);
// 6.1 记录登录日志
var loginLog = new UserLoginLog
{
UserId = userId,
LoginDate = today,
LoginTime = now,
LastLoginTime = now,
Device = device,
Ip = clientIp,
Location = locationResult?.Success == true
? $"{locationResult.Province}{locationResult.City}"
: null,
Year = now.Year,
Month = now.Month,
Week = GetWeekOfYear(now)
};
await _dbContext.UserLoginLogs.AddAsync(loginLog);
// 6.3 更新UserAccount表中的最后登录时间和IP信息
var userAccount = await _dbContext.UserAccounts.FirstOrDefaultAsync(ua => ua.UserId == userId);
if (userAccount != null)
{
userAccount.LastLoginTime = now;
userAccount.LastLoginIp = clientIp;
if (locationResult?.Success == true)
{
userAccount.IpProvince = locationResult.Province;
userAccount.IpCity = locationResult.City;
userAccount.IpAdcode = locationResult.Adcode;
}
_dbContext.UserAccounts.Update(userAccount);
}
// 更新用户最后登录时间
user.LastLoginTime = now;
_dbContext.Users.Update(user);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("Login recorded: UserId={UserId}, Device={Device}, IP={IP}", userId, device, clientIp);
// 6.4 返回用户的uid、昵称和头像
return new RecordLoginResponse
{
Uid = user.Uid,
Nickname = user.Nickname,
Headimg = user.HeadImg
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to record login: UserId={UserId}", userId);
throw;
}
}
/// <summary>
/// H5绑定手机号无需验证码
/// Requirements: 13.1
/// </summary>
public async Task<BindMobileResponse> BindMobileH5Async(int userId, string mobile)
{
if (string.IsNullOrWhiteSpace(mobile))
{
throw new ArgumentException("手机号不能为空", nameof(mobile));
}
// 获取当前用户
var currentUser = await _userService.GetUserByIdAsync(userId);
if (currentUser == null)
{
throw new InvalidOperationException("用户不存在");
}
// 检查手机号是否已被其他用户绑定
var existingUser = await _userService.GetUserByMobileAsync(mobile);
if (existingUser != null && existingUser.Id != userId)
{
// 手机号已被其他用户绑定,需要合并账户
return await MergeAccountsAsync(currentUser, existingUser);
}
// 手机号未被绑定,直接更新当前用户的手机号
await _userService.UpdateUserAsync(userId, new UpdateUserDto { Mobile = mobile });
_logger.LogInformation("H5 Mobile bound successfully: UserId={UserId}, Mobile={Mobile}", userId, MaskMobile(mobile));
return new BindMobileResponse { Token = null };
}
/// <summary>
/// 账号注销
/// Requirements: 7.1-7.3
/// </summary>
public async Task LogOffAsync(int userId, int type)
{
var user = await _userService.GetUserByIdAsync(userId);
if (user == null)
{
throw new InvalidOperationException("用户不存在");
}
try
{
// 7.1 记录注销请求日志
var action = type == 0 ? "注销账号" : "取消注销";
_logger.LogInformation("User log off request: UserId={UserId}, Type={Type}, Action={Action}", userId, type, action);
// 这里可以添加更多的注销逻辑,比如:
// - 将用户状态设置为已注销
// - 清理用户相关的缓存
// - 发送通知等
if (type == 0)
{
// 注销账号 - 可以设置用户状态为禁用
user.Status = 0;
_dbContext.Users.Update(user);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("User account deactivated: UserId={UserId}", userId);
}
else if (type == 1)
{
// 取消注销 - 恢复用户状态
user.Status = 1;
_dbContext.Users.Update(user);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("User account reactivated: UserId={UserId}", userId);
}
// 7.2 返回注销成功的消息(通过不抛出异常来表示成功)
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process log off: UserId={UserId}, Type={Type}", userId, type);
throw;
}
}
#region Refresh Token Methods
/// <summary>
/// 生成 Refresh Token 并存储到数据库
/// Requirements: 1.4, 1.5, 4.1
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="ipAddress">客户端 IP 地址</param>
/// <returns>生成的 Refresh Token 明文</returns>
private async Task<string> GenerateRefreshTokenAsync(int userId, string? ipAddress)
{
// 生成随机 Refresh Token
var refreshToken = GenerateSecureRandomString(RefreshTokenLength);
// 计算 SHA256 哈希值用于存储
var tokenHash = ComputeSha256Hash(refreshToken);
// 计算过期时间7天
var expiresAt = DateTime.Now.AddDays(_jwtSettings.RefreshTokenExpirationDays);
// 创建数据库记录
var userRefreshToken = new UserRefreshToken
{
UserId = userId,
TokenHash = tokenHash,
ExpiresAt = expiresAt,
CreatedAt = DateTime.Now,
CreatedByIp = ipAddress
};
await _dbContext.UserRefreshTokens.AddAsync(userRefreshToken);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("Generated refresh token for user {UserId}, expires at {ExpiresAt}", userId, expiresAt);
return refreshToken;
}
/// <summary>
/// 生成登录响应(包含双 Token
/// Requirements: 1.1, 1.2, 1.3, 1.4, 1.5
/// </summary>
/// <param name="user">用户实体</param>
/// <param name="ipAddress">客户端 IP 地址</param>
/// <returns>登录响应</returns>
private async Task<LoginResponse> GenerateLoginResponseAsync(User user, string? ipAddress)
{
// 生成 Access Token (JWT)
var accessToken = _jwtService.GenerateToken(user);
// 生成 Refresh Token 并存储
var refreshToken = await GenerateRefreshTokenAsync(user.Id, ipAddress);
// 计算 Access Token 过期时间(秒)
var expiresIn = _jwtSettings.ExpirationMinutes * 60;
return new LoginResponse
{
AccessToken = accessToken,
RefreshToken = refreshToken,
ExpiresIn = expiresIn,
UserId = user.Id
};
}
/// <summary>
/// 刷新 Token
/// Requirements: 2.1-2.6
/// </summary>
public async Task<RefreshTokenResult> RefreshTokenAsync(string refreshToken, string? ipAddress)
{
if (string.IsNullOrWhiteSpace(refreshToken))
{
_logger.LogWarning("Refresh token is empty");
return RefreshTokenResult.Fail("刷新令牌不能为空");
}
try
{
// 计算 Token 哈希值
var tokenHash = ComputeSha256Hash(refreshToken);
// 查找 Token 记录
var storedToken = await _dbContext.UserRefreshTokens
.Include(t => t.User)
.FirstOrDefaultAsync(t => t.TokenHash == tokenHash);
if (storedToken == null)
{
_logger.LogWarning("Refresh token not found: {TokenHash}", tokenHash.Substring(0, 8) + "...");
return RefreshTokenResult.Fail("无效的刷新令牌");
}
// 检查是否已过期
if (storedToken.IsExpired)
{
_logger.LogWarning("Refresh token expired for user {UserId}", storedToken.UserId);
return RefreshTokenResult.Fail("刷新令牌已过期");
}
// 检查是否已撤销
if (storedToken.IsRevoked)
{
_logger.LogWarning("Refresh token revoked for user {UserId}", storedToken.UserId);
return RefreshTokenResult.Fail("刷新令牌已失效");
}
// 检查用户是否存在且有效
var user = storedToken.User;
if (user == null)
{
_logger.LogWarning("User not found for refresh token");
return RefreshTokenResult.Fail("用户不存在");
}
if (user.Status == 0)
{
_logger.LogWarning("User {UserId} is disabled", user.Id);
return RefreshTokenResult.Fail("账号已被禁用");
}
// Token 轮换:生成新的 Refresh Token
var newRefreshToken = GenerateSecureRandomString(RefreshTokenLength);
var newTokenHash = ComputeSha256Hash(newRefreshToken);
// 撤销旧 Token 并记录关联关系
storedToken.RevokedAt = DateTime.Now;
storedToken.RevokedByIp = ipAddress;
storedToken.ReplacedByToken = newTokenHash;
// 创建新的 Token 记录
var newUserRefreshToken = new UserRefreshToken
{
UserId = user.Id,
TokenHash = newTokenHash,
ExpiresAt = DateTime.Now.AddDays(_jwtSettings.RefreshTokenExpirationDays),
CreatedAt = DateTime.Now,
CreatedByIp = ipAddress
};
await _dbContext.UserRefreshTokens.AddAsync(newUserRefreshToken);
await _dbContext.SaveChangesAsync();
// 生成新的 Access Token
var accessToken = _jwtService.GenerateToken(user);
var expiresIn = _jwtSettings.ExpirationMinutes * 60;
// 更新 UserAccount 表中的 token兼容旧系统
await CreateOrUpdateAccountTokenAsync(user.Id, accessToken);
_logger.LogInformation("Token refreshed successfully for user {UserId}", user.Id);
return RefreshTokenResult.Ok(new LoginResponse
{
AccessToken = accessToken,
RefreshToken = newRefreshToken,
ExpiresIn = expiresIn,
UserId = user.Id
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing token");
return RefreshTokenResult.Fail("刷新令牌失败,请稍后重试");
}
}
/// <summary>
/// 撤销 Token
/// Requirements: 4.4
/// </summary>
public async Task RevokeTokenAsync(string refreshToken, string? ipAddress)
{
if (string.IsNullOrWhiteSpace(refreshToken))
{
_logger.LogWarning("Cannot revoke empty refresh token");
return;
}
try
{
// 计算 Token 哈希值
var tokenHash = ComputeSha256Hash(refreshToken);
// 查找 Token 记录
var storedToken = await _dbContext.UserRefreshTokens
.FirstOrDefaultAsync(t => t.TokenHash == tokenHash);
if (storedToken == null)
{
_logger.LogWarning("Refresh token not found for revocation");
return;
}
// 如果已经撤销,直接返回
if (storedToken.IsRevoked)
{
_logger.LogInformation("Refresh token already revoked");
return;
}
// 撤销 Token
storedToken.RevokedAt = DateTime.Now;
storedToken.RevokedByIp = ipAddress;
await _dbContext.SaveChangesAsync();
_logger.LogInformation("Refresh token revoked for user {UserId}", storedToken.UserId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error revoking refresh token");
throw;
}
}
/// <summary>
/// 撤销用户的所有 Token
/// Requirements: 4.4
/// </summary>
public async Task RevokeAllUserTokensAsync(int userId, string? ipAddress)
{
try
{
// 查找用户所有有效的 Token
var activeTokens = await _dbContext.UserRefreshTokens
.Where(t => t.UserId == userId && t.RevokedAt == null)
.ToListAsync();
if (!activeTokens.Any())
{
_logger.LogInformation("No active tokens found for user {UserId}", userId);
return;
}
var now = DateTime.Now;
foreach (var token in activeTokens)
{
token.RevokedAt = now;
token.RevokedByIp = ipAddress;
}
await _dbContext.SaveChangesAsync();
_logger.LogInformation("Revoked {Count} tokens for user {UserId}", activeTokens.Count, userId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error revoking all tokens for user {UserId}", userId);
throw;
}
}
#endregion
#region Private Helper Methods
/// <summary>
/// 合并账户 - 将当前用户的openid迁移到手机号用户
/// </summary>
private async Task<BindMobileResponse> MergeAccountsAsync(User currentUser, User mobileUser)
{
using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
_logger.LogInformation("Merging accounts: CurrentUserId={CurrentUserId}, MobileUserId={MobileUserId}",
currentUser.Id, mobileUser.Id);
// 5.2 将当前用户的openid迁移到手机号用户
if (!string.IsNullOrWhiteSpace(currentUser.OpenId))
{
mobileUser.OpenId = currentUser.OpenId;
}
if (!string.IsNullOrWhiteSpace(currentUser.UnionId))
{
mobileUser.UnionId = currentUser.UnionId;
}
mobileUser.UpdatedAt = DateTime.UtcNow;
_dbContext.Users.Update(mobileUser);
// 删除当前用户的账户记录
var currentUserAccount = await _dbContext.UserAccounts.FirstOrDefaultAsync(ua => ua.UserId == currentUser.Id);
if (currentUserAccount != null)
{
_dbContext.UserAccounts.Remove(currentUserAccount);
}
// 删除当前用户
_dbContext.Users.Remove(currentUser);
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
// 5.3 生成新的token
var newToken = _jwtService.GenerateToken(mobileUser);
await CreateOrUpdateAccountTokenAsync(mobileUser.Id, newToken);
_logger.LogInformation("Accounts merged successfully: NewUserId={NewUserId}", mobileUser.Id);
return new BindMobileResponse { Token = newToken };
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Failed to merge accounts: CurrentUserId={CurrentUserId}, MobileUserId={MobileUserId}",
currentUser.Id, mobileUser.Id);
throw;
}
}
/// <summary>
/// 创建或更新账户Token用于兼容旧系统
/// </summary>
private async Task CreateOrUpdateAccountTokenAsync(int userId, string jwtToken)
{
var tokenNum = GenerateRandomString(10);
var time = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var accountToken = ComputeMd5($"{userId}{tokenNum}{time}");
var userAccount = await _dbContext.UserAccounts.FirstOrDefaultAsync(ua => ua.UserId == userId);
if (userAccount == null)
{
userAccount = new UserAccount
{
UserId = userId,
AccountToken = accountToken,
TokenNum = tokenNum,
TokenTime = DateTime.UtcNow,
LastLoginTime = DateTime.UtcNow,
LastLoginIp = string.Empty
};
await _dbContext.UserAccounts.AddAsync(userAccount);
}
else
{
userAccount.AccountToken = accountToken;
userAccount.TokenNum = tokenNum;
userAccount.TokenTime = DateTime.UtcNow;
userAccount.LastLoginTime = DateTime.UtcNow;
_dbContext.UserAccounts.Update(userAccount);
}
await _dbContext.SaveChangesAsync();
}
/// <summary>
/// 生成默认头像URL
/// </summary>
private static string GenerateDefaultAvatar(string seed)
{
// 使用种子生成一个简单的默认头像URL
// 实际项目中可以使用Identicon库或其他头像生成服务
var hash = ComputeMd5(seed);
return $"https://api.dicebear.com/7.x/identicon/svg?seed={hash}";
}
/// <summary>
/// 计算MD5哈希
/// </summary>
private static string ComputeMd5(string input)
{
var inputBytes = Encoding.UTF8.GetBytes(input);
var hashBytes = MD5.HashData(inputBytes);
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
/// <summary>
/// 生成随机字符串
/// </summary>
private static string GenerateRandomString(int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var result = new char[length];
for (int i = 0; i < length; i++)
{
result[i] = chars[Random.Shared.Next(chars.Length)];
}
return new string(result);
}
/// <summary>
/// 解析ClickId
/// </summary>
private static int? ParseClickId(string? clickId)
{
if (string.IsNullOrWhiteSpace(clickId))
return null;
return int.TryParse(clickId, out var result) ? result : null;
}
/// <summary>
/// 脱敏手机号
/// </summary>
private static string MaskMobile(string mobile)
{
if (string.IsNullOrWhiteSpace(mobile) || mobile.Length < 7)
return "***";
return $"{mobile.Substring(0, 3)}****{mobile.Substring(mobile.Length - 4)}";
}
/// <summary>
/// 获取年份中的周数
/// </summary>
private static int GetWeekOfYear(DateTime date)
{
var cal = System.Globalization.CultureInfo.CurrentCulture.Calendar;
return cal.GetWeekOfYear(date, System.Globalization.CalendarWeekRule.FirstDay, DayOfWeek.Monday);
}
/// <summary>
/// 计算 SHA256 哈希值
/// Requirements: 4.1
/// </summary>
private static string ComputeSha256Hash(string input)
{
var inputBytes = Encoding.UTF8.GetBytes(input);
var hashBytes = SHA256.HashData(inputBytes);
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
/// <summary>
/// 生成安全的随机字符串(用于 Refresh Token
/// </summary>
private static string GenerateSecureRandomString(int length)
{
var randomBytes = new byte[length];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomBytes);
return Convert.ToBase64String(randomBytes)
.Replace("+", "-")
.Replace("/", "_")
.Replace("=", "")
.Substring(0, length);
}
#endregion
}