やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

クリスマスプレゼントの交換会にElixirで備える

今年で24歳になりましたが、いつになってもクリスマスってのはワクワクしますね。山下達郎さんのクリスマスイブを耳にすると「もう12月かぁ〜」と胸が苦しくなります。
そんなこんなで友人とクリスマスプレゼントの交換会をすることになりました(唐突)。
誰が誰にプレゼントを送るかという決定に忖度がないように、プレゼント交換のペアを自動で出力できるようなプログラムを組みました。

f:id:takamizawa46:20201114133430p:plain これは完全に『遊び』です。

【超絶技巧】CARAVAN/村下孝蔵 - ニコニコ動画

実装方針について

案1: ピポットとランダムな選択

どんなデータ構造を使えば効率よく、ペアを組めるのかを考えてみました。最初に素直に参加メンバーの名簿をリストで用意してみます。実際には3人でやるんですが、見栄えをよくするために6人ぐらいにしておきます。

member = ["tomoya", "nagisa", "kyou",  "ryou", "tomoyo", "kotomi"]

簡単に考えると、リストの先頭要素(ピポット)を取得して、プレゼントを送る側としてfixします。それに対応するプレゼントを受け取る側は先頭要素以外のメンバーの配列から一人を無作為に選ぶことで決定することが出来そうです。簡単にコードに落としてみます。

defmodule SecretSanta do
    def create_pair([_ | []]), do: :ok
    def create_pair([h | t]) do
        IO.puts("#{h} => #{Enum.random(t)}")
        create_pair(t)
    end
end

member = ["tomoya", "nagisa", "kyou",  "ryou", "tomoyo", "kotomi"]
SecretSanta.create_pair(member)

再帰処理を用いて、上記の処理を完成させてあります。ランダムで受け取るメンバーを選択しているので実行ごとに結果が変わりますが、一例として結果を以下に記します。

tomoya => nagisa
nagisa => ryou
kyou => tomoyo
ryou => kotomi
tomoyo => kotomi

一見、良い感じにペアが決まっているように見えますが問題が潜在しています。プレゼントを送る側は重複することはありませんが、プレゼントを受け取る側はtailから無作為に選択されるので、重複する可能性があります。特にtailの要素数が少なくなれば少なくなるほど、その確率は高まります。

回避策として、誰がすでにペアとなっているか(選択されているか)を記録してあげれば重複が出ない様になるかと思いますが、その管理をするのが面倒です。ですので、こちらの案は一旦、保留としておきます。

案2 : グラフ構造を用いる

色々調べていたところ、この問題は既出の有名な問題であり、「Secret-Santa-Problem」というものだそうです。論文やらアルゴリズムの解説がいくつかありました。参考文献に貼っておきます。

概要を説明すると巡回グラフ構造を用いて、StartからEndまでの道を求めることで、プレゼント交換のペアが決定されるとのことです。次のグラフ(地点)の選択は最も簡単な方法であればランダム選択をするようです。応用として、Aさんがお金持ちであった場合、Bさんが貧相であった場合に重みを付与することで、忖度を表現することが出来そうですね。

とはいえ、今回の実装としてはリッチすぎるので、一旦保留としておきましょう。

案3: メンバーをランダムにシャッフル

グラフ構造を使うという発想を得てから、「こうすればいいんじゃないかね」と思い付いた方法です。案1ではプレゼントを送る人が重複してしまうため、選択されたメンバーを記録しておく必要がありそうでした。しかしながら面倒です。
データの列挙がイコール、応募者のペアとなっているのが最も楽です。それがグラフ構造でA->B->CというのはA->B, B->C, C->Aというペアを表しています。これをリストで表現できたら楽だなぁと思いました。

ランダムで選択されるという処理が面倒なので、事前にメンバーの一覧のリストをシャッフルしておけば、再帰処理で取得されるピポットがランダムで選択された一人だと同等だと考えることが出来そうです。まとめると以下の手順で処理を行います。

  • メンバーを記録しているリストをシャッフルする
  • シャッフルした配列に対して再帰処理を実行。headtailを取得する
  • headをプレゼントを送る側、tailの先頭要素をプレゼントを受け取る側とする
  • tailが空(残りのメンバーがいない)の場合に、最初の一人をプレゼントを受け取る側とする

この方法が良さそうですので、採用してコードに落としてみます。

実装

