에픽 계정 서비스(EAS)를 통한 플레이어 인증

Rajen Kishna, 테크니컬 어카운트 매니저, 에픽게임즈 |
2021년 10월 5일
Visual Studio 2019에서 솔루션 구조 설정을 마쳤으므로 이제 에픽 온라인 서비스 호출 구현을 시작할 수 있습니다. 에픽 계정 서비스(EAS)를 통한 인증부터 시작하겠습니다. 이 연재글에서 다루는 내용은 다음과 같습니다.  

인증 인터페이스와 연결 인터페이스의 차이

흔히들 혼돈스러워하는 것이 언제 인증 인터페이스(Auth Interface)를 사용해야 하고 언제 연결 인터페이스(Connect Interface)를 사용해야 하느냐는 것인데, 이는 둘 다 비슷한 패턴을 따르는 로그인 함수를 제공하기 때문입니다. 하지만, 둘 다 매우 구체적인 목적으로 사용됩니다.

인증 인터페이스
  • 인증 인터페이스는 에픽 계정에 대한 인증을 처리하므로 에픽 계정 서비스를 설정해야 합니다.
  • 인증 인터페이스를 통한 인증은 EOS에서 친구 및 현재상태, 전자 상거래 기능에 대한 액세스를 제공합니다.
  • 인증 인터페이스는 에픽 계정마다 고유한 에픽 계정 ID를 사용합니다.
연결 인터페이스
  • 연결 인터페이스는 에픽 게임 서비스에 대한 인증을 처리합니다. ID 제공자에 구애받지 않으므로 에픽게임즈, Steam, Xbox Live 등, 여러 ID 제공자와 함께 사용할 수 있습니다.
  • 연결 인터페이스는 에픽 계정에 의존하지 않는 대신 조직의 특정 제품마다 고유한 제품 사용자 ID(PUID)를 사용합니다.

이 두 개의 인터페이스를 가장 쉽게 구분하자면, 인증 인터페이스는 에픽 계정 및 관련 소셜 그래프 API를 처리하고, 연결 인터페이스는 게임을 위해 생성되며 외부 ID와 연결되어야 하는 고유한 사용자 ID를 처리합니다. 연결 인터페이스에서 사용하는 ID는 소셜 그래프가 아니므로 여러 ID에 연결된 크로스플레이와 크로스프로그레션에 사용될 수 있으며, 인증 인터페이스를 전혀 사용하지 않고도 사용할 수 있습니다.

마지막으로, 사용자가 (아직) 존재하지 않는 경우에 이러한 인터페이스 중 하나를 사용해야 할 수 있는 시나리오도 있습니다. 예를 들어, 서버에서 웹 API를 통해 인증 인터페이스와 연결 인터페이스를 모두 사용하여 제품 소유권을 검증하거나 음성 채팅방을 생성하고 관리할 수 있습니다. 또 다른 시나리오로는, 플레이어가 EOS 사용 전에 계정에 로그인하도록 강제하지 않으려는 경우가 있습니다. 즉, 연결 인터페이스의 디바이스 ID API를 사용하여 플레이어가 사용할 수 있는 영구 가계정을 생성하여 바로 게임을 플레이할 수 있게 하는 경우입니다. 이 디바이스 ID 시나리오는 추후 연재글에서 자세히 살펴보겠습니다.

개발자 포털에서 EAS 설정하기

인증 인터페이스를 사용하여 사용자를 인증하고 사용자의 현재상태 정보를 가져오거나 설정하고 사용자의 친구를 표시하려면, 먼저 개발자 포털에서 애플리케이션을 구성하여 EAS를 설정해야 합니다. 애플리케이션 환경설정은 세 부분으로 나뉩니다. 바로 브랜드 세팅(Brand Settings)과 권한(Permissions), 연동된 클라이언트(Linked Clients)입니다. 브랜드 세팅은 에픽게임즈 스토어에 제품을 퍼블리싱하는 데에만 필요하므로, 지금은 권한과 연동된 클라이언트만 완료하면 됩니다.
 
  1. https://dev.epicgames.com/portal/에서 개발자 포털에 로그인합니다.
  2. 왼쪽 메뉴에서 '내 제품'으로 이동하여 '에픽 계정 서비스(Epic Account Services)'를 클릭합니다. 약관을 검토하고 동의하면 수락합니다.
  3. 에픽 계정 서비스 아래에 이미 생성된 제품에 대한 애플리케이션 자리표시자가 있습니다. '구성(Configure)' 버튼을 클릭하여 설정합니다.
  4. 샘플은 에픽게임즈 스토어에 퍼블리싱하지 않으므로, 브랜드 세팅(Brand Settings) 탭은 건너뛰고 우측 상단의 '권한(Permissions)' 탭을 클릭하면 됩니다.
  5. 애플리케이션에서 사용자에게 요청할 수 있는 권한을 이곳에서 설정합니다. 일단, 다른 권한은 추후 해당 기능을 구현할 때 활성화할 것이므로, '기본 프로필(Basic Profile)' 권한만 활성화된 상태로 두겠습니다. '저장(Save)'을 클릭하여 확인합니다.
  6. 클라이언트(Clients)' 탭에서 이 애플리케이션과 연결된 클라이언트를 선택할 수 있습니다. '클라이언트 선택(Select Clients)' 드롭다운 메뉴에서 전에 설정한 클라이언트를 확인하고 '저장(Save)'을 클릭하여 확인합니다.
  7. 마지막으로, 좌측 상단의 '뒤로(Back)' 버튼을 클릭하여 개발자 포털로 돌아갑니다.
