From ee38072ff9579ec0dc2007fe5db5e0510609d601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A4=80=EC=98=81?= Date: Thu, 25 Jun 2026 13:50:54 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A3=BC=EA=B8=B0=EC=A0=81=EC=9D=B8=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B8=B0=EB=8A=A5=20=ED=8F=AC=ED=95=A8=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AuthTest/App.xaml | 10 ---- AuthTest/App.xaml.cs | 31 ---------- AuthTest/AssemblyInfo.cs | 10 ---- AuthTest/AuthTest.csproj | 15 ----- AuthTest/ExtraUserInvalidator.cs | 16 ----- AuthTest/MainWindow.xaml | 12 ---- AuthTest/MainWindow.xaml.cs | 24 -------- BaronSoftware.SSO.Sample/MainWindow.xaml.cs | 4 +- BaronSoftware.SSO.Sample/appsettings.json | 2 +- BaronSoftware.SSO.sln | 6 -- BaronSoftware.SSO/BaronSSO.cs | 66 +++++++++++++++++++-- BaronSoftware.SSO/BaronSSOOption.cs | 23 ++++++- BaronSoftware.SSO/BaronSoftware.SSO.csproj | 1 + BaronSoftware.SSO/OIDC/SsoClient.cs | 10 ++-- 14 files changed, 94 insertions(+), 136 deletions(-) delete mode 100644 AuthTest/App.xaml delete mode 100644 AuthTest/App.xaml.cs delete mode 100644 AuthTest/AssemblyInfo.cs delete mode 100644 AuthTest/AuthTest.csproj delete mode 100644 AuthTest/ExtraUserInvalidator.cs delete mode 100644 AuthTest/MainWindow.xaml delete mode 100644 AuthTest/MainWindow.xaml.cs diff --git a/AuthTest/App.xaml b/AuthTest/App.xaml deleted file mode 100644 index 4cd93b6..0000000 --- a/AuthTest/App.xaml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/AuthTest/App.xaml.cs b/AuthTest/App.xaml.cs deleted file mode 100644 index 983332d..0000000 --- a/AuthTest/App.xaml.cs +++ /dev/null @@ -1,31 +0,0 @@ -using BaronSoftware.SSO; -using System.Configuration; -using System.Data; -using System.Threading.Tasks; -using System.Windows; - -namespace AuthTest -{ - /// - /// Interaction logic for App.xaml - /// - 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(); - } - } - -} diff --git a/AuthTest/AssemblyInfo.cs b/AuthTest/AssemblyInfo.cs deleted file mode 100644 index b0ec827..0000000 --- a/AuthTest/AssemblyInfo.cs +++ /dev/null @@ -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) -)] diff --git a/AuthTest/AuthTest.csproj b/AuthTest/AuthTest.csproj deleted file mode 100644 index 4a4e51e..0000000 --- a/AuthTest/AuthTest.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - WinExe - net8.0-windows - enable - enable - true - - - - - - - diff --git a/AuthTest/ExtraUserInvalidator.cs b/AuthTest/ExtraUserInvalidator.cs deleted file mode 100644 index 456ca5a..0000000 --- a/AuthTest/ExtraUserInvalidator.cs +++ /dev/null @@ -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) - { - } - } -} diff --git a/AuthTest/MainWindow.xaml b/AuthTest/MainWindow.xaml deleted file mode 100644 index 669c1d9..0000000 --- a/AuthTest/MainWindow.xaml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/AuthTest/MainWindow.xaml.cs b/AuthTest/MainWindow.xaml.cs deleted file mode 100644 index 5124d38..0000000 --- a/AuthTest/MainWindow.xaml.cs +++ /dev/null @@ -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 -{ - /// - /// Interaction logic for MainWindow.xaml - /// - public partial class MainWindow : Window - { - public MainWindow() - { - InitializeComponent(); - } - } -} \ No newline at end of file diff --git a/BaronSoftware.SSO.Sample/MainWindow.xaml.cs b/BaronSoftware.SSO.Sample/MainWindow.xaml.cs index d395002..942a22d 100644 --- a/BaronSoftware.SSO.Sample/MainWindow.xaml.cs +++ b/BaronSoftware.SSO.Sample/MainWindow.xaml.cs @@ -25,7 +25,9 @@ namespace BaronSoftware.SSO.Sample Authority = _settings.Oidc.Authority, ClientId = _settings.Oidc.ClientId, RedirectUri = _settings.Oidc.RedirectUri, - ExtraUserValidator = new SimpleUserValidator() + ExtraUserValidator = new SimpleUserValidator(), + LicenseCheckInterval = 15, + ExpiredToken = () => { MessageBox.Show("토큰이 끊겼습니다. 다시 로그인 하세요"); }, }; _license = new BaronSSO(option); diff --git a/BaronSoftware.SSO.Sample/appsettings.json b/BaronSoftware.SSO.Sample/appsettings.json index 07fa915..e51af79 100644 --- a/BaronSoftware.SSO.Sample/appsettings.json +++ b/BaronSoftware.SSO.Sample/appsettings.json @@ -9,7 +9,7 @@ "Authority": "https://sso.hmac.kr/oidc", // 퍼블릭 클라이언트 ID (콘솔 '앱 자격 증명' 화면) - "ClientId": "4e4c88fc-2b0a-4b8b-b9b5-fb407cdbbfac", + "ClientId": "c072532b-3483-4539-a21f-e77a9f6e2c7a", // 콘솔 '리디렉션 URI 설정'에 등록된 값과 문자 그대로 일치해야 함 "RedirectUri": "http://127.0.0.1:8421/baron-sample/auth/callback", diff --git a/BaronSoftware.SSO.sln b/BaronSoftware.SSO.sln index 360dc8d..b477436 100644 --- a/BaronSoftware.SSO.sln +++ b/BaronSoftware.SSO.sln @@ -7,8 +7,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaronSoftware.SSO", "BaronS EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaronSoftware.SSO.Sample", "BaronSoftware.SSO.Sample\BaronSoftware.SSO.Sample.csproj", "{6F80A264-7099-3F9E-64A3-89E569A3E7A4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthTest", "AuthTest\AuthTest.csproj", "{CC253CA9-0F0E-43D9-80D5-CEC2A181E889}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.Release|Any CPU.ActiveCfg = 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 GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/BaronSoftware.SSO/BaronSSO.cs b/BaronSoftware.SSO/BaronSSO.cs index a88e0c3..363a7d0 100644 --- a/BaronSoftware.SSO/BaronSSO.cs +++ b/BaronSoftware.SSO/BaronSSO.cs @@ -1,3 +1,8 @@ +using FluentScheduler; +using System.ComponentModel; +using System.Net.Http; +using System.Net.NetworkInformation; + namespace BaronSoftware.SSO { /// @@ -7,6 +12,7 @@ namespace BaronSoftware.SSO { private readonly SsoClient client; private readonly BaronSSOOption option; + private Schedule licenseCheckScheduler; public UserInfo CurrentUser { get; private set; } @@ -76,13 +82,13 @@ namespace BaronSoftware.SSO /// UI 스레드에서 .Wait()/.Result 로 블로킹하면 데드락입니다. 자세한 이유는 /// 의 설명을 참고하세요. (UI 스레드면 await, 동기 호출은 ) /// - public async Task SignInAsync(string refreshToken) + public async Task SignInAsync(string refreshToken, bool retryWindow = true) { UserInfo user = null; var isConnected = await CheckConnection(); if (isConnected) { - var token = await client.LoginAsync(refreshToken); + var token = await client.LoginAsync(refreshToken, retryWindow); var userJson = await client.GetUserInfoAsync(token.AccessToken); if (string.IsNullOrEmpty(userJson)) throw new InvalidOperationException($"Failed to get userinfo : {token.ToString()}"); @@ -98,7 +104,7 @@ namespace BaronSoftware.SSO user = UserInfo.FromSsoFile(); 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.Save(); + + RunLicenseInterval(); } + + /// /// 로그아웃: 현재 사용자 정보 + 저장된 refresh_token + WebView SSO 세션 쿠키를 모두 삭제합니다. /// 세션 쿠키까지 지우므로 다음 로그인 시 로그인 창이 다시 표시됩니다. @@ -140,11 +150,59 @@ namespace BaronSoftware.SSO await client.GetDiscoveryAsync(); return true; } - catch + catch(Exception ex) { 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; + } + } } diff --git a/BaronSoftware.SSO/BaronSSOOption.cs b/BaronSoftware.SSO/BaronSSOOption.cs index 1ab4bb8..f8f066d 100644 --- a/BaronSoftware.SSO/BaronSSOOption.cs +++ b/BaronSoftware.SSO/BaronSSOOption.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System.ComponentModel.Design.Serialization; +using System.Globalization; namespace BaronSoftware.SSO { @@ -38,6 +39,7 @@ namespace BaronSoftware.SSO /// public bool EnableAutoLogin { get; set; } = true; + private IUserValidator familyValidator; public IUserValidator FamilyValidator { @@ -49,5 +51,24 @@ namespace BaronSoftware.SSO /// 사용자 인증에 대한 추가 검증이 필요한 경우, IUserValidator 인터페이스를 구현하여 Validator 속성에 할당할 수 있습니다. /// public required IUserValidator? ExtraUserValidator { get; set; } + + private int licenseCheckInterval = 60; + /// + /// 주기적인 라이센스 확인 (초) + /// 최소 값은 30초 입니다. + /// + public int LicenseCheckInterval + { + get => licenseCheckInterval; + set + { + if (value < 15) + licenseCheckInterval = 15; + licenseCheckInterval = value; + } + } + + + public Action ExpiredToken { get; set; } } } diff --git a/BaronSoftware.SSO/BaronSoftware.SSO.csproj b/BaronSoftware.SSO/BaronSoftware.SSO.csproj index 30f1ed1..b6d499b 100644 --- a/BaronSoftware.SSO/BaronSoftware.SSO.csproj +++ b/BaronSoftware.SSO/BaronSoftware.SSO.csproj @@ -9,6 +9,7 @@ + diff --git a/BaronSoftware.SSO/OIDC/SsoClient.cs b/BaronSoftware.SSO/OIDC/SsoClient.cs index 989893d..d9f3017 100644 --- a/BaronSoftware.SSO/OIDC/SsoClient.cs +++ b/BaronSoftware.SSO/OIDC/SsoClient.cs @@ -74,9 +74,9 @@ namespace BaronSoftware.SSO /// refreshToken 기반 인증: 저장된 refresh_token으로 창 없이(無窓) 토큰을 재발급한다. /// refresh_token이 만료/무효 등으로 실패하면 웹뷰 대화형 로그인(LoginWindow)으로 폴백한다. /// - public async Task LoginAsync(string refreshToken) + public async Task LoginAsync(string refreshToken, bool retryWindow = true) { - if (string.IsNullOrWhiteSpace(refreshToken)) + if (string.IsNullOrWhiteSpace(refreshToken) && retryWindow) return await LoginAsync(); try @@ -91,9 +91,9 @@ namespace BaronSoftware.SSO } catch { - // 저장된 refresh_token이 만료/폐기/무효 → 자동 로그인 실패. - // 사용자가 다시 로그인할 수 있도록 웹뷰 로그인 창을 띄운다. - return await LoginAsync(); + if(retryWindow) + return await LoginAsync(); + throw; } }