2025-10-16 09:56:36 +08:00
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
2025-10-16 15:21:52 +08:00
|
|
|
|
using Microsoft.IdentityModel.Tokens;
|
|
|
|
|
|
using System.IdentityModel.Tokens.Jwt;
|
|
|
|
|
|
using System.Security.Claims;
|
|
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
|
using System.Text;
|
2025-10-16 09:56:36 +08:00
|
|
|
|
using FutureMailAPI.Data;
|
|
|
|
|
|
using FutureMailAPI.DTOs;
|
2025-10-16 15:21:52 +08:00
|
|
|
|
using FutureMailAPI.Models;
|
2025-10-16 09:56:36 +08:00
|
|
|
|
|
|
|
|
|
|
namespace FutureMailAPI.Services
|
|
|
|
|
|
{
|
|
|
|
|
|
public class OAuthService : IOAuthService
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly FutureMailDbContext _context;
|
2025-10-16 15:21:52 +08:00
|
|
|
|
private readonly IConfiguration _configuration;
|
2025-10-16 09:56:36 +08:00
|
|
|
|
private readonly ILogger<OAuthService> _logger;
|
2025-10-16 15:21:52 +08:00
|
|
|
|
|
|
|
|
|
|
public OAuthService(FutureMailDbContext context, IConfiguration configuration, ILogger<OAuthService> logger)
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
|
|
|
|
|
_context = context;
|
2025-10-16 15:21:52 +08:00
|
|
|
|
_configuration = configuration;
|
2025-10-16 09:56:36 +08:00
|
|
|
|
_logger = logger;
|
|
|
|
|
|
}
|
2025-10-16 15:21:52 +08:00
|
|
|
|
|
|
|
|
|
|
public async Task<ApiResponse<OAuthTokenResponseDto>> LoginAsync(OAuthLoginRequestDto request)
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
2025-10-16 15:21:52 +08:00
|
|
|
|
try
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
2025-10-16 15:21:52 +08:00
|
|
|
|
// 验证OAuth客户端
|
|
|
|
|
|
var client = await _context.OAuthClients
|
|
|
|
|
|
.FirstOrDefaultAsync(c => c.ClientId == request.ClientId && c.ClientSecret == request.ClientSecret);
|
|
|
|
|
|
|
|
|
|
|
|
if (client == null)
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
2025-10-16 15:21:52 +08:00
|
|
|
|
return ApiResponse<OAuthTokenResponseDto>.ErrorResult("无效的客户端凭据");
|
2025-10-16 09:56:36 +08:00
|
|
|
|
}
|
2025-10-16 15:21:52 +08:00
|
|
|
|
|
|
|
|
|
|
// 验证用户凭据
|
|
|
|
|
|
var user = await _context.Users
|
|
|
|
|
|
.FirstOrDefaultAsync(u => u.Email == request.Username);
|
|
|
|
|
|
|
|
|
|
|
|
if (user == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return ApiResponse<OAuthTokenResponseDto>.ErrorResult("用户不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证密码
|
|
|
|
|
|
if (!BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash))
|
|
|
|
|
|
{
|
|
|
|
|
|
return ApiResponse<OAuthTokenResponseDto>.ErrorResult("密码错误");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成访问令牌
|
|
|
|
|
|
var accessToken = GenerateJwtToken(user, client);
|
|
|
|
|
|
var refreshToken = GenerateRefreshToken();
|
|
|
|
|
|
|
|
|
|
|
|
// 保存令牌到数据库
|
|
|
|
|
|
var oauthToken = new OAuthToken
|
|
|
|
|
|
{
|
|
|
|
|
|
AccessToken = accessToken,
|
|
|
|
|
|
RefreshToken = refreshToken,
|
|
|
|
|
|
UserId = user.Id,
|
|
|
|
|
|
ClientId = client.Id,
|
|
|
|
|
|
ExpiresAt = DateTime.UtcNow.AddHours(1), // 访问令牌1小时过期
|
|
|
|
|
|
CreatedAt = DateTime.UtcNow
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_context.OAuthTokens.Add(oauthToken);
|
|
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var response = new OAuthTokenResponseDto
|
|
|
|
|
|
{
|
|
|
|
|
|
AccessToken = accessToken,
|
|
|
|
|
|
RefreshToken = refreshToken,
|
|
|
|
|
|
TokenType = "Bearer",
|
|
|
|
|
|
ExpiresIn = 3600 // 1小时,单位秒
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return ApiResponse<OAuthTokenResponseDto>.SuccessResult(response);
|
2025-10-16 09:56:36 +08:00
|
|
|
|
}
|
2025-10-16 15:21:52 +08:00
|
|
|
|
catch (Exception ex)
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
2025-10-16 15:21:52 +08:00
|
|
|
|
_logger.LogError(ex, "OAuth登录时发生错误");
|
|
|
|
|
|
return ApiResponse<OAuthTokenResponseDto>.ErrorResult("服务器内部错误");
|
|
|
|
|
|
}
|
2025-10-16 09:56:36 +08:00
|
|
|
|
}
|
2025-10-16 15:21:52 +08:00
|
|
|
|
|
|
|
|
|
|
public async Task<ApiResponse<OAuthTokenResponseDto>> RefreshTokenAsync(OAuthRefreshTokenRequestDto request)
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
2025-10-16 15:21:52 +08:00
|
|
|
|
try
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
2025-10-16 15:21:52 +08:00
|
|
|
|
// 查找刷新令牌
|
|
|
|
|
|
var token = await _context.OAuthTokens
|
|
|
|
|
|
.Include(t => t.User)
|
|
|
|
|
|
.Include(t => t.Client)
|
|
|
|
|
|
.FirstOrDefaultAsync(t => t.RefreshToken == request.RefreshToken);
|
|
|
|
|
|
|
|
|
|
|
|
if (token == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return ApiResponse<OAuthTokenResponseDto>.ErrorResult("无效的刷新令牌");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成新的访问令牌
|
|
|
|
|
|
var accessToken = GenerateJwtToken(token.User, token.Client);
|
|
|
|
|
|
var refreshToken = GenerateRefreshToken();
|
|
|
|
|
|
|
|
|
|
|
|
// 更新令牌
|
|
|
|
|
|
token.AccessToken = accessToken;
|
|
|
|
|
|
token.RefreshToken = refreshToken;
|
|
|
|
|
|
token.ExpiresAt = DateTime.UtcNow.AddHours(1);
|
|
|
|
|
|
token.UpdatedAt = DateTime.UtcNow;
|
|
|
|
|
|
|
|
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var response = new OAuthTokenResponseDto
|
|
|
|
|
|
{
|
|
|
|
|
|
AccessToken = accessToken,
|
|
|
|
|
|
RefreshToken = refreshToken,
|
|
|
|
|
|
TokenType = "Bearer",
|
|
|
|
|
|
ExpiresIn = 3600 // 1小时,单位秒
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return ApiResponse<OAuthTokenResponseDto>.SuccessResult(response);
|
2025-10-16 09:56:36 +08:00
|
|
|
|
}
|
2025-10-16 15:21:52 +08:00
|
|
|
|
catch (Exception ex)
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
2025-10-16 15:21:52 +08:00
|
|
|
|
_logger.LogError(ex, "OAuth刷新令牌时发生错误");
|
|
|
|
|
|
return ApiResponse<OAuthTokenResponseDto>.ErrorResult("服务器内部错误");
|
2025-10-16 09:56:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-16 15:21:52 +08:00
|
|
|
|
|
|
|
|
|
|
public async Task<bool> RevokeTokenAsync(string accessToken)
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
2025-10-16 15:21:52 +08:00
|
|
|
|
try
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
2025-10-16 15:21:52 +08:00
|
|
|
|
var token = await _context.OAuthTokens
|
|
|
|
|
|
.FirstOrDefaultAsync(t => t.AccessToken == accessToken);
|
|
|
|
|
|
|
|
|
|
|
|
if (token != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_context.OAuthTokens.Remove(token);
|
|
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
2025-10-16 09:56:36 +08:00
|
|
|
|
}
|
2025-10-16 15:21:52 +08:00
|
|
|
|
catch (Exception ex)
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
2025-10-16 15:21:52 +08:00
|
|
|
|
_logger.LogError(ex, "OAuth撤销令牌时发生错误");
|
|
|
|
|
|
return false;
|
2025-10-16 09:56:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-16 15:21:52 +08:00
|
|
|
|
|
|
|
|
|
|
public async Task<OAuthToken?> GetTokenAsync(string accessToken)
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
2025-10-16 15:21:52 +08:00
|
|
|
|
try
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
2025-10-16 15:21:52 +08:00
|
|
|
|
return await _context.OAuthTokens
|
|
|
|
|
|
.Include(t => t.User)
|
|
|
|
|
|
.Include(t => t.Client)
|
|
|
|
|
|
.FirstOrDefaultAsync(t => t.AccessToken == accessToken && t.ExpiresAt > DateTime.UtcNow);
|
2025-10-16 09:56:36 +08:00
|
|
|
|
}
|
2025-10-16 15:21:52 +08:00
|
|
|
|
catch (Exception ex)
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
2025-10-16 15:21:52 +08:00
|
|
|
|
_logger.LogError(ex, "获取OAuth令牌时发生错误");
|
|
|
|
|
|
return null;
|
2025-10-16 09:56:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-16 15:21:52 +08:00
|
|
|
|
|
|
|
|
|
|
private string GenerateJwtToken(User user, OAuthClient client)
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
2025-10-16 15:21:52 +08:00
|
|
|
|
var jwtSettings = _configuration.GetSection("Jwt");
|
|
|
|
|
|
var key = Encoding.ASCII.GetBytes(jwtSettings["Key"] ?? throw new InvalidOperationException("JWT密钥未配置"));
|
|
|
|
|
|
var tokenDescriptor = new SecurityTokenDescriptor
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
2025-10-16 15:21:52 +08:00
|
|
|
|
Subject = new ClaimsIdentity(new[]
|
|
|
|
|
|
{
|
|
|
|
|
|
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
|
|
|
|
|
new Claim(ClaimTypes.Name, user.Username),
|
|
|
|
|
|
new Claim(ClaimTypes.Email, user.Email),
|
|
|
|
|
|
new Claim("client_id", client.ClientId)
|
|
|
|
|
|
}),
|
|
|
|
|
|
Expires = DateTime.UtcNow.AddHours(1),
|
|
|
|
|
|
Issuer = jwtSettings["Issuer"],
|
|
|
|
|
|
Audience = jwtSettings["Audience"],
|
|
|
|
|
|
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
|
2025-10-16 09:56:36 +08:00
|
|
|
|
};
|
2025-10-16 15:21:52 +08:00
|
|
|
|
|
|
|
|
|
|
var tokenHandler = new JwtSecurityTokenHandler();
|
|
|
|
|
|
var token = tokenHandler.CreateToken(tokenDescriptor);
|
|
|
|
|
|
return tokenHandler.WriteToken(token);
|
2025-10-16 09:56:36 +08:00
|
|
|
|
}
|
2025-10-16 15:21:52 +08:00
|
|
|
|
|
|
|
|
|
|
private string GenerateRefreshToken()
|
2025-10-16 09:56:36 +08:00
|
|
|
|
{
|
2025-10-16 15:21:52 +08:00
|
|
|
|
var randomNumber = new byte[32];
|
|
|
|
|
|
using var rng = RandomNumberGenerator.Create();
|
|
|
|
|
|
rng.GetBytes(randomNumber);
|
|
|
|
|
|
return Convert.ToBase64String(randomNumber);
|
2025-10-16 09:56:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|