Files
emall-api/FutureMailAPI/Services/OAuthService.cs
2025-10-16 09:56:36 +08:00

416 lines
16 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.EntityFrameworkCore;
using FutureMailAPI.Data;
using FutureMailAPI.Models;
using FutureMailAPI.DTOs;
using FutureMailAPI.Helpers;
using System.Security.Cryptography;
using System.Text.Json;
namespace FutureMailAPI.Services
{
public class OAuthService : IOAuthService
{
private readonly FutureMailDbContext _context;
private readonly ILogger<OAuthService> _logger;
private readonly IPasswordHelper _passwordHelper;
public OAuthService(FutureMailDbContext context, ILogger<OAuthService> logger, IPasswordHelper passwordHelper)
{
_context = context;
_logger = logger;
_passwordHelper = passwordHelper;
}
public async Task<ApiResponse<OAuthClientSecretDto>> CreateClientAsync(int userId, OAuthClientCreateDto createDto)
{
var clientId = GenerateRandomString(32);
var clientSecret = GenerateRandomString(64);
var client = new OAuthClient
{
ClientId = clientId,
ClientSecret = clientSecret,
Name = createDto.Name,
RedirectUris = JsonSerializer.Serialize(createDto.RedirectUris),
Scopes = JsonSerializer.Serialize(createDto.Scopes),
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
_context.OAuthClients.Add(client);
await _context.SaveChangesAsync();
var result = new OAuthClientSecretDto
{
ClientId = clientId,
ClientSecret = clientSecret
};
return ApiResponse<OAuthClientSecretDto>.SuccessResult(result, "OAuth客户端创建成功");
}
public async Task<ApiResponse<OAuthClientDto>> GetClientAsync(string clientId)
{
var client = await _context.OAuthClients
.FirstOrDefaultAsync(c => c.ClientId == clientId && c.IsActive);
if (client == null)
{
return ApiResponse<OAuthClientDto>.ErrorResult("客户端不存在");
}
var redirectUris = JsonSerializer.Deserialize<string[]>(client.RedirectUris) ?? Array.Empty<string>();
var scopes = JsonSerializer.Deserialize<string[]>(client.Scopes) ?? Array.Empty<string>();
var result = new OAuthClientDto
{
Id = client.Id,
ClientId = client.ClientId,
Name = client.Name,
RedirectUris = redirectUris,
Scopes = scopes,
IsActive = client.IsActive,
CreatedAt = client.CreatedAt,
UpdatedAt = client.UpdatedAt
};
return ApiResponse<OAuthClientDto>.SuccessResult(result);
}
public async Task<ApiResponse<OAuthAuthorizationResponseDto>> AuthorizeAsync(int userId, OAuthAuthorizationRequestDto request)
{
// 验证客户端
var client = await _context.OAuthClients
.FirstOrDefaultAsync(c => c.ClientId == request.ClientId && c.IsActive);
if (client == null)
{
return ApiResponse<OAuthAuthorizationResponseDto>.ErrorResult("无效的客户端ID");
}
// 验证重定向URI
var redirectUris = JsonSerializer.Deserialize<string[]>(client.RedirectUris) ?? Array.Empty<string>();
if (!redirectUris.Contains(request.RedirectUri))
{
return ApiResponse<OAuthAuthorizationResponseDto>.ErrorResult("无效的重定向URI");
}
// 验证范围
var clientScopes = JsonSerializer.Deserialize<string[]>(client.Scopes) ?? Array.Empty<string>();
var requestedScopes = request.Scope.Split(' ', StringSplitOptions.RemoveEmptyEntries);
foreach (var scope in requestedScopes)
{
if (!clientScopes.Contains(scope))
{
return ApiResponse<OAuthAuthorizationResponseDto>.ErrorResult($"无效的范围: {scope}");
}
}
// 生成授权码
var code = GenerateRandomString(64);
var authorizationCode = new OAuthAuthorizationCode
{
Code = code,
ClientId = client.Id,
UserId = userId,
RedirectUri = request.RedirectUri,
Scopes = request.Scope,
IsUsed = false,
ExpiresAt = DateTime.UtcNow.AddMinutes(10), // 授权码10分钟后过期
CreatedAt = DateTime.UtcNow
};
_context.OAuthAuthorizationCodes.Add(authorizationCode);
await _context.SaveChangesAsync();
var result = new OAuthAuthorizationResponseDto
{
Code = code,
State = request.State
};
return ApiResponse<OAuthAuthorizationResponseDto>.SuccessResult(result);
}
public async Task<ApiResponse<OAuthTokenResponseDto>> ExchangeCodeForTokenAsync(OAuthTokenRequestDto request)
{
// 验证客户端
var client = await GetClientByCredentialsAsync(request.ClientId, request.ClientSecret);
if (client == null)
{
return ApiResponse<OAuthTokenResponseDto>.ErrorResult("无效的客户端凭据");
}
// 验证授权码
var authCode = await _context.OAuthAuthorizationCodes
.Include(c => c.Client)
.Include(c => c.User)
.FirstOrDefaultAsync(c => c.Code == request.Code && c.ClientId == client.Id);
if (authCode == null || authCode.IsUsed || authCode.ExpiresAt < DateTime.UtcNow)
{
return ApiResponse<OAuthTokenResponseDto>.ErrorResult("无效的授权码");
}
// 验证重定向URI
if (authCode.RedirectUri != request.RedirectUri)
{
return ApiResponse<OAuthTokenResponseDto>.ErrorResult("重定向URI不匹配");
}
// 标记授权码为已使用
authCode.IsUsed = true;
await _context.SaveChangesAsync();
// 生成访问令牌和刷新令牌
var accessToken = GenerateRandomString(64);
var refreshToken = GenerateRandomString(64);
var scopes = !string.IsNullOrEmpty(request.Scope) ? request.Scope : authCode.Scopes;
var oauthAccessToken = new OAuthAccessToken
{
Token = accessToken,
ClientId = client.Id,
UserId = authCode.UserId,
Scopes = scopes,
IsRevoked = false,
ExpiresAt = DateTime.UtcNow.AddHours(1), // 访问令牌1小时后过期
CreatedAt = DateTime.UtcNow
};
var oauthRefreshToken = new OAuthRefreshToken
{
Token = refreshToken,
ClientId = client.Id,
UserId = authCode.UserId,
IsUsed = false,
ExpiresAt = DateTime.UtcNow.AddDays(30), // 刷新令牌30天后过期
CreatedAt = DateTime.UtcNow
};
_context.OAuthAccessTokens.Add(oauthAccessToken);
_context.OAuthRefreshTokens.Add(oauthRefreshToken);
await _context.SaveChangesAsync();
var result = new OAuthTokenResponseDto
{
AccessToken = accessToken,
TokenType = "Bearer",
ExpiresIn = 3600, // 1小时
RefreshToken = refreshToken,
Scope = scopes
};
return ApiResponse<OAuthTokenResponseDto>.SuccessResult(result);
}
public async Task<ApiResponse<OAuthTokenResponseDto>> RefreshTokenAsync(OAuthTokenRequestDto request)
{
// 验证客户端
var client = await GetClientByCredentialsAsync(request.ClientId, request.ClientSecret);
if (client == null)
{
return ApiResponse<OAuthTokenResponseDto>.ErrorResult("无效的客户端凭据");
}
// 验证刷新令牌
var refreshToken = await _context.OAuthRefreshTokens
.Include(t => t.Client)
.Include(t => t.User)
.FirstOrDefaultAsync(t => t.Token == request.RefreshToken && t.ClientId == client.Id);
if (refreshToken == null || refreshToken.IsUsed || refreshToken.ExpiresAt < DateTime.UtcNow)
{
return ApiResponse<OAuthTokenResponseDto>.ErrorResult("无效的刷新令牌");
}
// 标记旧刷新令牌为已使用
refreshToken.IsUsed = true;
// 撤销旧访问令牌
var oldAccessTokens = await _context.OAuthAccessTokens
.Where(t => t.UserId == refreshToken.UserId && t.ClientId == client.Id && !t.IsRevoked)
.ToListAsync();
foreach (var token in oldAccessTokens)
{
token.IsRevoked = true;
}
// 生成新的访问令牌和刷新令牌
var newAccessToken = GenerateRandomString(64);
var newRefreshToken = GenerateRandomString(64);
var scopes = !string.IsNullOrEmpty(request.Scope) ? request.Scope : "";
var newOAuthAccessToken = new OAuthAccessToken
{
Token = newAccessToken,
ClientId = client.Id,
UserId = refreshToken.UserId,
Scopes = scopes,
IsRevoked = false,
ExpiresAt = DateTime.UtcNow.AddHours(1), // 访问令牌1小时后过期
CreatedAt = DateTime.UtcNow
};
var newOAuthRefreshToken = new OAuthRefreshToken
{
Token = newRefreshToken,
ClientId = client.Id,
UserId = refreshToken.UserId,
IsUsed = false,
ExpiresAt = DateTime.UtcNow.AddDays(30), // 刷新令牌30天后过期
CreatedAt = DateTime.UtcNow
};
_context.OAuthAccessTokens.Add(newOAuthAccessToken);
_context.OAuthRefreshTokens.Add(newOAuthRefreshToken);
await _context.SaveChangesAsync();
var result = new OAuthTokenResponseDto
{
AccessToken = newAccessToken,
TokenType = "Bearer",
ExpiresIn = 3600, // 1小时
RefreshToken = newRefreshToken,
Scope = scopes
};
return ApiResponse<OAuthTokenResponseDto>.SuccessResult(result);
}
public async Task<ApiResponse<bool>> RevokeTokenAsync(string token)
{
var accessToken = await _context.OAuthAccessTokens
.FirstOrDefaultAsync(t => t.Token == token);
if (accessToken == null)
{
return ApiResponse<bool>.ErrorResult("令牌不存在");
}
accessToken.IsRevoked = true;
await _context.SaveChangesAsync();
return ApiResponse<bool>.SuccessResult(true, "令牌已撤销");
}
public async Task<ApiResponse<bool>> ValidateTokenAsync(string token)
{
var accessToken = await _context.OAuthAccessTokens
.FirstOrDefaultAsync(t => t.Token == token && !t.IsRevoked && t.ExpiresAt > DateTime.UtcNow);
if (accessToken == null)
{
return ApiResponse<bool>.ErrorResult("无效的令牌");
}
return ApiResponse<bool>.SuccessResult(true);
}
public async Task<OAuthAccessToken?> GetAccessTokenAsync(string token)
{
return await _context.OAuthAccessTokens
.Include(t => t.Client)
.Include(t => t.User)
.FirstOrDefaultAsync(t => t.Token == token && !t.IsRevoked && t.ExpiresAt > DateTime.UtcNow);
}
public async Task<OAuthClient?> GetClientByCredentialsAsync(string clientId, string clientSecret)
{
if (string.IsNullOrEmpty(clientSecret))
{
// 公开客户端只验证客户端ID
return await _context.OAuthClients
.FirstOrDefaultAsync(c => c.ClientId == clientId && c.IsActive);
}
// 机密客户端验证客户端ID和密钥
return await _context.OAuthClients
.FirstOrDefaultAsync(c => c.ClientId == clientId && c.ClientSecret == clientSecret && c.IsActive);
}
public async Task<ApiResponse<OAuthTokenResponseDto>> LoginAsync(OAuthLoginDto loginDto)
{
// 验证客户端
var client = await GetClientByCredentialsAsync(loginDto.ClientId, loginDto.ClientSecret);
if (client == null)
{
return ApiResponse<OAuthTokenResponseDto>.ErrorResult("无效的客户端凭据");
}
// 验证用户凭据
var user = await _context.Users
.FirstOrDefaultAsync(u => (u.Email == loginDto.UsernameOrEmail || u.Nickname == loginDto.UsernameOrEmail));
if (user == null)
{
return ApiResponse<OAuthTokenResponseDto>.ErrorResult("用户名或密码错误");
}
// 验证密码
if (!_passwordHelper.VerifyPassword(loginDto.Password, user.PasswordHash))
{
return ApiResponse<OAuthTokenResponseDto>.ErrorResult("用户名或密码错误");
}
// 生成访问令牌和刷新令牌
var accessToken = GenerateRandomString(64);
var refreshToken = GenerateRandomString(64);
var oauthAccessToken = new OAuthAccessToken
{
Token = accessToken,
ClientId = client.Id,
UserId = user.Id,
Scopes = loginDto.Scope,
IsRevoked = false,
ExpiresAt = DateTime.UtcNow.AddHours(1), // 访问令牌1小时后过期
CreatedAt = DateTime.UtcNow
};
var oauthRefreshToken = new OAuthRefreshToken
{
Token = refreshToken,
ClientId = client.Id,
UserId = user.Id,
IsUsed = false,
ExpiresAt = DateTime.UtcNow.AddDays(30), // 刷新令牌30天后过期
CreatedAt = DateTime.UtcNow
};
_context.OAuthAccessTokens.Add(oauthAccessToken);
_context.OAuthRefreshTokens.Add(oauthRefreshToken);
await _context.SaveChangesAsync();
var result = new OAuthTokenResponseDto
{
AccessToken = accessToken,
TokenType = "Bearer",
ExpiresIn = 3600, // 1小时
RefreshToken = refreshToken,
Scope = loginDto.Scope
};
return ApiResponse<OAuthTokenResponseDto>.SuccessResult(result);
}
private string GenerateRandomString(int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var random = new Random();
var result = new char[length];
for (int i = 0; i < length; i++)
{
result[i] = chars[random.Next(chars.Length)];
}
return new string(result);
}
}
}