주기적인 인증 기능 포함 및 코드 정리
This commit is contained in:
@@ -1,10 +0,0 @@
|
|||||||
<Application x:Class="AuthTest.App"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:local="clr-namespace:AuthTest"
|
|
||||||
ShutdownMode="OnMainWindowClose"
|
|
||||||
StartupUri="MainWindow.xaml">
|
|
||||||
<Application.Resources>
|
|
||||||
|
|
||||||
</Application.Resources>
|
|
||||||
</Application>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using BaronSoftware.SSO;
|
|
||||||
using System.Configuration;
|
|
||||||
using System.Data;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
|
|
||||||
namespace AuthTest
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for App.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class App : Application
|
|
||||||
{
|
|
||||||
protected override void OnStartup(StartupEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnStartup(e);
|
|
||||||
|
|
||||||
BaronSSOOption option = new()
|
|
||||||
{
|
|
||||||
Authority = "https://sso.hmac.kr/oidc",
|
|
||||||
ClientId = "aca44872-8280-40c3-9a80-3aefafdf722a",
|
|
||||||
RedirectUri = "http://localhost:9090/eg-bim/auth/callback",
|
|
||||||
PostLogoutRedirectUri = "http://localhost:9090/eg-bim/logout/callback",
|
|
||||||
ExtraUserValidator = new ExtraUserInvalidator()
|
|
||||||
};
|
|
||||||
var auth = new BaronSSO(option);
|
|
||||||
auth.SignIn();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
|
|
||||||
[assembly: ThemeInfo(
|
|
||||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
|
||||||
//(used if a resource is not found in the page,
|
|
||||||
// or application resource dictionaries)
|
|
||||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
|
||||||
//(used if a resource is not found in the page,
|
|
||||||
// app, or any theme specific resource dictionaries)
|
|
||||||
)]
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<UseWPF>true</UseWPF>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\BaronSoftware.SSO\BaronSoftware.SSO.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using BaronSoftware.SSO;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace AuthTest
|
|
||||||
{
|
|
||||||
public class ExtraUserInvalidator : IUserValidator
|
|
||||||
{
|
|
||||||
public void Validate(UserInfo user)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<Window x:Class="AuthTest.MainWindow"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:local="clr-namespace:AuthTest"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Title="MainWindow" Height="450" Width="800">
|
|
||||||
<Grid>
|
|
||||||
|
|
||||||
</Grid>
|
|
||||||
</Window>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
|
|
||||||
namespace AuthTest
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for MainWindow.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class MainWindow : Window
|
|
||||||
{
|
|
||||||
public MainWindow()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,9 @@ namespace BaronSoftware.SSO.Sample
|
|||||||
Authority = _settings.Oidc.Authority,
|
Authority = _settings.Oidc.Authority,
|
||||||
ClientId = _settings.Oidc.ClientId,
|
ClientId = _settings.Oidc.ClientId,
|
||||||
RedirectUri = _settings.Oidc.RedirectUri,
|
RedirectUri = _settings.Oidc.RedirectUri,
|
||||||
ExtraUserValidator = new SimpleUserValidator()
|
ExtraUserValidator = new SimpleUserValidator(),
|
||||||
|
LicenseCheckInterval = 15,
|
||||||
|
ExpiredToken = () => { MessageBox.Show("토큰이 끊겼습니다. 다시 로그인 하세요"); },
|
||||||
};
|
};
|
||||||
|
|
||||||
_license = new BaronSSO(option);
|
_license = new BaronSSO(option);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"Authority": "https://sso.hmac.kr/oidc",
|
"Authority": "https://sso.hmac.kr/oidc",
|
||||||
|
|
||||||
// 퍼블릭 클라이언트 ID (콘솔 '앱 자격 증명' 화면)
|
// 퍼블릭 클라이언트 ID (콘솔 '앱 자격 증명' 화면)
|
||||||
"ClientId": "4e4c88fc-2b0a-4b8b-b9b5-fb407cdbbfac",
|
"ClientId": "c072532b-3483-4539-a21f-e77a9f6e2c7a",
|
||||||
|
|
||||||
// 콘솔 '리디렉션 URI 설정'에 등록된 값과 문자 그대로 일치해야 함
|
// 콘솔 '리디렉션 URI 설정'에 등록된 값과 문자 그대로 일치해야 함
|
||||||
"RedirectUri": "http://127.0.0.1:8421/baron-sample/auth/callback",
|
"RedirectUri": "http://127.0.0.1:8421/baron-sample/auth/callback",
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaronSoftware.SSO", "BaronS
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaronSoftware.SSO.Sample", "BaronSoftware.SSO.Sample\BaronSoftware.SSO.Sample.csproj", "{6F80A264-7099-3F9E-64A3-89E569A3E7A4}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaronSoftware.SSO.Sample", "BaronSoftware.SSO.Sample\BaronSoftware.SSO.Sample.csproj", "{6F80A264-7099-3F9E-64A3-89E569A3E7A4}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthTest", "AuthTest\AuthTest.csproj", "{CC253CA9-0F0E-43D9-80D5-CEC2A181E889}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -23,10 +21,6 @@ Global
|
|||||||
{6F80A264-7099-3F9E-64A3-89E569A3E7A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{6F80A264-7099-3F9E-64A3-89E569A3E7A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{6F80A264-7099-3F9E-64A3-89E569A3E7A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{6F80A264-7099-3F9E-64A3-89E569A3E7A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{6F80A264-7099-3F9E-64A3-89E569A3E7A4}.Release|Any CPU.Build.0 = Release|Any CPU
|
{6F80A264-7099-3F9E-64A3-89E569A3E7A4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{CC253CA9-0F0E-43D9-80D5-CEC2A181E889}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{CC253CA9-0F0E-43D9-80D5-CEC2A181E889}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{CC253CA9-0F0E-43D9-80D5-CEC2A181E889}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{CC253CA9-0F0E-43D9-80D5-CEC2A181E889}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
using FluentScheduler;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.NetworkInformation;
|
||||||
|
|
||||||
namespace BaronSoftware.SSO
|
namespace BaronSoftware.SSO
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -7,6 +12,7 @@ namespace BaronSoftware.SSO
|
|||||||
{
|
{
|
||||||
private readonly SsoClient client;
|
private readonly SsoClient client;
|
||||||
private readonly BaronSSOOption option;
|
private readonly BaronSSOOption option;
|
||||||
|
private Schedule licenseCheckScheduler;
|
||||||
|
|
||||||
public UserInfo CurrentUser { get; private set; }
|
public UserInfo CurrentUser { get; private set; }
|
||||||
|
|
||||||
@@ -76,13 +82,13 @@ namespace BaronSoftware.SSO
|
|||||||
/// UI 스레드에서 <c>.Wait()</c>/<c>.Result</c> 로 블로킹하면 데드락입니다. 자세한 이유는
|
/// UI 스레드에서 <c>.Wait()</c>/<c>.Result</c> 로 블로킹하면 데드락입니다. 자세한 이유는
|
||||||
/// <see cref="SignInAsync()"/> 의 설명을 참고하세요. (UI 스레드면 await, 동기 호출은 <see cref="SignIn"/>)
|
/// <see cref="SignInAsync()"/> 의 설명을 참고하세요. (UI 스레드면 await, 동기 호출은 <see cref="SignIn"/>)
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public async Task SignInAsync(string refreshToken)
|
public async Task SignInAsync(string refreshToken, bool retryWindow = true)
|
||||||
{
|
{
|
||||||
UserInfo user = null;
|
UserInfo user = null;
|
||||||
var isConnected = await CheckConnection();
|
var isConnected = await CheckConnection();
|
||||||
if (isConnected)
|
if (isConnected)
|
||||||
{
|
{
|
||||||
var token = await client.LoginAsync(refreshToken);
|
var token = await client.LoginAsync(refreshToken, retryWindow);
|
||||||
var userJson = await client.GetUserInfoAsync(token.AccessToken);
|
var userJson = await client.GetUserInfoAsync(token.AccessToken);
|
||||||
if (string.IsNullOrEmpty(userJson))
|
if (string.IsNullOrEmpty(userJson))
|
||||||
throw new InvalidOperationException($"Failed to get userinfo : {token.ToString()}");
|
throw new InvalidOperationException($"Failed to get userinfo : {token.ToString()}");
|
||||||
@@ -98,7 +104,7 @@ namespace BaronSoftware.SSO
|
|||||||
|
|
||||||
user = UserInfo.FromSsoFile();
|
user = UserInfo.FromSsoFile();
|
||||||
if (user == null)
|
if (user == null)
|
||||||
throw new InvalidUserException("Not found sso data for offline.");
|
throw new InvalidUserException("Offline mode. Not found sso data for offline.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 가족사가 아니고, 인증도 실패 시
|
// 가족사가 아니고, 인증도 실패 시
|
||||||
@@ -109,8 +115,12 @@ namespace BaronSoftware.SSO
|
|||||||
|
|
||||||
CurrentUser = user;
|
CurrentUser = user;
|
||||||
CurrentUser.Save();
|
CurrentUser.Save();
|
||||||
|
|
||||||
|
RunLicenseInterval();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 로그아웃: 현재 사용자 정보 + 저장된 refresh_token + WebView SSO 세션 쿠키를 모두 삭제합니다.
|
/// 로그아웃: 현재 사용자 정보 + 저장된 refresh_token + WebView SSO 세션 쿠키를 모두 삭제합니다.
|
||||||
/// 세션 쿠키까지 지우므로 다음 로그인 시 로그인 창이 다시 표시됩니다.
|
/// 세션 쿠키까지 지우므로 다음 로그인 시 로그인 창이 다시 표시됩니다.
|
||||||
@@ -140,11 +150,59 @@ namespace BaronSoftware.SSO
|
|||||||
await client.GetDiscoveryAsync();
|
await client.GetDiscoveryAsync();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch
|
catch(Exception ex)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RunLicenseInterval()
|
||||||
|
{
|
||||||
|
if (licenseCheckScheduler != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
licenseCheckScheduler = new Schedule(async () =>
|
||||||
|
{
|
||||||
|
// 오프라인의 경우 주기적인 인증을 무시한다.
|
||||||
|
// 단, 언제든 온라인 상태가 될 수 있으니 주기 인증을 멈추지는 않는다.
|
||||||
|
var isOnline = await CheckConnection();
|
||||||
|
if (!isOnline)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SignInAsync(CurrentUser.RefreshToken, false);
|
||||||
|
}
|
||||||
|
catch(HttpRequestException)
|
||||||
|
{
|
||||||
|
//네트워크 오류는 무시한다.
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
StopLicenseInterval();
|
||||||
|
await SignOutAsync();
|
||||||
|
option.ExpiredToken?.Invoke();
|
||||||
|
}
|
||||||
|
}, run => run.Every(option.LicenseCheckInterval).Seconds()
|
||||||
|
);
|
||||||
|
|
||||||
|
licenseCheckScheduler.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopLicenseInterval()
|
||||||
|
{
|
||||||
|
if (licenseCheckScheduler == null)
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
licenseCheckScheduler.Stop();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
licenseCheckScheduler = null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Globalization;
|
using System.ComponentModel.Design.Serialization;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
namespace BaronSoftware.SSO
|
namespace BaronSoftware.SSO
|
||||||
{
|
{
|
||||||
@@ -38,6 +39,7 @@ namespace BaronSoftware.SSO
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableAutoLogin { get; set; } = true;
|
public bool EnableAutoLogin { get; set; } = true;
|
||||||
|
|
||||||
|
|
||||||
private IUserValidator familyValidator;
|
private IUserValidator familyValidator;
|
||||||
public IUserValidator FamilyValidator
|
public IUserValidator FamilyValidator
|
||||||
{
|
{
|
||||||
@@ -49,5 +51,24 @@ namespace BaronSoftware.SSO
|
|||||||
/// 사용자 인증에 대한 추가 검증이 필요한 경우, IUserValidator 인터페이스를 구현하여 Validator 속성에 할당할 수 있습니다.
|
/// 사용자 인증에 대한 추가 검증이 필요한 경우, IUserValidator 인터페이스를 구현하여 Validator 속성에 할당할 수 있습니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required IUserValidator? ExtraUserValidator { get; set; }
|
public required IUserValidator? ExtraUserValidator { get; set; }
|
||||||
|
|
||||||
|
private int licenseCheckInterval = 60;
|
||||||
|
/// <summary>
|
||||||
|
/// 주기적인 라이센스 확인 (초)
|
||||||
|
/// 최소 값은 30초 입니다.
|
||||||
|
/// </summary>
|
||||||
|
public int LicenseCheckInterval
|
||||||
|
{
|
||||||
|
get => licenseCheckInterval;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value < 15)
|
||||||
|
licenseCheckInterval = 15;
|
||||||
|
licenseCheckInterval = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Action ExpiredToken { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- 임베디드 웹뷰 (Edge/Chromium 기반) -->
|
<!-- 임베디드 웹뷰 (Edge/Chromium 기반) -->
|
||||||
|
<PackageReference Include="FluentScheduler" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.*" />
|
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.*" />
|
||||||
<!-- OIDC Discovery / JWKS / id_token 서명검증 -->
|
<!-- OIDC Discovery / JWKS / id_token 서명검증 -->
|
||||||
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.*" />
|
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.*" />
|
||||||
|
|||||||
@@ -74,9 +74,9 @@ namespace BaronSoftware.SSO
|
|||||||
/// refreshToken 기반 인증: 저장된 refresh_token으로 창 없이(無窓) 토큰을 재발급한다.
|
/// refreshToken 기반 인증: 저장된 refresh_token으로 창 없이(無窓) 토큰을 재발급한다.
|
||||||
/// refresh_token이 만료/무효 등으로 실패하면 웹뷰 대화형 로그인(LoginWindow)으로 폴백한다.
|
/// refresh_token이 만료/무효 등으로 실패하면 웹뷰 대화형 로그인(LoginWindow)으로 폴백한다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<TokenResponse> LoginAsync(string refreshToken)
|
public async Task<TokenResponse> LoginAsync(string refreshToken, bool retryWindow = true)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(refreshToken))
|
if (string.IsNullOrWhiteSpace(refreshToken) && retryWindow)
|
||||||
return await LoginAsync();
|
return await LoginAsync();
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -91,9 +91,9 @@ namespace BaronSoftware.SSO
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// 저장된 refresh_token이 만료/폐기/무효 → 자동 로그인 실패.
|
if(retryWindow)
|
||||||
// 사용자가 다시 로그인할 수 있도록 웹뷰 로그인 창을 띄운다.
|
return await LoginAsync();
|
||||||
return await LoginAsync();
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user