LoginSignup
2
0

More than 1 year has passed since last update.

【Swift】Vapor で Basic 認証を実装する

Last updated at Posted at 2023-04-28

こんにちは。kamimi です。🌞
前回に引き続き、Vapor を使った実装にハマっています。(二重の意味で)

今回は Vapor を使った API の Basic 認証の実装について書きたいと思います。

実装

こちらの公式ドキュメントに沿って、実装をしていきます。

ユーザータイプ

まず、Authenticatableに準拠したユーザータイプを実装します。

User.swift
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のプロトコルを使用して実装します。

UserAuthenticator.swift
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のみを渡します。

router.swift
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 でのリクエスト結果
$ 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 でのリクエスト結果
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 のミドルウェア実装はこちら

参考

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0