과거에 잠깐 회사에서 개발할 때 Tstory에 작성한 글이다. BLEX 블로그를 겸하면서 옮겨서 다시 정리하고자한다. 참고로 해당 글은 해당 깃허브 소스를 참고해 현재 버전에 맞게 수정한 것에 불과하다.
Blog
https://m.blog.naver.com/PostView.nhn?blogId=goldrushing&logNo=221482308506&proxyReferer=https:%2F%2Fwww.google.com%2F
Github
https://github.com/imagef5/Xamarin.SNS.Login.Sample
Xamarin.From
MS 측 .NET Framework를 이용해 크로스 플랫폼 앱을 개발하는 오픈소스 플랫폼이다.
각 플랫폼의 Native UI를 C#을 통해 제어하는 형태로 거의 네이티브에 유사한 속도를 자랑한다.
당시 주 언어가 C#이였기에 크로스플랫폼 개발 중 도입해 진행했었다.
최고의 장점은 MS에서 관리하기에 VIsual Studio를 이용해 개발할 수 있다는게 장점이고 C#의 간결한 문법도 장점에 속한다. 하지만 사용자 층이 적고 한국어로 된 자료는 없다시피하다. 그래서인지 SNS에 관련된 오픈소스 특히 Kakao Auth 로그인은 관련 정보가 하나 뿐이였고 그마저도 4년은 지난 내용이였다.
이번에 진행하게 된 기회삼아 Xamarin의 SNS 인증 관련 코드를 최신화 해보고자했다.
부족한 실력이라 많은 설명은 없으나 도움이 되길 기원한다.
❗ 참고로 IOS쪽에서는 추가 설정이 필요하다. IOS쪽은 필자가 맞은 파트가 아니라서 정확히 모르겠다.
Nuget Pakagae Version Info
C# Nuget
- Com.Airbnb.Xamarin.Forms.Lottie 4.0.8
- NETStandardLibary 2.0.3
- Newtonsoft.Json 12.0.3
- Prism.Unity.Forms 8.0.0.1909
- Xam.Plugins.Settings 3.1.1
- Xamarin.Auth 1.7.0
- Xamarin.Auth.XamarinnForms 1.7.0
- Xamarin.Essentials 1.6.1
- Xamarin.Forms 5.0.0.1931
Android Nuget
- Com.Airbnb.Android.Lottie 3.5.0
- Android API Level 29
Folder Tree
C# Nuget
- Com.Airbnb.Xamarin.Forms.Lottie 4.0.8
- NETStandardLibary 2.0.3
- Newtonsoft.Json 12.0.3
- Prism.Unity.Forms 8.0.0.1909
- Xam.Plugins.Settings 3.1.1
- Xamarin.Auth 1.7.0
- Xamarin.Auth.XamarinnForms 1.7.0
- Xamarin.Essentials 1.6.1
- Xamarin.Forms 5.0.0.1931
Android Nuget
- Com.Airbnb.Android.Lottie 3.5.0
- Android API Level 29
더 많은 폴더가 있지만 수정이나 추가할 폴더만 표시해보았다.
├── C# NameSpace (사용자가 정하는 것)
│ ├── Extensions
│ ├── ISettingExtensions.cs
│ ├── Models
└── Services/Authentication
폴더 별 소스코드
폴더 별로 소스코드를 작성한 것을 공유합니다.
Extensions Folder
Edtension 관련 파일이 들어가는 폴더입니다. 아래처럼 같이 솔루션 하위 폴더로 C# 네임스페이스에 만들고 정의합시다.
ISettingExtensions.cs
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Plugin.Settings.Abstractions;
namespace [YourProjectName].Extensions
{
public static class ISettingsExtensions
{
public static T GetValueOrDefault<T>(this ISettings settings, string key, T @default) where T : class
{
string serialized = settings.GetValueOrDefault(key, string.Empty);
T result = @default;
try
{
result = JsonConvert.DeserializeObject<T>(serialized);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error deserializing settings value: {ex}");
}
return result;
}
public static bool AddOrUpdateValue<T>(this ISettings settings, string key, T obj) where T : class
{
try
{
JsonSerializerSettings serializeSettings = GetSerializerSettings();
string serialized = JsonConvert.SerializeObject(obj, serializeSettings);
return settings.AddOrUpdateValue(key, serialized);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error serializing settings value: {ex}");
}
return false;
}
private static JsonSerializerSettings GetSerializerSettings()
{
return new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
}
}
}
Models Folder
Models 폴더 입니다.
아래처럼 같이 솔루션 하위 폴더로 C# 네임스페이스에 만들고 정의합시다.
🔥 참고 블로그에는 Kakao 외에도 구현되어 있으나 개발 당시 카카오 외에는 필요치않아서 제거했다.
User.cs
using System;
namespace [YourProjectName].Models
{
public class User
{
public string Id { get; set; }
public string Token { get; set; }
public string RefreshToken { get; set; }
public DateTime ExpiresIn { get; set; }
public string Name { get; set; }
public string NickName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Gender { get; set; }
public string Birthday { get; set; }
public string PictureUrl { get; set; }
public bool LoggedInWithSNSAccount { get; set; }
public SNSProvider Provider { get; set; }
}
}
User 클래스의 경우 유저 인증에 필요한 모델들을 정의하는데,
SNSProvider의 경우 카카오 인증만 구현할 땐 필요없는 모델이지만, 혹시나 다른 인증들을 구현할 기회가
생길까봐 유지했습니다.
SNSProvider
using System;
using System.Collections.Generic;
using System.Text;
namespace [YourProjectName].Models
{
public enum SNSProvider
{
None = -1,
Kakao,
//카카오 외의 다른 sns로그인 이용시 추가
//Line,
//Facebook,
//Naver,
}
}
OAuth2ProviderFactory
namespace [YourProjectName].Models.Providers
{
public class OAuth2ProviderFactory
{
public static OAuth2Base CreateProvider(SNSProvider provider)
{
//OAuth2Base oAuth2 = null;
OAuth2Base oAuth2 = KakaoOAuth2.Instance; ;
/*
switch (provider)
{
case SNSProvider.Kakao:
oAuth2 = KakaoOAuth2.Instance;
break;
case SNSProvider.Line:
oAuth2 = LineOAuth2.Instance;
break;
case SNSProvider.Facebook:
oAuth2 = FacebookOAuth2.Instance;
break;
case SNSProvider.Naver:
oAuth2 = NaverOAuth2.Instance;
break;
}
*/
return oAuth2;
}
}
}
OAuth2ProviderFactory는 OAuth2 인증을 진행할 Provider의 인스턴스를 생성해주는 코드였으나, KaKao외에는 사용하지 않아서 주석처리했다. 혹시 필요하시면 주석을 제거하면 사용하는게 좋을듯 싶다.
OAuth2Base.cs
using System;
using System.Threading.Tasks;
using Xamarin.Auth;
namespace [YourProjectName].Models.Providers
{
public abstract class OAuth2Base
{
public string ProviderName { get; set; }
public string Description { get; set; }
public SNSProvider Provider { get; protected set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string Scope { get; set; }
public Uri AuthorizationUri { get; set; }
public Uri RedirectUri { get; set; }
public Uri RequestTokenUri { get; set; }
public Uri UserInfoUri { get; set; }
public bool IsUsingNativeUI { get; set; } = false;
public abstract Task<User> GetUserInfoAsync(Account account);
public abstract Task<(bool IsRefresh, User User)> RefreshTokenAsync(User user);
}
}
OAuth2를 구현할 때 Auth 표준에서 필요한 필드들의 정의를 강제하기 위해 추상메서드로 구현했습니다.
KakaoUser.cs
using Newtonsoft.Json;
namespace [YourProjectName].Models.Providers
{
[JsonObject]
public class KakaoUser
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("kaccount_email")]
public string Email { get; set; }
[JsonProperty("kaccount_email_verified")]
public bool VerifiedEmail { get; set; }
public Properties Properties { get; set; }
}
[JsonObject]
public class Properties
{
[JsonProperty("nickname")]
public string NickName { get; set; }
[JsonProperty("thumbnail_image")]
public string Thumbnail { get; set; }
[JsonProperty("profile_image")]
public string ProfileImage { get; set; }
}
}
xamairn으로 SNS 인증을 하기위해서는 REST API로 구현해야됩니다.
따라서 Json 형태로 받는 값들을 처리하기 위한 코드입니다.
KakaoOAuth2.cs
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xamarin.Auth;
namespace [YourProjectName].Models.Providers
{
public class KakaoOAuth2 : OAuth2Base
{
private static readonly Lazy<KakaoOAuth2> lazy = new Lazy<KakaoOAuth2>(() => new KakaoOAuth2());
public static KakaoOAuth2 Instance
{
get
{
return lazy.Value;
}
}
private KakaoOAuth2()
{
Initialize();
}
void Initialize()
{
ProviderName = "Kakao";
Description = "Kakao Login Provider";
Provider = SNSProvider.Kakao;
ClientId = "{YourKakao REST API App Key}"; //REST API App Key 입력
ClientSecret = null;
Scope = null; //원하는 값 형태만 지정
AuthorizationUri = new Uri("https://kauth.kakao.com/oauth/authorize");
RequestTokenUri = new Uri("https://kauth.kakao.com/oauth/token");
RedirectUri = new Uri("https://www.naver.com/oauth");//oauth 경로가 있는 url (주로 자신의 블로그), 네이버는 임시로 설정했습니다.
UserInfoUri = new Uri("https://kapi.kakao.com/v2/user/me"); // V1과 V2는 다른 서버입니다.
}
#region Implement Abstract Method
public override async Task<User> GetUserInfoAsync(Account account)
{
User user = null;
string token = account.Properties["access_token"];
string refreshToke = account.Properties["refresh_token"];
int.TryParse(account.Properties["expires_in"], out int expriesIn);
var request = new OAuth2Request("GET", UserInfoUri, null, account);
var response = await request.GetResponseAsync();
if (response != null && response.StatusCode == HttpStatusCode.OK)
{
string userJson = await response.GetResponseTextAsync();
var kakaoUser = JsonConvert.DeserializeObject<KakaoUser>(userJson);
user = new User
{
Id = kakaoUser.Id,
Token = token,
RefreshToken = refreshToke,
Name = kakaoUser.Properties.NickName,
Email = kakaoUser.Email,
ExpiresIn = DateTime.UtcNow.Add(new TimeSpan(expriesIn)),
PictureUrl = kakaoUser.Properties.ProfileImage,
Provider = SNSProvider.Kakao,
LoggedInWithSNSAccount = true,
};
}
return user;
}
public override async Task<(bool IsRefresh, User User)> RefreshTokenAsync(User user)
{
bool refreshSuccess = false;
if (user == null)
{
return (refreshSuccess, user);
}
Dictionary<string, string> dictionary = new Dictionary<string, string> { { "grant_type", "refresh_token" }, { "refresh_token", user.RefreshToken }, { "client_id", ClientId } };
var request = new Request("POST", RequestTokenUri, dictionary, null);
var response = await request.GetResponseAsync();
if (response != null && response.StatusCode == HttpStatusCode.OK)
{
string tokenString = await response.GetResponseTextAsync();
JObject jwtDynamic = JsonConvert.DeserializeObject<JObject>(tokenString);
var accessToken = jwtDynamic.Value<string>("access_token");
var refreshToken = jwtDynamic.Value<string>("refresh_token");
var expiresIn = jwtDynamic.Value<int>("expires_in");
user.Token = accessToken;
user.RefreshToken = refreshToken;
user.ExpiresIn = DateTime.UtcNow.Add(new TimeSpan(0, 0, expiresIn));
refreshSuccess = true;
}
return (refreshSuccess, user);
}
#endregion
}
}
Initialize() 메서드에서 ClientId는 설정해주셔야됩니다.
카카오 Devleoer에 방문하셔서 REST API key를 받아 작성해주시면 됩니다.
Kakao Developers
https://developers.kakao.com/
Services/Authentication Folder
IAuthenticationService.cs
using System.Threading.Tasks;
using [YourProjectName].Models;
namespace [YourProjectName].Services.Authentication
{
public interface IAuthenticationService
{
bool IsAuthenticated { get; }
User AuthenticatedUser { get; }
Task<bool> LoginAsync(string email, string password);
Task LoginWithSNSAsync(SNSProvider provider);
Task<bool> UserIsAuthenticatedAndValidAsync();
Task LogoutAsync();
}
}
먼저 Auth interface를 정의해줍니다. __ Authentication.cs__
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Xamarin.Auth;
using Xamarin.Forms;
using [YourProjectName].Models;
using [YourProjectName].Models.Providers;
namespace [YourProjectName].Services.Authentication
{
public class AuthenticationService : IAuthenticationService
{
OAuth2Base oAuth2;
public bool IsAuthenticated => AppSettings.User != null;
public User AuthenticatedUser => AppSettings.User;
public Task<bool> LoginAsync(string email, string password)
{
var user = new User
{
Email = email,
Name = email,
LastName = string.Empty,
PictureUrl = "",
Token = email,
LoggedInWithSNSAccount = false,
Provider = SNSProvider.None
};
AppSettings.User = user;
return Task.FromResult(true);
}
public Task LoginWithSNSAsync(SNSProvider provider)
{
try
{
oAuth2 = OAuth2ProviderFactory.CreateProvider(provider);
var authenticator = new OAuth2Authenticator(
oAuth2.ClientId,
oAuth2.ClientSecret,
oAuth2.Scope,
oAuth2.AuthorizationUri,
oAuth2.RedirectUri,
oAuth2.RequestTokenUri,
null,
oAuth2.IsUsingNativeUI);
authenticator.Completed += async (s, e) =>
{
if (e.IsAuthenticated)
{
var user = await oAuth2.GetUserInfoAsync(e.Account);
AppSettings.User = user;
MessagingCenter.Send(user, MessengerKey.AuthenticationRequested, true);
Debug.WriteLine("Authentication Success");
}
};
authenticator.Error += (s, e) =>
{
Debug.WriteLine("Authentication error: " + e.Message);
};
var presenter = new Xamarin.Auth.Presenters.OAuthLoginPresenter();
presenter.Login(authenticator);
}
catch (Exception ex)
{
Debug.WriteLine("Login Error : " + ex.Message);
return Task.FromResult(false);
}
return Task.FromResult(true);
}
public async Task<bool> UserIsAuthenticatedAndValidAsync()
{
if (!IsAuthenticated)
{
return false;
}
else if (!AuthenticatedUser.LoggedInWithSNSAccount)
{
return true;
}
else
{
bool refreshSucceded = false;
oAuth2 = OAuth2ProviderFactory.CreateProvider(AuthenticatedUser.Provider);
try
{
var utcNow = DateTime.UtcNow.AddMinutes(30);
if (AuthenticatedUser.ExpiresIn < utcNow)
{
var ret = await oAuth2.RefreshTokenAsync(AuthenticatedUser);
if (ret.IsRefresh)
{
AppSettings.User = ret.User;
}
else
{
AppSettings.RemoveUserData();
}
refreshSucceded = ret.IsRefresh;
}
else
{
refreshSucceded = true;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error with refresh attempt: {ex}");
}
return refreshSucceded;
}
}
public Task LogoutAsync()
{
AppSettings.RemoveUserData();
return Task.FromResult(true);
}
}
}
실제로 Auth 요청이 이루어지는 코드입니다.
이전 코드 (참고에있는 이전 개발자분 깃헙 참고)에서 변화된 부분들은 다 작성했다. 이후 메인페이지 등을 정의하면된다. 필자의 경우 profile을 가져올 필요가 없는 프로젝트 이기에 원 개발자 분 코드에서 사용하는 FFImage를 사용하지 않았다.
etc
추가로 안드로이드 인터넷 사용권한을 부여해야된다.
방법은 안드로이드 네임스페이스에 마우스 오른쪽 - > 속성 -> Android 매니페스트 -> 필수권한 -> INTERNET 항목을 선택하면된다.
Android MainActivity.cs
Android 인터넷 사용권한
.
.
❗ 메인 Oncreate 메서드에서 아래 코드를 추가하지 않는다면 화면이 로딩 중에서 넘어가지 않는 오류가 발생한다. 유의하도록 하자.
global::Xamarin.Auth.Presenters.XamarinAndroid.AuthenticationConfiguration.Init(this,savedInstanceState)
BLEX를 이틀 째 사용중으로 당분간은 BLEX위주로 작성하면서 장단점을 잴 예정이다. 필자는 다크모드를 자주 사용하는데, 일반모드에서는 코드블록이 크게 거슬리지 않으나 다크모드의 경우 색감이 보기에 불편하지 않을까싶다.
Ghost