Вынес получение jwt другой проект
All checks were successful
Create and publish a Docker image / Publish image (push) Successful in 54s

This commit is contained in:
2025-11-14 03:16:58 +03:00
parent 4ac3494833
commit 8eb10180f0
7 changed files with 72 additions and 147 deletions

View File

@@ -10,17 +10,5 @@ RUN dotnet publish "SfeduSchedule.csproj" -c $BUILD_CONFIGURATION -o /app/publis
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
EXPOSE 8080
WORKDIR /app
RUN apt-get update && \
apt-get install -y --no-install-recommends wget && \
wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.5.2/powershell_7.5.2-1.deb_amd64.deb && \
apt-get install -y ./powershell_7.5.2-1.deb_amd64.deb && \
rm -f powershell_7.5.2-1.deb_amd64.deb && \
rm -rf /var/lib/apt/lists/*
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
COPY --from=build /app/publish .
RUN pwsh ./playwright.ps1 install --with-deps chromium
ENTRYPOINT ["dotnet", "SfeduSchedule.dll"]

View File

@@ -1,70 +0,0 @@
using System.Text.RegularExpressions;
using Microsoft.Playwright;
namespace SfeduSchedule.BrowserScripts;
public static class MicrosoftLoginHelper
{
private static readonly string LoginUrl = "https://sfedu.modeus.org/";
// private static readonly string StorageStatePath = "ms_storage_state.json";
public static async Task LoginMicrosoftAsync(IPage page, string username, string password)
{
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
throw new Exception("username и password обязательны для авторизации Microsoft");
await page.GotoAsync(LoginUrl, new PageGotoOptions { WaitUntil = WaitUntilState.DOMContentLoaded });
await page.WaitForURLAsync(new Regex("login\\.(microsoftonline|live)\\.com", RegexOptions.IgnoreCase), new PageWaitForURLOptions { Timeout = 60_000 });
var useAnotherAccount = page.Locator("div#otherTile, #otherTileText, div[data-test-id='useAnotherAccount']").First;
try
{
await Assertions.Expect(useAnotherAccount).ToBeVisibleAsync(new() { Timeout = 2000 });
await useAnotherAccount.ClickAsync();
}
catch (PlaywrightException)
{
// Кнопка не появилась — пропускаем
}
var emailInput = page.Locator("input[name='loginfmt'], input#i0116");
await emailInput.WaitForAsync(new LocatorWaitForOptions { State = WaitForSelectorState.Visible, Timeout = 30_000 });
await emailInput.FillAsync(username);
var nextButton = page.Locator("#idSIButton9, input#idSIButton9");
await nextButton.ClickAsync();
var passwordInput = page.Locator("input[name='passwd'], input#i0118");
await passwordInput.WaitForAsync(new LocatorWaitForOptions { State = WaitForSelectorState.Visible, Timeout = 30_000 });
await passwordInput.FillAsync(password);
await nextButton.ClickAsync();
await page.WaitForSelectorAsync("button, input[type='submit'], a", new PageWaitForSelectorOptions { Timeout = 8000 });
var locator = page.Locator("#idSIButton9, #idBtn_Back").First;
try
{
await Assertions.Expect(locator).ToBeVisibleAsync(new() { Timeout = 3000 });
var noBtn = page.Locator("#idBtn_Back");
if (await noBtn.IsVisibleAsync())
await noBtn.ClickAsync();
else
await page.Locator("#idSIButton9").ClickAsync();
}
catch (PlaywrightException)
{
// Кнопки не появились — пропускаем этот шаг
}
await page.WaitForURLAsync(url => !Regex.IsMatch(new Uri(url).Host, "login\\.(microsoftonline|live)\\.com", RegexOptions.IgnoreCase), new PageWaitForURLOptions { Timeout = 60_000 });
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// Сохраняем storage state после успешного входа
// await page.Context.StorageStateAsync(new BrowserContextStorageStateOptions { Path = StorageStatePath });
var currentHost = new Uri(page.Url).Host;
if (Regex.IsMatch(currentHost, "login\\.(microsoftonline|live)\\.com", RegexOptions.IgnoreCase))
throw new Exception("Авторизация не завершена: остались на странице Microsoft Login");
}
}

View File

@@ -1,87 +1,98 @@
using Microsoft.Playwright;
using Quartz;
using SfeduSchedule.BrowserScripts;
namespace SfeduSchedule.Jobs;
public class UpdateJwtJob(IConfiguration configuration, ILogger<UpdateJwtJob> logger) : IJob
public class UpdateJwtJob(IConfiguration configuration, ILogger<UpdateJwtJob> logger, IHttpClientFactory httpClientFactory) : IJob
{
private const int MaxAttempts = 5; // Максимальное число попыток
private const int DelaySeconds = 20; // Задержка между попытками в секундах
private const int TimeoutSeconds = 60; // Таймаут для каждого запроса в секундах
public async Task Execute(IJobExecutionContext jobContext)
{
logger.LogInformation("Начало выполнения UpdateJwtJob");
string? username = configuration["MS_USERNAME"];
string? password = configuration["MS_PASSWORD"];
var authUrl = configuration["AUTH_URL"] ?? "http://msauth/auth/ms";
var apiKey = configuration["AUTH_API_KEY"] ?? string.Empty;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
var client = httpClientFactory.CreateClient("authClient");
client.Timeout = TimeSpan.FromSeconds(TimeoutSeconds + 10);
if (!string.IsNullOrEmpty(apiKey))
{
logger.LogError("Не указаны учетные данные для входа");
return;
client.DefaultRequestHeaders.Remove("X-API-Key");
client.DefaultRequestHeaders.Add("X-API-Key", apiKey);
}
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
for (var attempt = 1; attempt <= MaxAttempts; attempt++)
{
Headless = true
});
var context = await browser.NewContextAsync(new BrowserNewContextOptions
{
ViewportSize = null
});
var page = await context.NewPageAsync();
try
{
logger.LogInformation("Начало выполнения авторизации Microsoft");
await MicrosoftLoginHelper.LoginMicrosoftAsync(page, username, password);
var sessionStorageJson = await page.EvaluateAsync<string>(@"
JSON.stringify(sessionStorage)
");
// Извлечение id_token из sessionStorageJson
string? idToken = null;
try
{
var sessionStorageDict = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(sessionStorageJson);
if (sessionStorageDict != null)
logger.LogInformation("Попытка {Attempt}/{MaxAttempts} получения JWT из {AuthUrl}", attempt, MaxAttempts, authUrl);
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(TimeoutSeconds));
var response = await client.GetAsync(authUrl, cts.Token);
if (!response.IsSuccessStatusCode)
{
var oidcKey = sessionStorageDict.Keys.FirstOrDefault(k => k.StartsWith("oidc.user:"));
if (oidcKey != null)
logger.LogWarning("Неуспешный статус при получении JWT: {StatusCode}", response.StatusCode);
if (attempt == MaxAttempts)
{
var oidcValueJson = sessionStorageDict[oidcKey]?.ToString();
if (!string.IsNullOrEmpty(oidcValueJson))
{
using var doc = System.Text.Json.JsonDocument.Parse(oidcValueJson);
if (doc.RootElement.TryGetProperty("id_token", out var idTokenElement))
{
idToken = idTokenElement.GetString();
}
}
logger.LogError("Достигнуто максимальное число попыток получения JWT");
return;
}
await Task.Delay(TimeSpan.FromSeconds(DelaySeconds), jobContext.CancellationToken);
continue;
}
var body = await response.Content.ReadFromJsonAsync<JwtResponse>(cancellationToken: jobContext.CancellationToken);
if (body is null || string.IsNullOrWhiteSpace(body.Jwt))
{
logger.LogWarning("Ответ от MSAuth не содержит jwt");
if (attempt == MaxAttempts)
{
logger.LogError("Достигнуто максимальное число попыток получения корректного JWT");
return;
}
await Task.Delay(TimeSpan.FromSeconds(DelaySeconds), jobContext.CancellationToken);
continue;
}
configuration["TOKEN"] = body.Jwt;
logger.LogInformation("JWT успешно обновлён");
return;
}
catch (OperationCanceledException ex)
{
logger.LogWarning(ex, "Таймаут при получении JWT (попытка {Attempt}/{MaxAttempts})", attempt, MaxAttempts);
if (attempt == MaxAttempts)
{
logger.LogError("Достигнут лимит по таймаутам при запросе JWT");
return;
}
await Task.Delay(TimeSpan.FromSeconds(DelaySeconds), jobContext.CancellationToken);
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при извлечении id_token из sessionStorageJson");
return;
logger.LogError(ex, "Ошибка при получении JWT (попытка {Attempt}/{MaxAttempts})", attempt, MaxAttempts);
if (attempt == MaxAttempts)
{
logger.LogError("Достигнуто максимальное число попыток из-за ошибок при запросе JWT");
return;
}
await Task.Delay(TimeSpan.FromSeconds(DelaySeconds), jobContext.CancellationToken);
}
configuration["TOKEN"] = idToken;
await File.WriteAllTextAsync(GlobalVariables.JwtFilePath, idToken + "\n" + DateTime.Now.ToString("O"));
logger.LogInformation("UpdateJwtJob выполнен успешно");
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при выполнении UpdateJwtJob");
}
finally
{
await context.CloseAsync();
await browser.CloseAsync();
}
}
private sealed record JwtResponse(string Jwt);
}

View File

@@ -60,6 +60,7 @@ if (!string.IsNullOrEmpty(tgChatId) && !string.IsNullOrEmpty(tgToken))
// Включаем MVC контроллеры
var mvcBuilder = builder.Services.AddControllers();
builder.Services.AddHttpClient<ModeusService>();
builder.Services.AddHttpClient("authClient");
builder.Services.AddAuthentication()
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(

View File

@@ -12,7 +12,6 @@
<ItemGroup>
<PackageReference Include="Ical.Net" Version="5.1.1" />
<PackageReference Include="Microsoft.Identity.Web" Version="3.14.1" />
<PackageReference Include="Microsoft.Playwright" Version="1.55.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageReference Include="Quartz.AspNetCore" Version="3.15.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />

View File

@@ -11,8 +11,6 @@ services:
- AzureAd:ClientSecret=
- AzureAd:Domain=sfedu.onmicrosoft.com
- AzureAd:CallbackPath=/signin-oidc
- MS_USERNAME=${MS_USERNAME}
- MS_PASSWORD=${MS_PASSWORD}
- TG_CHAT_ID=${TG_CHAT_ID}
- TG_TOKEN=${TG_TOKEN}
- API_KEY=${API_KEY}

View File

@@ -11,8 +11,6 @@ services:
- AzureAd:ClientSecret=
- AzureAd:Domain=sfedu.onmicrosoft.com
- AzureAd:CallbackPath=/signin-oidc
- MS_USERNAME=${MS_USERNAME}
- MS_PASSWORD=${MS_PASSWORD}
- TG_CHAT_ID=${TG_CHAT_ID}
- TG_TOKEN=${TG_TOKEN}
- API_KEY=${API_KEY}