先にコードを載せておきます。最近、ミニマムなコードはgistを使って管理しています。本当に便利。

gist.github.com

処理として面倒なのはtailが空(残りのメンバーがいない)の場合に、最初の一人をプレゼントを受け取る側とするという箇所でしょう。どうしたものかと悩みましたが、逆に考えると最初の一人だけを記録しておけば良いので、アキュムレーターとして最初の一人を記録させるようにしました。

defp print_result([h | _] = lst), do: _print_result(lst, h)
defp _print_result([h | []], first) do
  template_print(h, first)
  _print_result([], first)
end

引数パターンマッチングのおかげで、条件一致させるのも楽ですね。リストの中のリストでさえも簡単に条件を拾えます。処理は可能な限り関数として分散させています。同名関数が定義可能なのは命名に困らないので非常に助かります。

実行結果

最後に結果を出力しておきます。

ryou => tomoyo
tomoyo => kotomi
kotomi => nagisa
nagisa => kyou
kyou => tomoya
tomoya => ryou

グラフ構造を用いた案2のStart->Endの関係をリストでも表現することが出来ました。これでプレゼント交換のペア決めはばっちりですね。

おまけ

パフォーマンスの視点で考えるとEnum.shuffleがどれだけの計算量なのかが気になります。データのシャッフルとして有名なのはFisher-Yates shuffleというものがあるそうです。配列から要素をランダムに1つ選択して、それを末尾の要素と入れ替える処理を繰り返すことで配列をシャッフルします。計算量はO(n)で、非常にパフォーマンスが良いですね。

問題はElixirでこのFisher-Yates shuffleが実装されているのかということです。Elixirenum.exsfuffleの実装がありました。

@spec shuffle(t) :: list
def shuffle(enumerable) do
  randomized =
    reduce(enumerable, [], fn x, acc ->
      [{:rand.uniform(), x} | acc]
    end)

  shuffle_unwrap(:lists.keysort(1, randomized), [])
end

Elxiirでランダム値を先頭要素に持つタプルを作成して、erlang:lists.keysortという関数を用いて、ソートをしています。
mike-neck.github.io

なので乱数生成 + ソートを用いて配列をシャッフルしているということになります。これ以上、コードの追従はしませんが、ソートの計算量は多くてもセレクトソートのO(n^2)程度だと考えられるので、重荷になるような処理ではないかと思います。

参考文献

Elixirでモジュール内の関数を動的に呼び出す

パイセンの実装に感動

僕が感動したのはElixirでの実装ではありませんが、Rubyで特定のディレクトリ以下に_____.rbというファイルを作成し、命名規則に従ったモジュールとinterfaceを満たす関数を実装すると、自動でモジュール内の関数が読み込まれて動的に実行されるという設計に感動しました。
このような設計の方法はデザインパターンに通ずることがあるんじゃないかと思いましたが、僕自身、オブジェクト指向をやんちゃにしか取り組んだことしかないので詳細は不明。
とはいえ、この実装方法に感動したのでElixirを使って実装してみます。

Elixirでの実装方法

先に全体のコードを貼っておきます。

github.com

interface

Elixirinterfaceを指定することは出来ませんが、behaviorを使用することで指定の関数を満たすモジュールを定義させることが出来るようです。

elixir-lang.org

なるほど。たまに見かけてた@implってこんな感じで使うんですね。
とはいえ、behaviorを使うのはちょっとリッチだなと思ったので、今回は共通認識として「この関数はモジュール内に実装必須ね」と開発メンバーに事前に共有をしたという前提で、通常のモジュールを用いて実装を行います。

モジュールの読み込み

以下の順で実装をしてみようと思います。

  • 指定のディレクトリ階層に格納されている____.exの一覧を文字列のリストで取得
  • 指定のディレクトリ階層までのモジュール名を取得したListの要素、それぞれに結合

コードに落とすとこうなりました。思っていたよりも長くなってしまいました。staticな文字列にしたらもっと、短くなると思いますが自分のスタイルを貫きました。

github.com

def module_names do
    processes_path() # 指定のディレクトリのpathを文字列で取得
    |> File.ls! # ディレクトリ内のファイル一覧を取得
    |> Enum.filter(fn fname -> ex_extension_slice(fname) == ".ex" end) # .exファイル以外を除去
    |> Enum.map(fn fname -> merge_module_name(fname) end) # この階層までのモジュール名を付与(eg: Project.Sample <> FileModule)