Developer Portal Application Configured
애플리케이션 구성 완료

인증 로그인 구현하기

이전에 MVVM 아키텍처를 구축했으므로 인증 로그인과 로그아웃 함수 기능을 구현을 시작할 수 있습니다.  
  1. ViewModels' 폴더에서 'MainViewModel.cs'를 열고 다음 멤버를 추가합니다.

private string _accountId;
public string AccountId
{
    get { return _accountId; }
    set { SetProperty(ref _accountId, value); }
}

private string _displayName;
public string DisplayName
{
    get { return _displayName; }
    set { SetProperty(ref _displayName, value); }
}
 
  1. MainWindow.xaml'을 열고 인증 인터페이스에서 다시 가져올 AccountId와 DisplayName을 표시하는 간단한 UI를 생성한 다음, 로그인 및 로그아웃 버튼을 생성합니다.

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <Grid Grid.Row="0">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <StackPanel Grid.Column="1">
            <StackPanel Orientation="Horizontal">
                <Button Width="100" Height="23" Margin="2" Command="{Binding AuthLogin}" Content="Auth Login" />
                <Button x:Name="AuthLogoutButton" Width="100" Height="23" Margin="2" Command="{Binding AuthLogout}" Content="Auth Logout" />
            </StackPanel >
        </StackPanel >

        <StackPanel Grid.Column="0">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="AccountId:" Margin="2" />
                <TextBox Text="{Binding AccountId}" IsReadOnly="True" Margin="2" />
            </StackPanel >
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="DisplayName:" Margin="2" />
                <TextBox Text="{Binding DisplayName}" IsReadOnly="True" Margin="2" />
            </StackPanel >
        </StackPanel >
    </Grid>

    <TabControl Grid.Row="1" Margin="0,10,0,0">
    </TabControl>

    <Grid Grid.Row="2" Height="18">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <TextBlock Margin="2,0,0,2" Grid.Column="0" Text="{Binding StatusBarText}" />
        <ProgressBar Grid.Column="1" Height="18" Width="100" Visibility="{Binding StatusBarText, Converter={StaticResource StringToVisibilityConverter}}" IsIndeterminate="{Binding StatusBarText, Converter={StaticResource StringToBooleanConverter}}" />
    </Grid>
</Grid>
 
  • 인증 UI 외에도, 거의 모든 호출이 비동기식이므로 서비스를 호출하는 동안 진행률 표시줄과 일부 정보를 표시하여 네트워크 활동을 시각적으로 나타낼 수 있는 상태 표시줄을 하단에 추가했습니다. 진행률 표시줄은 변환 툴을 사용하여 비저빌리티를 제어합니다.
  • 이후 문서에서 TabControl을 사용하여 게임 서비스 함수 기능을 추가하겠지만, 지금 UI 모습은 다음과 같습니다.
 VS2019 MainWindow Auth UI
MainWindow 인증 UI
 
  1. 다음으로, 'MainWindow.xaml.cs'를 열고 'MainWindow()' 생성자에 다음 라인을 추가하여 XAML 바인딩이 의도한 대로 작동하도록 데이터 컨텍스트를 설정합니다.

DataContext = ViewModelLocator.Main;  
  1. 이제 애플리케이션 세팅을 편집하여 인증 함수 기능을 구현해야 합니다. 'ApplicationSettings.cs'를 열고 다음 멤버를 추가합니다.

public LoginCredentialType LoginCredentialType { get; private set; } = LoginCredentialType.AccountPortal;
public string Id { get; private set; } = "";
public string Token { get; private set; } = "";

