Closed23

MagicOnion + Unity + Firebase Auth でユーザー認証できるようにしたい

yukiyuki

UnityのプロジェクトにYetAnotherHttpHandlerのインストールする

1.NugetForUnityのインストール
https://github.com/GlitchEnzo/NuGetForUnity

  • openupm add com.github-glitchenzo.nugetforunity
  1. System.IO.PipelinesとSystem.Runtime.CompilerServices.Unsafe、Grpc.Net.Clientをインストール
  2. Unity Package ManagerからYetAnotherHttpHandlerwをgit URLからインストール
yukiyuki

MessagePackのインストール

  • openupm add com.neuecc.messagepack

openUPM経由でのインストールは公式にサポートされていない気がするけど、とりあえず試した時点で最新バージョンと一致していたのでこれでインストールする

yukiyuki

Server側のセットアップ
今回は同じリポジトリで作っていく

dotnet new console
dotnet add package MagicOnion.Server

yukiyuki

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>
yukiyuki

SharedにInterfaceを定義する

using MagicOnion;

namespace Shared
{
    public interface IMyFirstService : IService<IMyFirstService>
    {
        UnaryResult<int> SumAsync(int x, int y);
    }
}

yukiyuki

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;
    }
}

yukiyuki

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);
    }
}

yukiyuki

Repositry
├─server
├─client
└─shared

こんな感じのフォルダ構成でやってるけど、うまくいかない。
shared/interfacesに作ったIMyFirstServiceの定義が競合する。

サーバーを起動した時にできる、shared配下のbin/Debug/netstandard2.1配下のdllとobj/Debug配下のdllと競合してるっぽい?

yukiyuki

これの解決策があった。
https://neue.cc/2024/01/15_shareprojectinunity.html
ここを参考に
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>
yukiyuki

Repositry
├─server
└─client
   └─Assets
     └─App
       └─Shared

にフォルダ構成を変更する。Sharedフォルダ自体はAssets配下であればどこでも良いけれど、今回はここに置いておく。

yukiyuki

この構成だとサーバーからUnityの型(Vector3とか)が使えない。
参考元だとshared.csprojにMessagePack.UnityShimsを追加してるから扱えてるのかな。この構成だとshared.csprojはUnityが勝手に生成してて弄れないから良くなさそう。

yukiyuki

HttpRequestException: request has unsupported HTTP version
というエラーが出てくる。
YetAnotherHttpHandlerの設定の問題?

yukiyuki

IL2CPP向けのコード生成をしたら解決した。
BackendはMonoの設定のままにしてたけど、コード生成が必要っぽい(?)

yukiyuki

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配下にないと読み込まれない。

yukiyuki

Firebaseのアカウント、プロジェクトを作る。

https://firebase.google.com/docs/unity/setup?hl=ja#set_up_environment

この辺を参考に設定を進める。

SDKは以下からtgzをUPMで入れるようにする。
https://developers.google.com/unity/archive?hl=ja

com.google.external-dependency-manager
com.google.firebase.app
com.google.firebase.auth

この三つを入れればとりあえずは良いはず。

今回はとりあえず匿名認証をしたいので以下のページを参考にする
https://firebase.google.com/docs/auth/unity/anonymous-auth?hl=ja

        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);
        });

こんな感じで匿名認証はできる。けど、これ起動するたびに別ユーザーになってる。どうしたら良いんだ

yukiyuki
        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で持ってるみたい。

yukiyuki
string idToken = await auth.CurrentUser.TokenAsync(true);

これでトークンを取得できる

yukiyuki

Server側でのトークン認証

この辺を参考に進める
https://firebase.google.com/docs/admin/setup?hl=ja#c

dotnet add package FirebaseAdmin --version 2.4.0

Firebaseの秘密鍵を生成してDLする。
それをサーバーからアクセスできるように階層をしてする必要がある。
環境変数にパスを設定するのとプログラム内に直接パスを書くかの選択肢がある。
公式は環境変数に書くのをお勧めしてるけど、今回はあまり環境を汚したくないので直接パスを書く(本当はdocker内にサーバーを建ててそこに環境を封じ込めたい)。

Program.csのMainの一番最初にこんな感じで追加した。

Program.cs
        FirebaseApp.Create(new AppOptions()
        {
            Credential = GoogleCredential.FromFile("spath/to/serviceAccountKey.json"),
        });

yukiyuki

最後に動作確認をする。

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

yukiyuki

以上でサーバー側でuidが取得できるのであとは煮るなり焼くなりできるはず

このスクラップは2024/03/09にクローズされました