end

これで指定モジュール内に格納されている.exファイルを読み込む準備が出来ました。あとは読み込んだモジュールへのpathを良い感じに使ってあげます。今回は、モジュール内に共通で実装されているstartという関数を実行させます(startは新たなプロセスを起動させるための関数)。

文字列から動的にモジュール内の関数を実行する方法は以下を参考にしました。ノリはspawnでモジュール名と関数のアトムを渡すのに似ていますね。

dynamic - Elixir - Call method on module by String-name - Stack Overflowstackoverflow.com

ハマったのは新規で自分が作成したモジュールを読み込む場合にはElixirをモジュール名に付与する必要があるということです。気付かなかった。

defp call_start_func(module_name, p_pid) do
    String.to_existing_atom("Elixir." <> module_name) # eg: Elixir. + Project.Sample.FileModule
    |> apply(:start, [p_pid])
end

モジュールに実装する動作について

これで指定ディレクトリ内のモジュールに実装されたstart関数を実行することが出来る様になりました。 今回の場合にはlib/processes/childrenに新たな.exファイルを追加して、モジュール内にstartという関数を実行して、新たなプロセスを生成して、特定のプロセスにmessage passingを行う(メッセージの送信)という実装にしてあります。

defmodule Processes.Children.Apple do
  def start(p_pid) do
    pid = spawn(fn -> run(p_pid) end)
    pid
  end

  defp run(p_pid) do
    # 特定のプロセスに「🍎」というメッセージを送信
    Processes.Job.run(p_pid, "🍎")
  end
end

モジュール名はProcesses.Children以降は何でも良いです。分かりやすさ重視のため、絵文字を送っています。メッセージの送信処理はProcesses.Jobに共通化してありますが、以下のように新たに任意で作成することも当然可能です。

# original job
defp run(p_pid) do
  IO.puts('hello world')
  run(p_pid)
end

動作確認

メッセージを受け取る側のプロセスにloggerを仕込んであるので、実際にプロセスが動的に起動して、メッセージを送って来ているのかを確認してみます。

iex(1)> Main.execute

ログが流れ始めました。立ち上がったプロセス情報(PID)を返すようにしています。

