こんにちは。kamimi です。🌞
前回に引き続き、Vapor を使った実装にハマっています。(二重の意味で)
今回は Vapor を使った API の Basic 認証の実装について書きたいと思います。
実装
こちらの公式ドキュメントに沿って、実装をしていきます。
ユーザータイプ
まず、Authenticatable
に準拠したユーザータイプを実装します。
import Vapor
struct User: Authenticatable {
let name: String
}
オーセンティケーター
次に要となる認証の実装です。
公式ドキュメントでは、認証のミドルウェアを作成するためのヘルパーのプロトコルが2つ紹介されていました。結論、今回は2つ目を使います。
BasicAuthenticator
AsyncBasicAuthenticator
1つ目は、async/await 登場前に実装されたプロトコルです。
SwiftNIO という Apple の OSS ライブラリを使って実装されています。ただ、以下のドキュメントにも詳しく書いてあるとおり、「大抵のコードが async/await を使用して書くことができるようになったため、基本的には asycn/await を使ってください」とあります。
AsyncBasicAuthenticator
のコードコメントにも、This is an async version of BasicAuthenticator
(これは BasicAuthenticator の async バージョンです)と記載がありました。
ということで今回は AsyncBasicAuthenticator
のプロトコルを使用して実装します。
import Vapor
class UserAuthenticator: AsyncBasicAuthenticator {
func authenticate(basic: Vapor.BasicAuthorization, for request: Vapor.Request) async throws {
// username と password をハードコードしているが、実際はこれやってはだめ
if basic.username == "test" && basic.password == "secret" {
request.auth.login(User(name: "Vapor"))
}
}
}
呼び出し方
ミドルウェアとして、各ルートにいく前に認証を挟むように実装します。
grouped(_ middleware: Middleware...)
は、ミドルウェアをラップしてルーターを返すメソッドです。以下のように可変長の引数を渡すことができます。
let group = router.grouped(FooMiddleware(), BarMiddleware())
今回は認証のミドルウェアとして、先ほど実装したUserAuthenticator
のみを渡します。
import Vapor
func routes(_ app: Application) throws {
let protected = app.grouped(UserAuthenticator())
protected.post("authTest") { req in
try req.auth.require(User.self).name
}
}
結果
実装できたので、ちゃんと認証が実装されているか確認します。
リクエストしている API は私が最近実装中の API です。
認証失敗
以下のようにリクエストしてみました。Authorization ヘッダーなしでのリクエストです。
$ curl -v --location --request POST 'http://127.0.0.1:8080/postAppStoreState' \
--header 'Content-Type: application/json' \
--data-raw '{
"channelID": "C01JJKQPKCK",
"appIDs": [
"1673161138", "1668609447", "1668244395", "1667179734"
]
}'
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST /postAppStoreState HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.87.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 120
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 401 Unauthorized
< content-length: 38
< content-type: application/json; charset=utf-8
< connection: keep-alive
< date: Fri, 28 Apr 2023 22:52:28 GMT
<
* Connection #0 to host 127.0.0.1 left intact
{"error":true,"reason":"Unauthorized"}
ステータスコードは401 Unauthorized
が返ってきています。
ボディは
{"error":true,"reason":"Unauthorized"}
でレスポンスされていることがわかります。
認証成功
確認するまでもないかと思いますが、一応こちらの結果も載せておきます。ちゃんとAuthorization
ヘッダーをつけます。
curl -v --location --request POST 'http://127.0.0.1:8080/postAppStoreState' \
--header 'Authorization: Basic dGVzdDpzZWNyZXQ=' \
--header 'Content-Type: application/json' \
--data-raw '{
"channelID": "C01JJKQPKCK",
"appIDs": [
"1673161138"
]
}'
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST /postAppStoreState HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.87.0
> Accept: */*
> Authorization: Basic dGVzdDpzZWNyZXQ=
> Content-Type: application/json
> Content-Length: 78
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-type: application/json; charset=utf-8
< content-length: 290
< connection: keep-alive
< date: Fri, 28 Apr 2023 22:56:45 GMT
<
* Connection #0 to host 127.0.0.1 left intact
{"channelID":"C01JJKQPKCK","appIDs":["1673161138"],"postMessage":"iOS アプリのステータスをお知らせします :apple:\n【TopicGen】\nバージョン:1.1\nステータス:販売準備完了(READY_FOR_SALE) :ok_hand::moneybag:\n作成日時:2023\/02\/21 20:51:45\n\n"}
ステータスコードは200 OK
、
ボディは
{
"channelID": "C01JJKQPKCK",
"appIDs": [
"1673161138"
],
"postMessage": "iOS アプリのステータスをお知らせします :apple:\n【TopicGen】\nバージョン:1.1\nステータス:販売準備完了(READY_FOR_SALE) :ok_hand::moneybag:\n作成日時:2023/02/21 20:51:45\n\n"
}
ということで、想定したレスポンスが返ってきています。
おわりに
この認証の実装だけではないのですが、Vapor のドキュメントはとても充実していてわかりやすいです。日本語はないのでそこは頑張って読む必要があります。
どのサーバーサイドフレームワークも同様なのかもしれないですが、各ルートにいく前にミドルウェアを挟めたりする点は、express とかと一緒なんだなあと、初心者的感想を持ちました。(express のミドルウェア実装はこちら)
参考