public ExternalCredentialType ExternalCredentialType { get; private set; } = ExternalCredentialType.Epic;

public AuthScopeFlags ScopeFlags
{
    get
    {
        return AuthScopeFlags.BasicProfile;
    }
}
  • 이 샘플에서는 브라우저를 사용하여 빠르게 로그인할 수 있게 해 주는 AccountPortal LoginCredentialType을 사용하겠습니다. 다른 사용 사례에 사용할 수 있는 다른 유형도 있습니다.
  • 또한, 인증을 에픽 계정으로 제한하지만, 다른 ExternalCredentialType 값(예: Nintendo, Steam, Discord 등)도 지원됩니다.
  • 마지막으로, AuthScopeFlags를 정의하여 액세스할 사용자의 정보를 나타냅니다. 이는 개발자 포털에서 애플리케이션에 설정한 권한과 같아야 합니다.
 
  1. 명령줄 실행인자를 통해 전달되는 경우 이러한 새 멤버를 적절하게 처리할 수 있도록 'Initialize()' 메서드에 다음을 추가합니다.

LoginCredentialType = commandLineArgs.ContainsKey("-logincredentialtype") ? (LoginCredentialType)System.Enum.Parse(typeof(LoginCredentialType), commandLineArgs.GetValueOrDefault("-logincredentialtype")) : LoginCredentialType;
Id = commandLineArgs.ContainsKey("-id") ? commandLineArgs.GetValueOrDefault("-id") : Id;
Token = commandLineArgs.ContainsKey("-token") ? commandLineArgs.GetValueOrDefault("-token") : Token;
ExternalCredentialType = commandLineArgs.ContainsKey("-externalcredentialtype") ? (ExternalCredentialType)System.Enum.Parse(typeof(ExternalCredentialType), commandLineArgs.GetValueOrDefault("-externalcredentialtype")) : ExternalCredentialType;
 
  1. EOS 인터랙션 로직을 보관할 'Services'라는 폴더를 만들고 'AuthService.cs'라는 클래스를 추가합니다.

public static class AuthService
{
    public static void AuthLogin()
    {
        ViewModelLocator.Main.StatusBarText = "Getting auth interface...";

        var authInterface = App.Settings.PlatformInterface.GetAuthInterface();
        if (authInterface == null)
        {
            Debug.WriteLine("Failed to get auth interface");
            ViewModelLocator.Main.StatusBarText = string.Empty;
            return;
        }

        var loginOptions = new LoginOptions()
        {
            Credentials = new Credentials()
            {
                Type = App.Settings.LoginCredentialType,
                Id = App.Settings.Id,
                Token = App.Settings.Token,
                ExternalType = App.Settings.ExternalCredentialType
            },
            ScopeFlags = App.Settings.ScopeFlags
        };

        ViewModelLocator.Main.StatusBarText = "Requesting user login...";

        authInterface.Login(loginOptions, null, (LoginCallbackInfo loginCallbackInfo) =>
        {
            Debug.WriteLine($"Auth login {loginCallbackInfo.ResultCode}");

            if (loginCallbackInfo.ResultCode == Result.Success)
            {
                ViewModelLocator.Main.StatusBarText = "Auth login successful.";

                ViewModelLocator.Main.AccountId = loginCallbackInfo.LocalUserId.ToString();

                var userInfoInterface = App.Settings.PlatformInterface.GetUserInfoInterface();
                if (userInfoInterface ==  null)
                {
                    Debug.WriteLine("Failed to get user info interface");
                    return;
                }

                var queryUserInfoOptions = new QueryUserInfoOptions()
                {
                    LocalUserId = loginCallbackInfo.LocalUserId,
                    TargetUserId = loginCallbackInfo.LocalUserId
                };

                ViewModelLocator.Main.StatusBarText = "Getting user info...";

                userInfoInterface.QueryUserInfo(queryUserInfoOptions, null, (QueryUserInfoCallbackInfo queryUserInfoCallbackInfo) =>
                {
                    Debug.WriteLine($"QueryUserInfo {queryUserInfoCallbackInfo.ResultCode}");

                    if (queryUserInfoCallbackInfo.ResultCode == Result.Success)
                    {
                        ViewModelLocator.Main.StatusBarText = "User info retrieved.";

                        var copyUserInfoOptions = new CopyUserInfoOptions()
                        {
                            LocalUserId = queryUserInfoCallbackInfo.LocalUserId,
                            TargetUserId = queryUserInfoCallbackInfo.TargetUserId
                        };

                        var result = userInfoInterface.CopyUserInfo(copyUserInfoOptions, out var userInfoData);
                        Debug.WriteLine($"CopyUserInfo: {result}");

                        if (userInfoData != null)
                        {
                            ViewModelLocator.Main.DisplayName = userInfoData.DisplayName;
                        }

                        ViewModelLocator.Main.StatusBarText = string.Empty;
                        ViewModelLocator.RaiseAuthCanExecuteChanged();
                    }
                });
            }
            else if (Common.IsOperationComplete(loginCallbackInfo.ResultCode))
            {
                Debug.WriteLine("Login failed: " + loginCallbackInfo.ResultCode);
            }
        });
    }
}

