MagicOnion + Unity + Firebase Auth でユーザー認証できるようにしたい
この辺を参考にする
UnityのプロジェクトにYetAnotherHttpHandlerのインストールする
1.NugetForUnityのインストール
- openupm add com.github-glitchenzo.nugetforunity
- System.IO.PipelinesとSystem.Runtime.CompilerServices.Unsafe、Grpc.Net.Clientをインストール
- Unity Package ManagerからYetAnotherHttpHandlerwをgit URLからインストール
MessagePackのインストール
- openupm add com.neuecc.messagepack
openUPM経由でのインストールは公式にサポートされていない気がするけど、とりあえず試した時点で最新バージョンと一致していたのでこれでインストールする
Server側のセットアップ
今回は同じリポジトリで作っていく
dotnet new console
dotnet add package MagicOnion.Server
共有プロジェクトの作成
参考
dotnet new classlib
dotnet add package MagicOnion.Abstractions
dotnet add package MessagePack.UnityShims
dotnet add package MessagePack.Annotations
package.jsonの作成
{
"name": "com.sample.shared",
"version": "0.0.1",
"displayName": "magiconion-sample-server shared"
}
server側のcsproj更新
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Server</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.61.0" />
<PackageReference Include="MagicOnion.Server" Version="6.0.0" />
<ProjectReference Include="..\Shared\Shared.csproj"/>
</ItemGroup>
</Project>
SharedにInterfaceを定義する
using MagicOnion;
namespace Shared
{
public interface IMyFirstService : IService<IMyFirstService>
{
UnaryResult<int> SumAsync(int x, int y);
}
}
ServerにInterfaceの実装をする
using Shared;
using MagicOnion;
using MagicOnion.Server;
public class MyFirstService : ServiceBase<IMyFirstService>, IMyFirstService
{
public async UnaryResult<int> SumAsync(int x, int y)
{
Console.WriteLine($"Called SumAsync - x:{x} y:{y}");
return x + y;
}
}
UnityにClientの実装をする
using UnityEngine;
using MagicOnion.Client;
using Shared;
using MagicOnion;
public class SampleSum : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
CallTest();
}
public async void CallTest()
{
Debug.Log("start");
string host = "localhost";
int port = 5001;
var _channel = GrpcChannelx.ForAddress(host + ":" + port);
var serviceClient = MagicOnionClient.Create<IMyFirstService>(_channel);
var result = await serviceClient.SumAsync(100, 200);
Debug.Log(result);
}
}
Repositry
├─server
├─client
└─shared
こんな感じのフォルダ構成でやってるけど、うまくいかない。
shared/interfacesに作ったIMyFirstServiceの定義が競合する。
サーバーを起動した時にできる、shared配下のbin/Debug/netstandard2.1配下のdllとobj/Debug配下のdllと競合してるっぽい?
これの解決策があった。
Directory.Build.propsファイルをsharedに作って以下の内容を記載
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!-- Unity ignores . prefix folder -->
<ArtifactsPath>$(MSBuildThisFileDirectory).artifacts</ArtifactsPath>
</PropertyGroup>
</Project>
Repositry
├─server
└─client
└─Assets
└─App
└─Shared
にフォルダ構成を変更する。Sharedフォルダ自体はAssets配下であればどこでも良いけれど、今回はここに置いておく。
この構成だとサーバーからUnityの型(Vector3とか)が使えない。
参考元だとshared.csprojにMessagePack.UnityShimsを追加してるから扱えてるのかな。この構成だとshared.csprojはUnityが勝手に生成してて弄れないから良くなさそう。
HttpRequestException: request has unsupported HTTP version
というエラーが出てくる。
YetAnotherHttpHandlerの設定の問題?
IL2CPP向けのコード生成をしたら解決した。
BackendはMonoの設定のままにしてたけど、コード生成が必要っぽい(?)
MessagePackのコード生成手順
UnityだとEditor拡張として用意されているけど、Mac環境だと何故か動かないのでコードで実行する
dotnet new tool-manifest
dotnet tool install MessagePack.Generator
dotnet mpc -i "../Shared.csproj" -o "Assets/Scripts/Generated/MessagePackGenerated.cs" -m
こんな感じで動くはず。注意点としては生成したコードは当然Assets配下にないと読み込まれない。
Firebaseのアカウント、プロジェクトを作る。
この辺を参考に設定を進める。
SDKは以下からtgzをUPMで入れるようにする。
com.google.external-dependency-manager
com.google.firebase.app
com.google.firebase.auth
この三つを入れればとりあえずは良いはず。
今回はとりあえず匿名認証をしたいので以下のページを参考にする
Firebase.Auth.FirebaseAuth auth = Firebase.Auth.FirebaseAuth.DefaultInstance;
auth.SignInAnonymouslyAsync().ContinueWith(task =>
{
if (task.IsCanceled)
{
Debug.LogError("SignInAnonymouslyAsync was canceled.");
return;
}
if (task.IsFaulted)
{
Debug.LogError("SignInAnonymouslyAsync encountered an error: " + task.Exception);
return;
}
Firebase.Auth.AuthResult result = task.Result;
Debug.LogFormat("User signed in successfully: {0} ({1})",
result.User.DisplayName, result.User.UserId);
});
こんな感じで匿名認証はできる。けど、これ起動するたびに別ユーザーになってる。どうしたら良いんだ
Firebase.Auth.FirebaseAuth auth = Firebase.Auth.FirebaseAuth.DefaultInstance;
Firebase.Auth.FirebaseUser user = auth.CurrentUser;
if (user != null)
{
string name = user.DisplayName;
string email = user.Email;
System.Uri photo_url = user.PhotoUrl;
// The user's Id, unique to the Firebase project.
// Do NOT use this value to authenticate with your backend server, if you
// have one; use User.TokenAsync() instead.
string uid = user.UserId;
Debug.Log(uid);
}
else
{
auth.SignInAnonymouslyAsync().ContinueWith(task =>
{
if (task.IsCanceled)
{
Debug.LogError("SignInAnonymouslyAsync was canceled.");
return;
}
if (task.IsFaulted)
{
Debug.LogError("SignInAnonymouslyAsync encountered an error: " + task.Exception);
return;
}
Firebase.Auth.AuthResult result = task.Result;
Debug.LogFormat("User signed in successfully: {0} ({1})",
result.User.DisplayName, result.User.UserId);
});
}
こんな感じで行けた。
なんか知らないけど、Firebase.Auth.FirebaseAuth.DefaultInstance.CurrentUserで持ってるみたい。
string idToken = await auth.CurrentUser.TokenAsync(true);
これでトークンを取得できる
Server側でのトークン認証
この辺を参考に進める
dotnet add package FirebaseAdmin --version 2.4.0
Firebaseの秘密鍵を生成してDLする。
それをサーバーからアクセスできるように階層をしてする必要がある。
環境変数にパスを設定するのとプログラム内に直接パスを書くかの選択肢がある。
公式は環境変数に書くのをお勧めしてるけど、今回はあまり環境を汚したくないので直接パスを書く(本当はdocker内にサーバーを建ててそこに環境を封じ込めたい)。
Program.csのMainの一番最初にこんな感じで追加した。
FirebaseApp.Create(new AppOptions()
{
Credential = GoogleCredential.FromFile("spath/to/serviceAccountKey.json"),
});
最後に動作確認をする。
interfaceをこんな感じで追加して
public interface IUserHelper : IService<IUserHelper>
{
UnaryResult<string> verifyIdToken(string idToken);
}
サーバーにこんな感じの実装をする。
public class UserHelper : ServiceBase<IUserHelper>, IUserHelper
{
public async UnaryResult<string> verifyIdToken(string idToken)
{
FirebaseToken decodedToken = await FirebaseAuth.DefaultInstance
.VerifyIdTokenAsync(idToken);
string uid = decodedToken.Uid;
return uid;
}
}
最後にクライアントから呼び出して終わり。
Firebase.Auth.FirebaseAuth auth = Firebase.Auth.FirebaseAuth.DefaultInstance;
Firebase.Auth.FirebaseUser user = auth.CurrentUser;
if (user != null)
{
string name = user.DisplayName;
string email = user.Email;
System.Uri photo_url = user.PhotoUrl;
// The user's Id, unique to the Firebase project.
// Do NOT use this value to authenticate with your backend server, if you
// have one; use User.TokenAsync() instead.
string uid = user.UserId;
Debug.Log(uid);
}
else
{
await auth.SignInAnonymouslyAsync().ContinueWith(task =>
{
if (task.IsCanceled)
{
Debug.LogError("SignInAnonymouslyAsync was canceled.");
return;
}
if (task.IsFaulted)
{
Debug.LogError("SignInAnonymouslyAsync encountered an error: " + task.Exception);
return;
}
Firebase.Auth.AuthResult result = task.Result;
Debug.LogFormat("User signed in successfully: {0} ({1})",
result.User.DisplayName, result.User.UserId);
});
}
var idToken = await auth.CurrentUser.TokenAsync(true);
var channel = GrpcChannelx.ForAddress("http://127.0.0.1:5001/");
var idClient = MagicOnionClient.Create<IUserHelper>(channel);
var result = await idClient.verifyIdToken(idToken);
Debug.Log($"result {result}");
Debug.Logからそれっぽいidが確認できればOK
以上でサーバー側でuidが取得できるのであとは煮るなり焼くなりできるはず