Files
TakeoutSaaS.AdminApi/src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs

149 lines
6.5 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 Microsoft.AspNetCore.Http;
using System.Net;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Domain.Identity.Entities;
using TakeoutSaaS.Domain.Identity.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Identity.Services;
/// <summary>
/// 小程序认证服务实现。
/// </summary>
public sealed class MiniAuthService(
IWeChatAuthService weChatAuthService,
IMiniUserRepository miniUserRepository,
IJwtTokenService jwtTokenService,
IRefreshTokenStore refreshTokenStore,
ILoginRateLimiter rateLimiter,
IHttpContextAccessor httpContextAccessor,
ITenantProvider tenantProvider) : IMiniAuthService
{
/// <summary>
/// 微信小程序登录:通过微信 code 获取用户信息并生成令牌。
/// </summary>
/// <param name="request">微信登录请求</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>令牌响应</returns>
/// <exception cref="BusinessException">获取微信用户信息失败、缺少租户标识时抛出</exception>
public async Task<TokenResponse> LoginWithWeChatAsync(WeChatLoginRequest request, CancellationToken cancellationToken = default)
{
// 1. 限流检查(基于 IP 地址)
var throttleKey = BuildThrottleKey();
await rateLimiter.EnsureAllowedAsync(throttleKey, cancellationToken);
// 2. 通过微信 code 获取 sessionOpenId、UnionId、SessionKey
var session = await weChatAuthService.Code2SessionAsync(request.Code, cancellationToken);
if (string.IsNullOrWhiteSpace(session.OpenId))
{
throw new BusinessException(ErrorCodes.Unauthorized, "获取微信用户信息失败");
}
// 3. 获取当前租户 ID多租户支持
var tenantId = tenantProvider.GetCurrentTenantId();
if (tenantId == 0)
{
throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
}
// 4. 获取或创建小程序用户(如果 OpenId 已存在则返回现有用户,否则创建新用户)
var (user, isNew) = await GetOrBindMiniUserAsync(session.OpenId, session.UnionId, request.Nickname, request.Avatar, tenantId, cancellationToken);
// 5. 登录成功后重置限流计数
await rateLimiter.ResetAsync(throttleKey, cancellationToken);
// 6. 构建用户档案并生成令牌
var profile = BuildProfile(user);
return await jwtTokenService.CreateTokensAsync(profile, isNew, cancellationToken);
}
/// <summary>
/// 刷新访问令牌:使用刷新令牌获取新的访问令牌和刷新令牌。
/// </summary>
/// <param name="request">刷新令牌请求</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>新的令牌响应</returns>
/// <exception cref="BusinessException">刷新令牌无效、已过期或用户不存在时抛出</exception>
public async Task<TokenResponse> RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default)
{
// 1. 验证刷新令牌(检查是否存在、是否过期、是否已撤销)
var descriptor = await refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken);
if (descriptor == null || descriptor.ExpiresAt <= DateTime.UtcNow || descriptor.Revoked)
{
throw new BusinessException(ErrorCodes.Unauthorized, "RefreshToken 无效或已过期");
}
// 2. 根据用户 ID 查找用户
var user = await miniUserRepository.FindByIdAsync(descriptor.UserId, cancellationToken)
?? throw new BusinessException(ErrorCodes.Unauthorized, "用户不存在");
// 3. 撤销旧刷新令牌(防止重复使用)
await refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken);
// 4. 生成新的令牌对
var profile = BuildProfile(user);
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
}
/// <summary>
/// 获取用户档案。
/// </summary>
/// <param name="userId">用户 ID</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>用户档案</returns>
/// <exception cref="BusinessException">用户不存在时抛出</exception>
public async Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default)
{
var user = await miniUserRepository.FindByIdAsync(userId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
return BuildProfile(user);
}
/// <summary>
/// 获取或绑定小程序用户:如果 OpenId 已存在则返回现有用户,否则创建新用户。
/// </summary>
/// <param name="openId">微信 OpenId</param>
/// <param name="unionId">微信 UnionId可选</param>
/// <param name="nickname">昵称</param>
/// <param name="avatar">头像地址(可选)</param>
/// <param name="tenantId">租户 ID</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>用户实体和是否为新用户的元组</returns>
private async Task<(MiniUser user, bool isNew)> GetOrBindMiniUserAsync(string openId, string? unionId, string? nickname, string? avatar, long tenantId, CancellationToken cancellationToken)
{
// 检查用户是否已存在
var existing = await miniUserRepository.FindByOpenIdAsync(openId, cancellationToken);
if (existing != null)
{
return (existing, false);
}
// 创建新用户
var created = await miniUserRepository.CreateOrUpdateAsync(openId, unionId, nickname, avatar, tenantId, cancellationToken);
return (created, true);
}
private static CurrentUserProfile BuildProfile(MiniUser user)
=> new()
{
UserId = user.Id,
Account = user.OpenId,
DisplayName = user.Nickname,
TenantId = user.TenantId,
MerchantId = null,
Roles = Array.Empty<string>(),
Permissions = Array.Empty<string>(),
Avatar = user.Avatar
};
private string BuildThrottleKey()
{
var ip = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress ?? IPAddress.Loopback;
return $"mini-login:{ip}";
}
}