인증 로그인 구현의 핵심이므로 자세히 살펴보겠습니다.
 
  • 함수 전체에서 ViewModelLocator.Main.StatusBarText를 사용하여 UI에 상태 표시줄 텍스트를 설정합니다. 그러면 텍스트가 비어 있지 않을 때 진행률 표시줄이 자동으로 표시됩니다.
  • PlatformInterface.GetAuthInterface()를 사용하여 함수 전체에서 사용할 수 있는 인증 인터페이스의 인스턴스를 가져옵니다.
  • EOS의 모든 인터페이스 함수는 'options' 클래스를 인스턴스화해야 하며, 이는 종종 콜백 메서드 이벤트 핸들러와 함께 서비스 호출에 전달됩니다. 예를 들어, ApplicationSettings.cs의 크리덴셜과 범위를 사용하여 LoginOptions()를 인스턴스화하고 authInterface.Login()에 전달합니다.
  • 람다 콜백 메서드를 authInterface.Login()에 전달하여 로그인 응답을 처리하며, 이는 타이머가 업데이트될 때 MainWindow.xaml.cs에서 App.Settings.PlatformInterface?.Tick() 호출을 통해 트리거됩니다.
  • 콜백에서 ResultCode를 확인하고 이 코드가 Result.Success이면 로그인 호출이 성공한 것입니다. 그렇지 않은 경우에는, 디버그 출력에 실패 메시지와 해당 ResultCode를 작성합니다.
  • 로그인이 성공하면, MainViewModel 인스턴스에 loginCallbackInfo.LocalUserId를 저장하므로, 이를 애플리케이션의 다른 서비스 호출에서 사용할 수 있습니다.
  • 마지막으로, PlatformInterface.GetUserInfoInterface()를 사용하여 UserInfo Interface의 인스턴스를 가져옵니다. 이 인스턴스는 userInfoInterface.QueryUserInfo()를 통해 DisplayName 같은 추가 사용자 정보를 가져오는 데 사용할 수 있습니다.

ViewModelLocator.RaiseAuthCanExecuteChanged();가 해석되지 않았는데요. 이는 다음에 다루겠습니다.
 
  1. 프로젝트 루트에 'Commands'라는 폴더를 생성하고 'AuthLoginCommand.cs'라는 클래스를 추가합니다. 이는 MainWindow.xaml에서 로그인 버튼으로 트리거되는 명령입니다.

public class AuthLoginCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return string.IsNullOrWhiteSpace(ViewModelLocator.Main.AccountId);
    }

    public override void Execute(object parameter)
    {
        AuthService.AuthLogin();
    }
}
 
  1. MainViewModel.cs'를 열고 다음 멤버를 추가합니다.

public AuthLoginCommand AuthLogin { get; set; }
 
  1. 또한, 다음 라인을 추가하여 'MainViewModel()' 생성자에서 명령을 인스턴스화합니다.

AuthLogin = new AuthLoginCommand();
 
  1. 이제 사용자가 로그인하지 않은 경우에만 로그인 버튼이 활성화되도록 'ViewModelLocator.cs'를 열고 'RaiseAuthCanExecuteChanged()' 함수를 구현합니다.

public static void RaiseAuthCanExecuteChanged()
{
    Main.AuthLogin.RaiseCanExecuteChanged();
}

이제 애플리케이션을 실행하고 로그인 버튼을 클릭하면 에픽 계정을 통해 인증하는 브라우저 창이 트리거됩니다. 저희 애플리케이션은 개발 단계에 있으므로, 개발자 포털에서 해당 조직에 속한 에픽 계정으로만 로그인할 수 있습니다. 로그인하면, 브랜드 리뷰(Brand Review)를 거치지 않았기 때문에 검증되지 않은 애플리케이션(Unverified Application)이라는 공지가 표시됩니다. 계속하는 경우, 제품이 액세스를 요청하는 범위(이 경우, 기본 프로필 정보)를 보여주는 사용자 동의 대화창이 표시됩니다. 허용(Allow)을 클릭하면 브라우저가 닫히고, DisplayName을 검색하여 앱 UI에서 AccountId와 함께 표시하는 콜백 메서드가 트리거됩니다.  
Auth Unverified Application Auth User Consent
검증되지 않은 애플리케이션 경고 및 사용자 동의 대화창

