Вынес получение jwt другой проект
All checks were successful
Create and publish a Docker image / Publish image (push) Successful in 54s
All checks were successful
Create and publish a Docker image / Publish image (push) Successful in 54s
This commit is contained in:
12
Dockerfile
12
Dockerfile
@@ -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
|
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
WORKDIR /app
|
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 .
|
COPY --from=build /app/publish .
|
||||||
RUN pwsh ./playwright.ps1 install --with-deps chromium
|
|
||||||
|
|
||||||
ENTRYPOINT ["dotnet", "SfeduSchedule.dll"]
|
ENTRYPOINT ["dotnet", "SfeduSchedule.dll"]
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +1,98 @@
|
|||||||
using Microsoft.Playwright;
|
|
||||||
using Quartz;
|
using Quartz;
|
||||||
using SfeduSchedule.BrowserScripts;
|
|
||||||
|
|
||||||
namespace SfeduSchedule.Jobs;
|
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)
|
public async Task Execute(IJobExecutionContext jobContext)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Начало выполнения UpdateJwtJob");
|
logger.LogInformation("Начало выполнения UpdateJwtJob");
|
||||||
|
|
||||||
string? username = configuration["MS_USERNAME"];
|
var authUrl = configuration["AUTH_URL"] ?? "http://msauth/auth/ms";
|
||||||
string? password = configuration["MS_PASSWORD"];
|
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("Не указаны учетные данные для входа");
|
client.DefaultRequestHeaders.Remove("X-API-Key");
|
||||||
return;
|
client.DefaultRequestHeaders.Add("X-API-Key", apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
using var playwright = await Playwright.CreateAsync();
|
for (var attempt = 1; attempt <= MaxAttempts; attempt++)
|
||||||
await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
|
|
||||||
{
|
{
|
||||||
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
|
try
|
||||||
{
|
{
|
||||||
var sessionStorageDict = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(sessionStorageJson);
|
logger.LogInformation("Попытка {Attempt}/{MaxAttempts} получения JWT из {AuthUrl}", attempt, MaxAttempts, authUrl);
|
||||||
if (sessionStorageDict != null)
|
|
||||||
|
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:"));
|
logger.LogWarning("Неуспешный статус при получении JWT: {StatusCode}", response.StatusCode);
|
||||||
if (oidcKey != null)
|
|
||||||
|
if (attempt == MaxAttempts)
|
||||||
{
|
{
|
||||||
var oidcValueJson = sessionStorageDict[oidcKey]?.ToString();
|
logger.LogError("Достигнуто максимальное число попыток получения JWT");
|
||||||
if (!string.IsNullOrEmpty(oidcValueJson))
|
return;
|
||||||
{
|
|
||||||
using var doc = System.Text.Json.JsonDocument.Parse(oidcValueJson);
|
|
||||||
if (doc.RootElement.TryGetProperty("id_token", out var idTokenElement))
|
|
||||||
{
|
|
||||||
idToken = idTokenElement.GetString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Ошибка при извлечении id_token из sessionStorageJson");
|
logger.LogError(ex, "Ошибка при получении JWT (попытка {Attempt}/{MaxAttempts})", attempt, MaxAttempts);
|
||||||
return;
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@@ -60,6 +60,7 @@ if (!string.IsNullOrEmpty(tgChatId) && !string.IsNullOrEmpty(tgToken))
|
|||||||
// Включаем MVC контроллеры
|
// Включаем MVC контроллеры
|
||||||
var mvcBuilder = builder.Services.AddControllers();
|
var mvcBuilder = builder.Services.AddControllers();
|
||||||
builder.Services.AddHttpClient<ModeusService>();
|
builder.Services.AddHttpClient<ModeusService>();
|
||||||
|
builder.Services.AddHttpClient("authClient");
|
||||||
|
|
||||||
builder.Services.AddAuthentication()
|
builder.Services.AddAuthentication()
|
||||||
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(
|
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Ical.Net" Version="5.1.1" />
|
<PackageReference Include="Ical.Net" Version="5.1.1" />
|
||||||
<PackageReference Include="Microsoft.Identity.Web" Version="3.14.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="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||||
<PackageReference Include="Quartz.AspNetCore" Version="3.15.1" />
|
<PackageReference Include="Quartz.AspNetCore" Version="3.15.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ services:
|
|||||||
- AzureAd:ClientSecret=
|
- AzureAd:ClientSecret=
|
||||||
- AzureAd:Domain=sfedu.onmicrosoft.com
|
- AzureAd:Domain=sfedu.onmicrosoft.com
|
||||||
- AzureAd:CallbackPath=/signin-oidc
|
- AzureAd:CallbackPath=/signin-oidc
|
||||||
- MS_USERNAME=${MS_USERNAME}
|
|
||||||
- MS_PASSWORD=${MS_PASSWORD}
|
|
||||||
- TG_CHAT_ID=${TG_CHAT_ID}
|
- TG_CHAT_ID=${TG_CHAT_ID}
|
||||||
- TG_TOKEN=${TG_TOKEN}
|
- TG_TOKEN=${TG_TOKEN}
|
||||||
- API_KEY=${API_KEY}
|
- API_KEY=${API_KEY}
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ services:
|
|||||||
- AzureAd:ClientSecret=
|
- AzureAd:ClientSecret=
|
||||||
- AzureAd:Domain=sfedu.onmicrosoft.com
|
- AzureAd:Domain=sfedu.onmicrosoft.com
|
||||||
- AzureAd:CallbackPath=/signin-oidc
|
- AzureAd:CallbackPath=/signin-oidc
|
||||||
- MS_USERNAME=${MS_USERNAME}
|
|
||||||
- MS_PASSWORD=${MS_PASSWORD}
|
|
||||||
- TG_CHAT_ID=${TG_CHAT_ID}
|
- TG_CHAT_ID=${TG_CHAT_ID}
|
||||||
- TG_TOKEN=${TG_TOKEN}
|
- TG_TOKEN=${TG_TOKEN}
|
||||||
- API_KEY=${API_KEY}
|
- API_KEY=${API_KEY}
|
||||||
|
|||||||
Reference in New Issue
Block a user