Compare commits
2 Commits
8a469625c5
...
858c383a9b
| Author | SHA1 | Date | |
|---|---|---|---|
| 858c383a9b | |||
| 0e7ee28791 |
8
.env.example
Normal file
8
.env.example
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# URL сервиса Modeus
|
||||||
|
MODEUS_URL=
|
||||||
|
# Имя пользователя для авторизации в Microsoft
|
||||||
|
MS_USERNAME=
|
||||||
|
# Пароль для авторизации в Microsoft
|
||||||
|
MS_PASSWORD=
|
||||||
|
# (опционально) Ключ API для защиты эндпоинта /auth/ms
|
||||||
|
API_KEY=
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -485,3 +485,5 @@ $RECYCLE.BIN/
|
|||||||
*.lnk
|
*.lnk
|
||||||
|
|
||||||
|
|
||||||
|
src/appsettings.Development.json
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ cd src
|
|||||||
dotnet run
|
dotnet run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
или
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose-dev.yml up
|
||||||
|
```
|
||||||
|
|
||||||
1. Выполните запрос:
|
1. Выполните запрос:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
14
docker-compose-dev.yml
Normal file
14
docker-compose-dev.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
container_name: ModeusSchedule.MSAuth
|
||||||
|
build:
|
||||||
|
context: ./src
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- '5000:8080'
|
||||||
|
environment:
|
||||||
|
- MODEUS_URL=${MODEUS_URL}
|
||||||
|
- MS_USERNAME=${MS_USERNAME}
|
||||||
|
- MS_PASSWORD=${MS_PASSWORD}
|
||||||
|
- API_KEY=${API_KEY}
|
||||||
|
restart: unless-stopped
|
||||||
27
src/.dockerignore
Normal file
27
src/.dockerignore
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
**/.playwright
|
||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/.idea
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/azds.yaml
|
||||||
|
**/bin
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/obj
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
|
**/appsettings.Development.json
|
||||||
@@ -1,61 +1,92 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Playwright;
|
using Microsoft.Playwright;
|
||||||
|
|
||||||
namespace ModeusSchedule.MSAuth.BrowserScripts;
|
namespace ModeusSchedule.MSAuth.BrowserScripts;
|
||||||
|
|
||||||
public static class MicrosoftLoginHelper
|
public static class MicrosoftLoginHelper
|
||||||
{
|
{
|
||||||
public static async Task LoginMicrosoftAsync(IPage page, string username, string password, string loginUrl)
|
public static async Task LoginMicrosoftAsync(
|
||||||
|
IPage page,
|
||||||
|
ILogger logger,
|
||||||
|
string username,
|
||||||
|
string password,
|
||||||
|
string loginUrl
|
||||||
|
)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation("Начало входа в Microsoft. Url: {LoginUrl}, пользователь: {Username}", loginUrl, username);
|
||||||
|
|
||||||
await page.GotoAsync(loginUrl, new PageGotoOptions { WaitUntil = WaitUntilState.DOMContentLoaded });
|
await page.GotoAsync(loginUrl, new PageGotoOptions { WaitUntil = WaitUntilState.DOMContentLoaded });
|
||||||
|
|
||||||
|
logger.LogDebug("Страница логина загружена, ожидаем переход на домен Microsoft.");
|
||||||
|
|
||||||
await page.WaitForURLAsync(new Regex("login\\.(microsoftonline|live)\\.com", RegexOptions.IgnoreCase), new PageWaitForURLOptions { Timeout = 60_000 });
|
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;
|
var useAnotherAccount = page.Locator("div#otherTile, #otherTileText, div[data-test-id='useAnotherAccount']").First;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Assertions.Expect(useAnotherAccount).ToBeVisibleAsync(new() { Timeout = 2000 });
|
await Assertions.Expect(useAnotherAccount).ToBeVisibleAsync(new() { Timeout = 2000 });
|
||||||
|
logger.LogDebug("Обнаружена кнопка 'Использовать другой аккаунт'. Нажимаем.");
|
||||||
await useAnotherAccount.ClickAsync();
|
await useAnotherAccount.ClickAsync();
|
||||||
}
|
}
|
||||||
catch (PlaywrightException)
|
catch (PlaywrightException ex)
|
||||||
{
|
{
|
||||||
// Кнопка не появилась — пропускаем
|
logger.LogDebug(ex, "Кнопка 'Использовать другой аккаунт' не появилась — пропускаем шаг.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var emailInput = page.Locator("input[name='loginfmt'], input#i0116");
|
var emailInput = page.Locator("input[name='loginfmt'], input#i0116");
|
||||||
await emailInput.WaitForAsync(new LocatorWaitForOptions { State = WaitForSelectorState.Visible, Timeout = 30_000 });
|
await emailInput.WaitForAsync(new LocatorWaitForOptions { State = WaitForSelectorState.Visible, Timeout = 30_000 });
|
||||||
|
logger.LogDebug("Поле ввода email найдено, вводим логин.");
|
||||||
await emailInput.FillAsync(username);
|
await emailInput.FillAsync(username);
|
||||||
|
|
||||||
var nextButton = page.Locator("#idSIButton9, input#idSIButton9");
|
var nextButton = page.Locator("#idSIButton9, input#idSIButton9");
|
||||||
await nextButton.ClickAsync();
|
await nextButton.ClickAsync();
|
||||||
|
|
||||||
|
logger.LogDebug("Нажата кнопка 'Далее' после ввода логина.");
|
||||||
|
|
||||||
var passwordInput = page.Locator("input[name='passwd'], input#i0118");
|
var passwordInput = page.Locator("input[name='passwd'], input#i0118");
|
||||||
await passwordInput.WaitForAsync(new LocatorWaitForOptions { State = WaitForSelectorState.Visible, Timeout = 30_000 });
|
await passwordInput.WaitForAsync(new LocatorWaitForOptions { State = WaitForSelectorState.Visible, Timeout = 30_000 });
|
||||||
|
logger.LogDebug("Поле ввода пароля найдено, вводим пароль.");
|
||||||
await passwordInput.FillAsync(password);
|
await passwordInput.FillAsync(password);
|
||||||
await nextButton.ClickAsync();
|
await nextButton.ClickAsync();
|
||||||
|
|
||||||
|
logger.LogDebug("Нажата кнопка входа после ввода пароля.");
|
||||||
|
|
||||||
await page.WaitForSelectorAsync("button, input[type='submit'], a", new PageWaitForSelectorOptions { Timeout = 8000 });
|
await page.WaitForSelectorAsync("button, input[type='submit'], a", new PageWaitForSelectorOptions { Timeout = 8000 });
|
||||||
|
|
||||||
var locator = page.Locator("#idSIButton9, #idBtn_Back").First;
|
var locator = page.Locator("#idSIButton9, #idBtn_Back").First;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Assertions.Expect(locator).ToBeVisibleAsync(new() { Timeout = 3000 });
|
await Assertions.Expect(locator).ToBeVisibleAsync(new() { Timeout = 3000 });
|
||||||
|
logger.LogDebug("Обнаружен экран 'Остаться в системе?'.");
|
||||||
var noBtn = page.Locator("#idBtn_Back");
|
var noBtn = page.Locator("#idBtn_Back");
|
||||||
if (await noBtn.IsVisibleAsync())
|
if (await noBtn.IsVisibleAsync())
|
||||||
|
{
|
||||||
|
logger.LogDebug("Нажимаем кнопку 'Нет'.");
|
||||||
await noBtn.ClickAsync();
|
await noBtn.ClickAsync();
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
logger.LogDebug("Кнопка 'Нет' не найдена, нажимаем 'Да'/'Далее'.");
|
||||||
await page.Locator("#idSIButton9").ClickAsync();
|
await page.Locator("#idSIButton9").ClickAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (PlaywrightException)
|
catch (PlaywrightException ex)
|
||||||
{
|
{
|
||||||
// Кнопки не появились — пропускаем этот шаг
|
logger.LogDebug(ex, "Экран 'Остаться в системе?' не появился — пропускаем шаг.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Ожидаем завершения редиректов после логина.");
|
||||||
await page.WaitForURLAsync(url => !Regex.IsMatch(new Uri(url).Host, "login\\.(microsoftonline|live)\\.com", RegexOptions.IgnoreCase), new PageWaitForURLOptions { Timeout = 60_000 });
|
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);
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||||
|
|
||||||
var currentHost = new Uri(page.Url).Host;
|
var currentHost = new Uri(page.Url).Host;
|
||||||
if (Regex.IsMatch(currentHost, "login\\.(microsoftonline|live)\\.com", RegexOptions.IgnoreCase))
|
if (Regex.IsMatch(currentHost, "login\\.(microsoftonline|live)\\.com", RegexOptions.IgnoreCase))
|
||||||
|
{
|
||||||
|
logger.LogError("Авторизация не завершена: остались на странице Microsoft Login. Текущий URL: {Url}", page.Url);
|
||||||
throw new Exception("Авторизация не завершена: остались на странице Microsoft Login");
|
throw new Exception("Авторизация не завершена: остались на странице Microsoft Login");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Успешный вход в Microsoft. Текущий URL: {Url}", page.Url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/Dockerfile
Normal file
24
src/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine3.22 AS build
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
RUN dotnet restore "ModeusSchedule.MSAuth.csproj"
|
||||||
|
RUN dotnet publish "ModeusSchedule.MSAuth.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
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", "ModeusSchedule.MSAuth.dll"]
|
||||||
@@ -8,6 +8,8 @@ if (string.IsNullOrWhiteSpace(builder.Configuration["MODEUS_URL"]))
|
|||||||
Environment.Exit(1);
|
Environment.Exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Используется URL Modeus: {builder.Configuration["MODEUS_URL"]}");
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(builder.Configuration["MS_USERNAME"]) || string.IsNullOrWhiteSpace(builder.Configuration["MS_PASSWORD"]))
|
if (string.IsNullOrWhiteSpace(builder.Configuration["MS_USERNAME"]) || string.IsNullOrWhiteSpace(builder.Configuration["MS_PASSWORD"]))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("Ошибка: не заданы учетные данные для MicrosoftAuth. Пожалуйста, укажите MS_USERNAME и MS_PASSWORD в файле конфигурации или переменных окружения.");
|
Console.Error.WriteLine("Ошибка: не заданы учетные данные для MicrosoftAuth. Пожалуйста, укажите MS_USERNAME и MS_PASSWORD в файле конфигурации или переменных окружения.");
|
||||||
|
|||||||
@@ -18,11 +18,15 @@ public class MicrosoftAuthService(ILogger<MicrosoftAuthService> logger, IConfigu
|
|||||||
|
|
||||||
public async Task<string> GetJwtAsync(CancellationToken ct = default)
|
public async Task<string> GetJwtAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
logger.LogDebug("Запрошен JWT токен. HasFreshToken=" + HasFreshToken);
|
||||||
await EnsureBrowsersAsync();
|
await EnsureBrowsersAsync();
|
||||||
|
|
||||||
// Если кэш актуален — вернуть сразу
|
// Если кэш актуален — вернуть сразу
|
||||||
if (HasFreshToken)
|
if (HasFreshToken)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Возвращаем закэшированный JWT токен, возраст={AgeSeconds} сек", (DateTime.UtcNow - _cachedAtUtc).TotalSeconds);
|
||||||
return _cachedToken!;
|
return _cachedToken!;
|
||||||
|
}
|
||||||
|
|
||||||
// Пытаемся единолично выполнить авторизацию
|
// Пытаемся единолично выполнить авторизацию
|
||||||
if (!await FetchLock.WaitAsync(0, ct))
|
if (!await FetchLock.WaitAsync(0, ct))
|
||||||
@@ -45,19 +49,29 @@ public class MicrosoftAuthService(ILogger<MicrosoftAuthService> logger, IConfigu
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
logger.LogInformation("Старт авторизации через Microsoft");
|
logger.LogInformation("Старт авторизации через Microsoft");
|
||||||
await MicrosoftLoginHelper.LoginMicrosoftAsync(page, configuration["MS_USERNAME"]!, configuration["MS_PASSWORD"]!, configuration["MODEUS_URL"]!);
|
await MicrosoftLoginHelper.LoginMicrosoftAsync(page, logger, configuration["MS_USERNAME"]!, configuration["MS_PASSWORD"]!, configuration["MODEUS_URL"]!);
|
||||||
|
|
||||||
var sessionStorageJson = await page.EvaluateAsync<string>("JSON.stringify(sessionStorage)");
|
var sessionStorageJson = await page.EvaluateAsync<string>("JSON.stringify(sessionStorage)");
|
||||||
|
|
||||||
|
logger.LogDebug("Пробуем извлечь id_token из sessionStorage");
|
||||||
string? idToken = ExtractIdToken(sessionStorageJson);
|
string? idToken = ExtractIdToken(sessionStorageJson);
|
||||||
if (string.IsNullOrWhiteSpace(idToken))
|
if (string.IsNullOrWhiteSpace(idToken))
|
||||||
|
{
|
||||||
|
logger.LogError("Не удалось извлечь id_token из sessionStorage");
|
||||||
throw new Exception("Не удалось извлечь id_token из sessionStorage");
|
throw new Exception("Не удалось извлечь id_token из sessionStorage");
|
||||||
|
}
|
||||||
|
|
||||||
// Сохраняем в кэш
|
// Сохраняем в кэш
|
||||||
_cachedToken = idToken;
|
_cachedToken = idToken;
|
||||||
_cachedAtUtc = DateTime.UtcNow;
|
_cachedAtUtc = DateTime.UtcNow;
|
||||||
|
logger.LogInformation("Успешно получили и закэшировали id_token");
|
||||||
return idToken;
|
return idToken;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) when (ex is not MicrosoftAuthInProgressException)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Ошибка при получении JWT через Microsoft авторизацию");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
await context.CloseAsync();
|
await context.CloseAsync();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Debug",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user