인증 로그아웃 구현하기

이제 구조가 갖춰졌으니, 로그아웃은 좀 더 빠르게 구현할 수 있습니다.
 
  1. AuthService.cs'를 열고 'Logout()' 메서드를 추가합니다.

public static void AuthLogout()
{
    var logoutOptions = new LogoutOptions()
    {
        LocalUserId = EpicAccountId.FromString(ViewModelLocator.Main.AccountId)
    };

    App.Settings.PlatformInterface.GetAuthInterface().Logout(logoutOptions, null, (LogoutCallbackInfo logoutCallbackInfo) =>
    {
        Debug.WriteLine($"Logout {logoutCallbackInfo.ResultCode}");

        if (logoutCallbackInfo.ResultCode == Result.Success)
        {
            ViewModelLocator.Main.StatusBarText = "Logout successful.";

            var deletePersistentAuthOptions = new DeletePersistentAuthOptions();
            App.Settings.PlatformInterface.GetAuthInterface().DeletePersistentAuth(deletePersistentAuthOptions, null, (DeletePersistentAuthCallbackInfo deletePersistentAuthCallbackInfo) =>
            {
                Debug.WriteLine($"DeletePersistentAuth {logoutCallbackInfo.ResultCode}");

                if (logoutCallbackInfo.ResultCode == Result.Success)
                {
                    ViewModelLocator.Main.StatusBarText = "Persistent auth deleted.";

                    ViewModelLocator.Main.AccountId = string.Empty;
                    ViewModelLocator.Main.DisplayName = string.Empty;

                    ViewModelLocator.Main.StatusBarText = string.Empty;
                    ViewModelLocator.RaiseAuthCanExecuteChanged();
                }
            });
        }
        else if (Common.IsOperationComplete(logoutCallbackInfo.ResultCode))
        {
            Debug.WriteLine("Logout failed: " + logoutCallbackInfo.ResultCode);
        }
    });
}
 
  1. AuthLogoutCommand.cs'라는 새 클래스를 'Commands' 폴더에 추가합니다.

public class AuthLogoutCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return !string.IsNullOrWhiteSpace(ViewModelLocator.Main.AccountId);
    }

    public override void Execute(object parameter)
    {
        AuthService.AuthLogout();
    }
}
 
  1. 새 명령의 'MainViewModel.cs'에 멤버와 인스턴스화를 추가합니다.

public AuthLoginCommand AuthLogin { get; set; }
public AuthLogoutCommand AuthLogout { get; set; }

public MainViewModel()
{
    AuthLogin = new AuthLoginCommand();
    AuthLogout = new AuthLogoutCommand();
}
 
  1. 마지막으로, 'ViewModelLocator.cs'의 'RaiseAuthCanExecuteChanged()' 메서드에 다음 라인을 추가합니다.

Main.AuthLogout.RaiseCanExecuteChanged();

F5 키를 눌러 애플리케이션을 실행하면 로그인 플로를 완료할 때까지 인증 로그아웃 버튼이 비활성화되어 있는 것을 확인할 수 있으며, 로그인 이후에는 이 버튼을 사용해 로그아웃할 수 있습니다.

코드 다운로드

아래에서 이 연재글에 쓰인 코드를 확인할 수 있습니다. Visual Studio 2019에서 EOS를 위한 C# 솔루션 설정하기 문서에 있는 C# 솔루션 설정하기 섹션의 5단계와 10단계에 따라 SDK를 솔루션에 추가하고, SDK 크리덴셜을 포함하도록 ApplicationSettings.cs를 편집해야 합니다.
 
다음 연재글에서는 사용자 현재상태 정보를 가져오고 설정하는 방법에 대해 살펴보겠습니다. 언제든 시리즈 참조에서 이 시리즈의 모든 연재글 목록을 확인할 수 있습니다.

    여러분의 성공이 곧 에픽의 성공입니다

    에픽은 개방되고 통합된 게이밍 커뮤니티를 지향합니다. 에픽 온라인 서비스를 누구에게나 무료로 제공함으로써 더 많은 개발자들이 플레이어 커뮤니티에 더 나은 서비스를 제공할 수 있도록 지원하고자 합니다.