%{
  children: [#PID<0.141.0>, #PID<0.142.0>, #PID<0.143.0>, #PID<0.144.0>,
   #PID<0.145.0>],
  parent: #PID<0.140.0>
}
iex(2)> 
10:08:07.574 [info]  🐓⏰ => 2020/11/09 10:08:07 from #PID<0.145.0> as 🍍
 
10:08:08.698 [info]  (′・ω・`) Received message from #PID<0.142.0> as 🍑
 
10:08:08.836 [info]  (′・ω・`) Received message from #PID<0.141.0> as 🍊
 
10:08:09.789 [info]  (′・ω・`) Received message from #PID<0.144.0> as 🍌
 
10:08:09.906 [info]  (′・ω・`) Received message from #PID<0.143.0> as 🍎
 
10:08:10.041 [info]  🐓⏰ => 2020/11/09 10:08:10 from #PID<0.145.0> as 🍍
 
10:08:10.679 [info]  (′・ω・`) Received message from #PID<0.141.0> as 🍊
:
:

f:id:takamizawa46:20201109233442g:plain

絵文字とモジュール名を連動させるようにしてあります。このログをみる限り以下のモジュールからメッセージを受けていることが分かります。

  • pineapple(🍍)
  • apple(🍎)
  • peach(🍊)
  • banana(🍌)

pineappleからメッセージが送られて来た場合には現在時刻を表示するようにさせてあります。独自の処理を組み込むのも簡単だねってElixirの宣伝です。

参考文献

気づいたらプロダクト指向な開発者になっていた

仕事の内容が変わって考え方が変わった

数ヶ月程前から、既存の受託案件の開発チームから自社開発チームに移籍して、継続的な開発をしている。 受託案件をゴリゴリ作っていた時は、作って終わりの考えでとにかく速さを重視していた。gitを使ってコードの品質管理はしていたものの今、改めて考えてみれば変更に弱く、品質が担保されていかないコードを書いていたと思う。

(なぜ、そう思うのかについては後述)

最近の開発における考え方は既存のアーキテクチャを保守し、危険性の低いかつ、変更に強く、使い回しの良いコードを書くように意識している。「そんなの受託の時もそうでしょうが」と言われれば、意識としては可能だと思うが、やはり明確に納期があり、交渉不可能という現実があるのとないのとでは心理状態は異なってくる。

そうなると、長い目で見た時にここはどう実装しておくのが良いだろうかという考えを自然としていることに気づいた。

自分が考えているプロダクト思考について

記事のタイトルにも用いた「プロダクト指向」という言葉が自分の造語なのではないか?と思い、軽くググってみたところ、造語ではないよう。製品を顧客の声元に、より良いものにアップデートしていくというのがプロダクト指向だとある。

自分が考えてるプロダクト指向と非常に近い意味合いだったので良かった。もう少しだけ開発者視点でのプロダクト指向という意味を加えて、自分の言葉でプロダクト指向という言葉を説明すると以下のような感じになる。

プロダクト指向

製品(アプリケーション)の品質を高く保ち、改修に強く、長い期間の運用を可能にするための継続的な開発を行うこと

大勢のユーザーがいるという責任感

上記のような考え方に至った上で一番、大きな要因はやはり「現在進行形で使ってくれるユーザーが大勢いる」ということ。しょうもないバグを出せば、直ぐにクレームになり、サービスを解約されてしまうだろう。グローバルに公開されているアプリなので、すぐに口コミも広まってしまう。 加えて、他社競合などに負けぬように新たな機能を追加、既存機能の改修していく必要がある。運用していく中で、「こうしたい、あれもほしい、これはいらなかった」という事が分かってくる。

こういったリクエストにも答えつつ、1行の変更が大きなバグを生み出すかもしれないという責任感を乗り越えていく必要がある。そうなる自然と既存のアーキテクチャを保守し、危険性の低いかつ、変更に強く、使い回しの良いコードを書くようになる。

何よりも大切なのはユーザーに高品質な製品を継続的に提供することだ。

プロダクトコードを書いていることの価値

少し前に社内のベテランパイセンエンジニアに「大勢のユーザーがいるプロダクトコードを書いたことがあるやつと書いたことがないやつでは雲泥の差がある」という話を自分にしてくれたことがある。その時は受託開発をゴリゴリやっていた時で、その意味、価値がよく分からなかった。

しかし、今になって思うと確かになと思うことがある。先ほども記述したように、時に1行のコードが大きな問題を発生させることがある。そういった危機感のある視点を持つように訓練するには大勢のユーザーがいるプロダクトコードを書くというのが一番、手っ取り早い。

あ、この人はプロだなと感じる時

先日、エンジニアになりたかったが挫折して、技術営業になった方とお話する機会があった。その際にふと、「エンジニアが駆け出しから抜け出すのってどのぐらいのタイミングなんですか」という質問をされた。 少し前の自分なら、基本的な文法が書けて...データ構造に理解があって...アルゴリズムを知ってて...というような回答をしていたと思う(もちろん3つともどれも重要)。自然と口が動き「プロダクトコードを書き始めた時じゃないですかね」と答えた。 理由については上記ですでに記述してきたので、省略する。
その後に「ならOKBさんとベテランのAさんとの違いって何かあるんですか」と質問をされて「ぐぬぬ(心の声)」となったが、少し考えて直ぐに答えが出た。

「なんていうか、ベテランの人は嗅覚が鋭い。このコードが将来的にどういう危険性を孕んでいる、こういう場合にヤバイってのが直感的か経験的に分かってコードレビューの時に指摘されることが多い」

あとは単純に知識量も。

反省点

こんな感じでプロダクト指向になれたのはプロに一歩近づくことが出来たなと感じる一方で反省している点もある。それはトレンドな技術や興味関心のある技術に対するハングリーな気持ちがすり減ってしまったことでだ。少し前は東海で誰も使わないようなプログラミン言語Elixirの情報収集に熱を出したものだが、今はどうか。何ともつまらない開発者になってしまっている気もしている。 時間がなかったというのは言い訳にはならない、と以前の最強の同僚にも言われたので「なんでやらなくなったの」に対して弁明が出来ない。あんなに書いていたブログも月に1本でるかどうかだ。

三井寿が暴力沙汰を起こしてまでバスケットに復帰したようなハングリーなマインドを徐々に取り戻していきたいと思う。

Appleのカスタムサポートが神対応だった

相棒のMacBookから焦げた匂いが...

ある日のこと、突如、MacBookから「バチンッ!!」という音がして電源が落ちた。一瞬だが、火花も見えた。
あー、これはマザーボードがショートする系のアカンやつだなぁと故障を覚悟した。案の定、電源を長押しして起動を試みるも、やはり電源はつかない...。

ああああああああああああ...

人生でPCが壊れたのはこれで二度目だが、学生時代に預金をゼロにしてまで買った思い入れのあるPCだけに超ショック。
パッとみる感じだと外傷はないが、裏側にある排熱口から何か焦げたような匂いがする。これはヤバイ。

f:id:takamizawa46:20200819094228j:plain:w500

カメラのキタムラに持ち込むも

※カメラのキタムラはApple正規サービスプロバイダです  
(田舎にAppleStoreなんてものはない)  

合計で2回も店舗に行くことになった。1回目は21時に営業終了のところ、業務終了時間との兼ね合いで20時頃に店舗に到着。なにやら、ビラが設置してあり確認してみると

「本日の受付は終了しました」

とのこと。まぁ閉店1時間前に来た自分が悪いわなぁと思い、後日に改めることに。

今度は休日のお昼の時間帯を選んで店舗に向かった。「流石にこの時間なら受け付けてくれるでしょ」という期待を胸に店舗に到着した所、なにやら、ビラが設置してあるではないか。

あっ(察し

「本日の受付は終了しました」

えぇ...この時間に来てもダメなのかと落ち込んでいたところ、店員さんが声をかけてくれた。修理に出したい旨を伝えたところ、現在(2020/08)、AppleStoreでは修理依頼を行うには事前に予約をする必要があるらしく、店舗での予約なしの受付は出来ないとのこと。
「予約してから来店してください」と案内を受け取った。

f:id:takamizawa46:20200819094203j:plain:w500

合わせて、MacBookの場合は専門のスタッフがいないと対応出来ないらしく、急ぎであればAppleの公式のカスタムサポートに電話するのが良いと教えて頂いた。

Appleのカスタムサポートを予約

「いきなり今から掛けます〜」という事は出来ないようで、事前にカスタムサポートを予約する必要がある。

support.apple.com

ここで、自分が抱えているトラブルをカテゴリ分けする。
自分の場合は「MacBook -> 起動または電源 -> 正常に起動しない」を選択。
合わせて、

  • チャット対応
  • 電話対応
  • 店舗に持ち込んで対応

と3つの中から対応を選択することが出来る。今回は店舗に持ち込むのが上記の通り、難しくて早く修理に出したかったので電話対応を予約した。時間帯や平日なのか祝日なのかにもよるが、8月15日23:00時に予約を試みて、8月16日10:30時の枠を予約することが出来た。朝は9時からやっているようだ。

運が良ければ、すぐにサポートを受ける事が出来る。
f:id:takamizawa46:20200818095144p:plain:w500

電話対応の内容

翌日になり、予約した10時30分の5分ほど前からイヤホンをつけて電話がかかってくるのを待っていた。しかし、10時30分を過ぎても電話がかかってこない。「あれ、予約の時間を間違えたか」と思ったものの、33分頃になって電話がかかってきた。

かなり繁忙期のようで、これぐらいは仕方がないと思う。

電話に出ると、自動対応の音声が流れる。このままオペレーターと繋ぎたい場合は「1」を押してねというので、「1」を押した。2分ほどして、オペレーターと電話が繋がった。こちらから、今回、電話した旨を伝えようと思ったが、事前に電話予約の際にカテゴリを選択しているため、不要だった。

簡単な確認作業(今、電源が入るか, OSは何だったか)を行った後、専門のサポーター(Macスペシャリスト)に繋いで問題の確認をしたいとのこと。この判断が早く、最初の方と通話して5分程でMacスペシャリストに繋いでくれることになった。

3分ほど待っている(保留中に流せる音楽が選べるのが笑った)とMacスペシャリストの方と電話がつながった。今回の問題を質問を受けて、1つ1つ明らかにして頂いた。

  • どれぐらい使用しているか
  • スペック
  • Serial.Noは何か
  • 発生場所(住所))はどこか
  • 今まで同じことが発生したことがあるか
  • 他に何か気になる現象があるか ... etc

15分ほど、やり取りをした結果、内部トラブルの可能性が高いとのことで修理をするのが良いという判断になった。とはいえ、修理代が2016年のMacbookProの場合、最低でも53,000円するそうで、「ちょっと考えさせてください」と伝え、修理をするかどうかをその場で即決しなかった(出来なかった)。

(53,000もするなら新しいMacbook買うもんなぁ...)

しかし、サポートが非常に手厚く、さすがスペシャリストだなぁと思った。変な言い方だけど、自分の言いたいことが伝わるのは非常に楽。以前、別の会社のPCを使っていて、サポートセンターに連絡したものの、担当が中国の方で日本語が通じない上に、専門の方でもないため、電話したのに何も解決しなかった。

そんなこともあって、カスタムサポートにはあまりイメージがなかったが、今回の一件で印象が大きく変わった。
Appleのサポートのスマートな対応に感謝の気持ちしかない。

結果

額が額だったんで、一旦、修理は見送ることにした。業務で支給されているMacBookProがもう1台あるので、プライベートで使うPCは10万ぐらいで自分で組んでしまうのがいいんじゃないかと思った。

と、思ったら何か壊れたと思ったMacBookの電源がついた。え、どういうことなの...。焦げた匂いが若干するが、1週間経過したものの、特に問題なく使えている。発火する恐れがあるので、用心が必要だが、しばらく様子を見てみる。

Oauthを簡単なプロセス通信でそれっぽく再現して覚える

ワイ認証が全然分かっていない

認証部分は自分が実装する前にパイセンエンジニアが既におおむね、下地を作った状態で、「これ使ってね〜」状態になっている事が多く、初期の開発に参加出来なかったりする。過去に趣味開発でOauthなりを使用したことはあるが、コピペで作ったので正直、よく覚えていないし理解もしていなかった。
最近はバックエンドでの開発が多いので、自分の苦手分野でもある認証について改めて理解を深めたいと思った。

ざっくりと調べてみた

認証と言っても幅広く、様々なものあった。簡単に今回、目にしたものをリスト形式で書き出した。
(調査時のメモはgitのREADME.mdに記載しました)

  • SSO(SingleSignOn) -> eg: 免許書1枚で様々な契約が可能
    • 知ってるやつ: SAML(プロトコル) -> Idp(IdentityProvider: ユーザー認証)とSP(ServiceProvider)でお互いに利用できるように設定(SAML対応していることが大前提)
    • Oauth(Oauth -> 認可(認証じゃない), OpenIDConnect -> 認証)
  • Basic認証
    • IDとPASSをheader paramaterに含めて送信して認証

普段よく聞く単語と出会うことが出来た。Web開発をメインでやっているので、自分と最も関わりが深いのはOauthだと思うし、最も気になる所。
Oauthの流れについては以下の記事が驚く程、分かりやすかった。

qiita.com

確かに一番分かりやすい。

動くものを作って学ぶ

やはり、動くものを作って理解を深めたいのでTwitterなりのAPIを申請しようかと思ったが、申請期間を待ったり、合わせてデータベースを用意したりするのが面倒だったので、今自分が持っているレシピの中でサクッと作れるであろう、プロセス間通信を利用してOauthの流れを再現してみようと思う。

概要図

処理の流れについて図にまとめた。以下のようにプロセス間通信を行い、一連の認可(Oauth故)の処理を行えば、再現出来ていると言えるのではないだろうか。

f:id:takamizawa46:20200816113039p:plain
process_oauth

実装

github.com

上記の図に登場した3つのプロセスをそれぞれモジュールで表現する。

順にそれぞれを簡単に解説しておく。

メインプロセス

github.com

他2つのプロセスを立ち上げて、一連の処理の流れを実行するためのプロセスとなる。command lineから受け取った引数によって、実行する処理を流れを変更する。

  • PASS -> 認証に成功してリソースの取得が可能
  • UNAUTHORIZED -> 認証に失敗してアクセストークンの発行がされない
  • UNVALID_TOKEN -> 認証にし成功してアクセストークンが発行されるが、有効ではないトークンが送られるためリソースの取得に失敗

認証を実行するプロセス

github.com

このプロセスは2種類のメッセージを受け取り、認証処理を実行する。ユーザー情報の保持には、サーバーにkey valueのobject(Elixirではmap)を保持させて、管理している。なお、パスワードのハッシュ化や暗号化は簡単のために今回は行っていない。合わせて、ユーザー情報はプロセスを立ち上げる際に、Fakerという外部ライブラリを使用して、適当な名前(id)とパスワード(password)を作成している。

www.okb-shelf.work

eg: ユーザー情報

[
  %{"id" => "yu-gi", "password" => "atemu"},
  %{"id" => "kaiba", "password" => "mokuba"},
  %{"id" => "zyo-no-chi", "password" => "sizuka"},
]
  • AUTH
    • 送られてきたidpassword情報を元にサーバーで保持しているユーザーのidpasswordに一致する存在するかを確認
    • 存在しているユーザーであれば、アクセストークンを作成してメッセージを通して返す
    • 作成したアクセストークン(value)をユーザー情報に紐づけてaccess_tokenというkeyで保持させる

eg: 認証に成功した後のuser情報

[
  %{"id" => "yu-gi", "password" => "atemu", "access_token": "xxxxxxxxx"},
  %{"id" => "kaiba", "password" => "mokuba"},
  %{"id" => "zyo-no-chi", "password" => "sizuka"},
]
  • CONFIRM
    • アクセストークンを受け取り、サーバーで保持しているユーザーで該当のアクセストークンが発行されたユーザーが存在するかを確認
    • 確認結果(boolean)をメッセージを通して返す

リソースを返却するプロセス

github.com

ここでは1種のメッセージ(as requset)を受け取り、処理を実行する。

  • REQUEST
    • 送られてきたアクセストークンを認証を実行するプロセスにメッセージ経由で送信
    • 認証結果を受け取り、true(通過)した場合はリソース(今回は簡単のため、ただの文字列)をメッセージを通して返す。falseの場合は401errorをraiseする

実行結果

処理の流れが確認できるようにloggerを仕込んであるので、順に結果を確認してみよう。
先にiex -S mixを使って、iexを立ち上げておく。

AUTH

iex(1)> OauthDemo.main("PASS")

# id => Ollie, password => 4HDiMF8jを受け取り、認証を実行
10:47:16.365 [info]  Received request 'Oauth': id => Ollie, password => 4HDiMF8j 
# 存在するユーザーだったため、アクセストークンが発行される
10:47:16.377 [info]  Created access token: access_token => husjlbdpho 
# リソースサーバーにリクエスト。合わせて認証を実行するプロセスでアクセストークンの確認を行う
10:47:16.377 [info]  Received request 'Confirm': access_token => husjlbdpho
# 問題ないアクセストークンであったため、リソースを返す
10:47:16.378 [info]  Received request: passed authorization
# 返ってきたリソースを出力
[Success] Response: Nasubi
:ok

UNAUTHORIZED

iex(2)> OauthDemo.main("UNAUTHORIZED")

# id => okabe, password => 1234を受け取り、認証を実行
10:52:29.226 [info]  Received request 'Oauth': id => okabe, password => 1234
 # 存在しないユーザーだったため、アクセストークンが発行されず401 errorをraise
10:52:29.227 [info]  Unauthorized. Not registed user => okabe
# 認証失敗の結果が返ってきたことを確認し出力 
[Success] Response: Unauthorized
:ok

UNVALID_TOKEN

iex(3)> OauthDemo.main("UNVALID_TOKEN")

# id => Derrick, password => ebOyk+eoを受け取り、認証を実行
10:54:08.658 [info]  Received request 'Oauth': id => Derrick, password => ebOyk+eo
# 存在するユーザーだったため、アクセストークンが発行される
10:54:08.658 [info]  Created access token: access_token => bsnmpgfrhb
# リソースサーバーにリクエスト。合わせて認証を実行するプロセスでアクセストークンの確認を行う
10:54:08.658 [info]  Received request 'Confirm': access_token => xxxxxxxx
# 不正なアクセストークンだったため、401errorをraise
10:54:08.659 [info]  Received request:  unauthorization
# 不正なアクセストークンによる失敗の結果が返ってきたことを確認し出力
[Success] Response: Unvalid token
:ok

最後に

一連のOauthの流れを作成して覚えることが出来た。今回は簡単のため、多くの機能やアーキテクチャを省略している。パスワードが筒抜けなのはありえないし、ユーザーの新たな追加やユーザー情報の確認など、追加しようと思えばいくらで機能は追加が可能だが、今回の本流となるところではないので、触れなかった。少しは認証に強くなれたと思いたい。

参考文献