사용자 검증기 구조 분리: 내부(Family) / 외부(Extra) 검증 구분

- IUserValidator.Validate: bool 반환 → void (인증 실패 시 예외로 처리)
- BaronSSOOption: Validator → ExtraUserValidator로 명명, FamilyValidator(기본 DefaultFamilyUserValidator) 추가
- DefaultFamilyUserValidator 신규: Center/Family 테넌트 사용자 통과, 그 외 InvalidUserException
- BaronSSO.SignInAsync: Family/Extra 검증기 적용 흐름 정리
- InvalidUserException: UserInfo 기반 생성자
- Sample(MainWindow/SampleSettings/SimpleUserValidator) 갱신

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 14:54:21 +09:00
parent 1c41230021
commit a15cab4bc9
9 changed files with 103 additions and 58 deletions

View File

@@ -18,14 +18,10 @@
FontSize="16" FontWeight="Bold" Margin="0,0,0,12"/> FontSize="16" FontWeight="Bold" Margin="0,0,0,12"/>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,12"> <StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,12">
<Button x:Name="LoginButton" Content="웹뷰로 로그인" Width="140" Height="34" <Button x:Name="LoginButton" Content="웹뷰로 로그인" Width="140" Height="34"/>
Click="LoginButton_Click"/> <Button x:Name="TokenLoginBuggon" Content="토큰 로그인" Width="150" Height="34"/>
<Button x:Name="TokenLoginBuggon" Content="토큰 로그인" Width="150" Height="34" <Button x:Name="LogoutButton" Content="로그아웃" Width="100" Height="34"/>
Margin="8,0,0,0" Click="TokenLoginBuggon_Click"/> <Button x:Name="SettingsButton" Content="설정 변경" Width="90" Height="34"/>
<Button x:Name="LogoutButton" Content="로그아웃" Width="100" Height="34"
Margin="8,0,0,0" Click="LogoutButton_Click"/>
<Button x:Name="SettingsButton" Content="설정 변경" Width="90" Height="34"
Margin="8,0,0,0" Click="SettingsButton_Click"/>
</StackPanel> </StackPanel>
<Border Grid.Row="2" BorderBrush="#DDDDDD" BorderThickness="1" CornerRadius="4"> <Border Grid.Row="2" BorderBrush="#DDDDDD" BorderThickness="1" CornerRadius="4">

View File

