416 lines
16 KiB
C#
416 lines
16 KiB
C#
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);
|
||
}
|
||
}
|
||
} |