概要
開発の動機
このツールを作ろうと思った動機は、普段youtubeなどを見ていてタグごとに細かく整理できなかったり、ブラウザのブックマーク機能では不満なところがあったりしたので、もっと便利なもの(URLをタグごとにグルーピングしたりそれを共有したり)を作りたかったからです。
記事作成の動機
ポートフォリオ的なものがないので作りたかったのと後で振り返れるように。
認証機能の実装
TODO
- 必要な機能の整理
- 各機能の簡単な設計
- 実装方法の調査
- エンドポイント設計
- 自動テストの作成
設計・開発の心得
設計・開発はシンプルに
シンプルであることは手段である。
何故シンプルに?
-> それにより可読性、堅牢性、柔軟性、保守性が上がるからである
何故可読性、可読性、堅牢性、柔軟性、保守性を?
-> それにより開発をより円滑に進めるためである。
つまり、ゴールは開発をより円滑に進めることである。そのためにシンプルにする。
ちなみにこれを俯瞰で見たときに最も抽象どの高い部分に位置するのが「ゴールは開発をより円滑に進めること」で低い部分にあるのが「可読性、堅牢性、柔軟性、保守性」だと思われ。
ではシンプルとは何か?
-
必要最低限であること
Why? -> 量が多いと単純に必要な作業が増える。 -
適切に分割されていること
Why? -> 各機能が実装と1対1になることによって理解しやすくなり、変更にも強くなる。 -
適切に体系化(モデリング)されていること
Why? -> これは分割というより、どのように業務を認知するかである。技術的にはドメイン駆動設計、アジャイル開発の考え方に近い。モデリングとは物事を抽象化することなのでこれが正しければ正しいほど変更に強く、ソフトウェアが理解しやすくなる。 -
適切な名前を与えられていること
Why? -> 上に近い。ソフトウェアの構造がわかりやすくなる。 -
一貫した規律があること(命名規則とか)
Why? -> メタ的に理解しやすい。(例えばPCゲームではwasdで動くがこれがルールとしてあることで移動という操作をどのゲームでも学び直す必要がない。)またルールにしたがってれば何かの自動化などがしやすくなたたりもするかも--...XD
ps.車輪の再発明はするな。
必要な機能の整理
認証関連の処理
アカウントの作成
ログイン
ログアウト
簡易設計
基本ユースケース.1アカウント作成
- 登録するユーザーの情報を送信
- ユーザ名が使用済みか確認
- 使用済みであればその旨を返す
- 使用していない場合は新しくアカウントを作成する
- その後そのデータで新しくセッションを発行しIDをクッキーにセットしレスポンスを返す
基本ユースケース.2ログイン
- ログインに必要な情報の送信
- 送ってきたユーザが登録されているか確認
- 登録されていなければその旨を返す。
- 登録されていれば、セッション作成
- レスポンスを返す。
基本ユースケース.3ログアウトの流れ
- ログアウトのリクエスト送信
- ログインしていたらログアウト
- レスポンスを返す
実装関連調査
セッション
gin-contrib/sessionsを使えばセッション管理がginでできるらしい。
実装方法がcookieベースとpostgre,redisなどがあるが今回はredisを選択します。
後cookies.Sessionを使うと値が全てクッキーの保存されてしまうのでセキュリティ的に悪し。
サンプルコード
package main
import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/redis"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret"))
r.Use(sessions.Sessions("mysession", store))
r.GET("/incr", func(c *gin.Context) {
session := sessions.Default(c)
var count int
v := session.Get("count")
if v == nil {
count = 0
} else {
count = v.(int)
count++
}
session.Set("count", count)
session.Save()
c.JSON(200, gin.H{"count": count})
})
r.Run(":8000")
}
解説
ginの機能に関しては割愛する。
まずcookie.NewStoreに関して
size: maximum number of idle connections. network: tcp or udp address: host:port password: redis-password Keys are defined in pairs to allow key rotation, but the common case is to set a single authentication key and optionally an encryption key.
The first key in a pair is used for authentication and the second for encryption. The encryption key can be set to nil or omitted in the last pair, but the authentication key is required in all pairs.
It is recommended to use an authentication key with 32 or 64 bytes. The encryption key, if set, must be either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256 modes.
引数解説
size: 最大コネクション数
network: tcpかudp
adress: アドレス(localhost:1234)
password: パスワード
keyPairs: 可変長引数で認証キーに関しては必須。1つ目のバイト配列は認証キー用に2つ目は暗号化キーを渡す。認証キーには32or64バイト、暗号化キーには16or24or32バイトにするのがおすすめらしい。
sessions.Sessionsに関して
1つ目の引数はセッションストア名、2つ目の引数がデータの保存に使用するストア。
これをミドルウェアとして登録することでアプリで使用できるようになる。
後はsession.[Get,Set,Save,Clear]などでリソースの処理をすればよし。
Redisコンテナ作成方法
docker-compose redisでググれば出てくる。
ログイン失敗時のステータスコード
ログイン失敗時には401を返す。
エラーハンドリングルール
久しくちゃんとしたエラー処理をしていなかったので例えばアプリケーションに関しての処理ではエラーを返さないほうがいい。ファイルの読み書きやネットワークI/O、DBなどの操作に関してのエラーはエラーを投げるべきとか細かいグッドプラクティスがあったはずだが全部忘れてしまったので再調査。
ginのミドルウェアの仕様
RequireLoginミドルウェアで一部のリソースにログイン要求の管理をしたいのだが(ユーザ個人情報ではログインが必要だがユーザ作成にはログインが必要ない)ミドルウェア使用前にルーティングすれば適用されないのかもしくは1つずつ個別にミドルウェアを適用しないといけないのか調査する必要がある。
エンドポイント設計
一緒にUsersControllerのエンドポイントもあるがそれは別の機会に実装する。
メソッド | url | アクション |
---|---|---|
POST | /login | Login |
DELETE | /logout | Logout |
GET | /users | IndexUsers |
POST | /users | CreateUser |
GET | /users/:id | ShowUser |
PUT | /users/:id | UpdateUser |
DELETE | /users/:id | DestroyUser |
テストの作成方法
apiのテストにはpostmanを使用。
セッションを使ってテストするのでPostman Interceptorも使用。
framework
調査中後々使っていきたい
mock・stub系: gomock
DI: wire
BDD: Ginkgo
https://tyablog.net/2020/04/26/stub-mock-spy-introduction-in-spock-testing-framework/
テスト項目
テストのパッケージはテスト対象のパッケージと同じにする。
https://zenn.dev/diwamoto/articles/aba45dc2da36b8
- コントローラ
- モデル
- レポジトリ
- セッション
- 結合
- apiテスト用のツールもしくはそういったフレームワークがあればそれでテスト。
コントローラのテスト方法
まず最初に調べて出てきたのがgin.CreateTestContextを使用してのテスト。
これを使ってコントローラのテストをするところまではいけたがDBのところで毎回エラーが起きてしまう。
パッと思いつく解決方法はrepositoryのモックを作ってそれを使うこと。
次にそれをするために必要なことがmockをどこに格納するか。
今はメソッドではなく関数で実装している状態なのでコントローラの構造体を作成しそれにrepositoryをセットし、セットしたコントローラのアクションを登録するように変更するということが考えられる。
次にそれをどうやって入れ込むか。
DIを利用する。
まずmockだがgomockという公式が出しているものがあるらしいので調べてみる。
DIは今回はそこまで必要性がなかったので保留。
その他収穫
/auth/loginにpostされたJsonを表すクラスの命名 PostLoginJson LoginPostJsonどちらがいいか?
考えるきっかけになったお。それはなんなのと言われたらJsonなのでHogeJsonで良いのは確か。
次にLoginPostJsonがPostLoginJsonなのか?
わからん。だがPostJsonだと何のポストとはなるしLoginJsonだともっとアバウトになる。
こういう時は日本語にしてみるポストのJson、ログインのJson、ログインにポストされたJson
こう考えるとPostedLoginJsonがいいのか?だけど個人的にはというか確かreadblecodeにも受動態を使わんほうが良いとかあった気がするしどうなんだ。まあでもログインにポストされたJsonだからLoginPostJsonでいいのかな?
色々考えた結果LoginParameterにした。これだとログインに使うパラメータと一瞬でわかる...?
後はrequireLoginミドルウェアを作成してそこでログインしてればコントローラへしてなければ401を返しているのだがそれをただログインしているか確認してコンテキストのSetを使ってLoggedinの値を設定するだけにしてそれを全てのコントローラの前で使用してログイン時の処理と未ログイン時の処理の分岐をコントローラに任せるという実装も考えられる。
sessions.Get("key")で取得した値はSet時に使った型が使用される。
なのでcontext.Params("key")で取得した値かもう一方のセッションの型どちらかに合わせないといけない。
後はstring -> uintに戻すには strconvのparseuintでまずuint64にしてからuint(v)を使ってuintにキャストしないとけない。
テストの動かし方
https://qiita.com/KEINOS/items/a4e00f10bd944758e600
モデルとレポジトリの違い
https://zenn.dev/naoki_oshiumi/articles/0467a0ecf4d56a
作業中に何故かdocker for macのcpu使用率が100%超えるということが起きた。
原因は不明。
インストールし直しても変わらず。
OSが最新でなかったので更新すると直った。
今後実装したい機能
テーマのカスタマイズ
テンプレートテーマの作成
お気に入りのURLグループのテンプレート化と共有など