@@ -1,11 +1,7 @@
using BaronSoftware;
using BaronSoftware.Auth;
using BaronSoftware.Auth.Sample; using BaronSoftware.Auth.Sample;
using System;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
namespace BaronSoftware.SSO.Sample namespace BaronSoftware.SSO.Sample
@@ -23,11 +19,22 @@ namespace BaronSoftware.SSO.Sample
InitializeComponent(); InitializeComponent();
_settings = SampleSettings.Load(); _settings = SampleSettings.Load();
ApplySettings();
}
/// <summary>현재 설정으로 SSO 클라이언트를 (재)생성한다.</summary> var option = new BaronSSOOption()
private void ApplySettings() => _license = new BaronSSO(_settings.ToOidcOptions()); {
Authority = _settings.Oidc.Authority,
ClientId = _settings.Oidc.ClientId,
RedirectUri = _settings.Oidc.RedirectUri,
ExtraUserValidator = new SimpleUserValidator()
};
_license = new BaronSSO(option);
LoginButton.Click += async (s,e) => await RunLogin("웹뷰 로그인", () => _license.SignInAsync());
TokenLoginBuggon.Click += async (s,e) => await RunLogin("토큰 로그인", () => _license.SignInAsync(_license.CurrentUser.RefreshToken));
SettingsButton.Click += SettingsButton_Click;
LogoutButton.Click += LogoutButton_Click;
}
private void SettingsButton_Click(object sender, RoutedEventArgs e) private void SettingsButton_Click(object sender, RoutedEventArgs e)
{ {
@@ -41,10 +48,21 @@ namespace BaronSoftware.SSO.Sample
_settings.Oidc.ClientId = dlg.ClientId; _settings.Oidc.ClientId = dlg.ClientId;
_settings.Oidc.RedirectUri = dlg.RedirectUri; _settings.Oidc.RedirectUri = dlg.RedirectUri;
_settings.Oidc.LogoutUri = dlg.LogoutUri; _settings.Oidc.LogoutUri = dlg.LogoutUri;
try try
{ {
_settings.Save(); _settings.Save();
ApplySettings();
var option = new BaronSSOOption()
{
Authority = _settings.Oidc.Authority,
ClientId = _settings.Oidc.ClientId,
RedirectUri = _settings.Oidc.RedirectUri,
ExtraUserValidator = new SimpleUserValidator()
};
_license = new BaronSSO(option);
OutputBox.Text = OutputBox.Text =
"설정 저장 완료 ✔ (appsettings.json)\n\n" + "설정 저장 완료 ✔ (appsettings.json)\n\n" +
$"Authority : {_settings.Oidc.Authority}\n" + $"Authority : {_settings.Oidc.Authority}\n" +
@@ -59,8 +77,6 @@ namespace BaronSoftware.SSO.Sample
} }
} }
private async void LoginButton_Click(object sender, RoutedEventArgs e)
=> await RunAsync("웹뷰 로그인", () => _license.SignInAsync());
private async void LogoutButton_Click(object sender, RoutedEventArgs e) private async void LogoutButton_Click(object sender, RoutedEventArgs e)
{ {
@@ -80,7 +96,7 @@ namespace BaronSoftware.SSO.Sample
} }
} }
private async Task RunAsync(string action, Func<Task> work) private async Task RunLogin(string action, Func<Task> work)
{ {
SetBusy(true); SetBusy(true);
OutputBox.Text = $"{action} 진행 중..."; OutputBox.Text = $"{action} 진행 중...";
@@ -88,7 +104,7 @@ namespace BaronSoftware.SSO.Sample
{ {
await work(); await work();
OutputBox.Text = $"{action} 완료 ✔\n\n"; OutputBox.Text = $"{action} 완료 ✔\n\n";
OutputBox.Text += Format(_license.CurrentUser); OutputBox.Text += ShowLoginUserInfo(_license.CurrentUser);
OutputBox.CaretIndex = 0; // 맨 위(요약)부터 보이도록 OutputBox.CaretIndex = 0; // 맨 위(요약)부터 보이도록
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -105,10 +121,10 @@ namespace BaronSoftware.SSO.Sample
} }
} }
private void SetBusy(bool busy) private void SetBusy(bool busy)=> LoginButton.IsEnabled = LogoutButton.IsEnabled = !busy;
=> LoginButton.IsEnabled = LogoutButton.IsEnabled = !busy;
private string Format(UserInfo u) // For Display=============
private string ShowLoginUserInfo(UserInfo u)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine("로그인 성공 ✔"); sb.AppendLine("로그인 성공 ✔");
@@ -150,6 +166,9 @@ namespace BaronSoftware.SSO.Sample
catch { return json; } catch { return json; }
} }
private async void TokenLoginBuggon_Click(object sender, RoutedEventArgs e) => await RunAsync("토큰 로그인", () => _license.SignInAsync(_license.CurrentUser.RefreshToken)); // ================
} }
} }

View File

@@ -73,7 +73,7 @@ namespace BaronSoftware.Auth.Sample
Authority = Oidc.Authority, Authority = Oidc.Authority,
ClientId = Oidc.ClientId, ClientId = Oidc.ClientId,
RedirectUri = Oidc.RedirectUri, RedirectUri = Oidc.RedirectUri,
Validator = new SimpleUserValidator() ExtraUserValidator = new SimpleUserValidator()
}; };
} }
} }

View File

@@ -3,15 +3,15 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using BaronSoftware.SSO;
namespace BaronSoftware.SSO.Sample namespace BaronSoftware.SSO.Sample
{ {
internal class SimpleUserValidator : IUserValidator internal class SimpleUserValidator : IUserValidator
{ {
public bool Validate(UserInfo user) public void Validate(UserInfo user)
{ {
// 가족사 아니면 거절 throw new InvalidUserException("user is not family member");
return false;
} }
} }
} }

View File

@@ -1,5 +1,3 @@
using BaronSoftware.SSO.Exceptions;
namespace BaronSoftware.SSO namespace BaronSoftware.SSO
{ {
/// <summary> /// <summary>
@@ -36,37 +34,45 @@ namespace BaronSoftware.SSO
} }
/// <summary>웹뷰 로그인 창을 띄워 인증합니다.</summary> /// <summary>웹뷰 로그인 창을 띄워 인증합니다.</summary>
public async Task SignInAsync() => await SignInAsync(null); public async Task SignInAsync()
{
UserInfo? user = null;
if (option.EnableAutoLogin)
user = UserInfo.FromSsoFile();
await SignInAsync(user?.RefreshToken);
}
/// <summary>웹뷰 로그인 창을 띄워 인증합니다.</summary>
public async Task SignInAsync(string refreshToken) public async Task SignInAsync(string refreshToken)
{ {
UserInfo user = null; UserInfo user = null;
if(option.EnableAutoLogin)
user = UserInfo.FromSsoFile();
var isConnected = await CheckConnection(); var isConnected = await CheckConnection();
if(!isConnected && option.EnableOffline) if (isConnected)
{ {
var token = await client.LoginAsync(refreshToken);
var userJson = await client.GetUserInfoAsync(token.AccessToken);
if (string.IsNullOrEmpty(userJson))
throw new InvalidOperationException($"Failed to get userinfo : {token.ToString()}");
user = UserInfo.FromJson(userJson, token);
if (user == null) if (user == null)
throw new InvalidUserException("Not found authorized last user."); throw new InvalidUserException($"Broken user token. Token {token.ToString()}, UserJson {userJson}");
option?.Validator?.Validate(user); }
return; else
{
if (!option.EnableOffline)
throw new InvalidOperationException("Network isn't available.");
user = UserInfo.FromSsoFile();
if (user == null)
throw new InvalidUserException("Not found sso data for offline.");
} }
var token = await client.LoginAsync(refreshToken);
var userJson = await client.GetUserInfoAsync(token.AccessToken);
if (string.IsNullOrEmpty(userJson))
throw new InvalidOperationException($"Failed to get userinfo : {token.ToString()}");
user = UserInfo.FromJson(userJson, token);
if (user == null || option?.Validator == null)
throw new NullReferenceException("user or validator option is null");
// 가족사가 아니고, 인증도 실패 시 // 가족사가 아니고, 인증도 실패 시
if(!user.IsFamily()&& !option.Validator.Validate(user)) if (user.IsFamily())
throw new InvalidUserException("Failed to authorize user"); option?.FamilyValidator?.Validate(user);
else
option?.ExtraUserValidator?.Validate(user);
CurrentUser = user; CurrentUser = user;
CurrentUser.Save(); CurrentUser.Save();

View File

@@ -38,9 +38,16 @@ namespace BaronSoftware.SSO
/// </summary> /// </summary>
public bool EnableAutoLogin { get; set; } = true; public bool EnableAutoLogin { get; set; } = true;
private IUserValidator familyValidator;
public IUserValidator FamilyValidator
{
get => familyValidator ?? new DefaultFamilyUserValidator();
set => familyValidator = value;
}
/// <summary> /// <summary>
/// 사용자 인증에 대한 추가 검증이 필요한 경우, IUserValidator 인터페이스를 구현하여 Validator 속성에 할당할 수 있습니다. /// 사용자 인증에 대한 추가 검증이 필요한 경우, IUserValidator 인터페이스를 구현하여 Validator 속성에 할당할 수 있습니다.
/// </summary> /// </summary>
public required IUserValidator? Validator { get; set; } public required IUserValidator? ExtraUserValidator { get; set; }
} }
} }

View File

@@ -4,10 +4,13 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace BaronSoftware.SSO.Exceptions namespace BaronSoftware.SSO
{ {
internal class InvalidUserException : Exception public class InvalidUserException : Exception
{ {
public UserInfo userinfo { get; }
public InvalidUserException(UserInfo userinfo) { }
public InvalidUserException() { } public InvalidUserException() { }
public InvalidUserException(string message) : base(message) { } public InvalidUserException(string message) : base(message) { }
public InvalidUserException(string message, Exception inner) : base(message, inner) { } public InvalidUserException(string message, Exception inner) : base(message, inner) { }

View File

@@ -0,0 +1,13 @@
namespace BaronSoftware.SSO
{
internal class DefaultFamilyUserValidator : IUserValidator
{
public void Validate(UserInfo user)
{
if (user.IsCenter() || user.IsFamily())
return;
throw new InvalidUserException(user);
}
}
}

View File

@@ -5,9 +5,10 @@ namespace BaronSoftware.SSO
public interface IUserValidator public interface IUserValidator
{ {
/// <summary> /// <summary>
/// 인증 실패 시 예외를 던지세요 /// 사용자 인증 처리기.
/// 인증 실패 시, 반드시 예외를 던져서 처리하세요
/// </summary> /// </summary>
/// <param name="user"></param> /// <param name="user"></param>
public bool Validate(UserInfo user); public void Validate(UserInfo user);
} }
} }