質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.97%

SWIFTでライブラリを使わないAPI認証でのAuthorizationエラー

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 70

masayoshi555

score 5

概要

SWIFTを使って、Alamofireなどのライブラリを使わないAPI通信を構築しています。

Swiftで始める仮想通貨システムトレード入門
こちらのサイトを参考にAPI全体の構成はBuildエラーが出ないところまで進めました。

そこで、以下のエラーが出て解決策が見出せずにいるので教えて頂きたく投稿しました。

エラー内容

Build Succeeded->Runでsimulatorを立ち上げ、リクエストを送信するトリガーになる
ボタンを押すと、デバッグコンソールに以下のように出力されます。
通信先からのResponseをString表示しています。

Optional("{\"error\":{\"message\":\"Authorization Required\",\"name\":\"HTTPError\"}}")


認証が必要とのエラー内容だと認識したので、デバッグコンソールでブレイクポイント作りながら
リクエスト内容を見ると、プログラム上認証作成するフローはしっかり動いており、リクエストURLは
できているように見えます。

以下はブレイクポイントを作りデバッグコンソールにて確認したurlRequestの値です。
アクセス先 : https://testnet.bitmex.com/api/

Printing description of urlRequest:
▿ https:/(アクセス先).com/api/v1/order?orderType=Limit&symbol=XBTUSD&orderQty=1&price=590
  ▿ url : Optional<URL>
    ▿ some : https:/(アクセス先).com/api/v1/order?orderType=Limit&symbol=XBTUSD&orderQty=1&price=590
      - _url : https:/(アクセス先).com/api/v1/order?orderType=Limit&symbol=XBTUSD&orderQty=1&price=590
  - cachePolicy : 0
  - timeoutInterval : 60.0
  - mainDocumentURL : nil
  - networkServiceType : __C.NSURLRequestNetworkServiceType
  - allowsCellularAccess : true
  ▿ httpMethod : Optional<String>
    - some : "POST"
  ▿ allHTTPHeaderFields : Optional<Dictionary<String, String>>
    ▿ some : 4 elements
      ▿ 0 : 2 elements
        - key : "Content-Type"
        - value : "application/json"
      ▿ 1 : 2 elements
        - key : "ACCESS-SIGN"
        - value : "b2dce06bcb67b6db3418b5018b45cf9f4442986ed05b76fc91bfb4303788332f"
      ▿ 2 : 2 elements
        - key : "ACCESS-TIMESTAMP"
        - value : "1575546"
      ▿ 3 : 2 elements
        - key : "ACCESS-KEY"
        - value : "アクセスキーが出力されています"
  - httpBody : nil
  - httpBodyStream : nil
  - httpShouldHandleCookies : true
  - httpShouldUsePipelining : false

認証周りコード

プログラム実行の流れ

  1. ユーザーアクション(ボタン押下)
  2. Sessionクラス呼び出し
  3. buildURLRequestメソッド(RequestAPI内のextension Requestable)実行
  4. buildURLRequest内でisAuthorizedRequestがtrueなら認証ヘッダを作成開始(コンソールでtrueであることは確認済)
  5. makeAccessSignWithにてHMAC SHA256認証のhashを作成
  6. buildURLRequestに戻り、全部を詰め込んでurlRequestとして完成
  7. Sessionクラスに戻り、URLSessionを使ってtask.resume()にて実行
enum Result<T, E: Error> {
    case success(T)
    case failure(E)
}
enum ResponseError: Error {
    case unexpectedResponse
}
//プロトコル定義
protocol Requestable {
    (省略)
}
extension Requestable {
    func buildURLRequest() -> URLRequest {
        let url = Self.baseURL.appendingPathComponent(Self.path)
        var urlRequest = URLRequest(url: url)
        var header: [String: String] = Self.headerField
        urlRequest.httpMethod = Self.httpMethod
        if Self.isAuthorizedRequest {
            header["ACCESS-KEY"] = "アクセスキーを入力してあります"
            let now = (Date().timeIntervalSince1970 / 1000) + 60
            let timeStamp = String(Int(now.rounded())
            header["ACCESS-TIMESTAMP"] = timeStamp
            header["ACCESS-SIGN"] = makeAccessSignWith(accessKey: "アクセスキーを入力してあります",
                                                       timeStamp: timeStamp,
                                                       method: Self.httpMethod,
                                                       path: Self.path,
                                                       queryParams: Self.queryParameters,
                                                       body: Self.httpBody)
            header["Content-Type"] = "application/json"
        }
        header.forEach { key, value in
            urlRequest.addValue(value, forHTTPHeaderField: key)
        }
        if let body = Self.httpBody {
            urlRequest.httpBody = body
        }
        guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
            return urlRequest
        }
        urlComponents.query = Self.queryParameters
            .map { "\($0.key)=\($0.value)" }
            .joined(separator: "&")

        urlRequest.url = urlComponents.url

        return urlRequest
    }
}
final class Session {
    static let shared = Session()
    private init() {}
    func send<T: Requestable>(_ request: T, closure: @escaping (Result<T.Response, ResponseError>) -> Void) {
        let urlRequest = request.buildURLRequest()
        let task = URLSession.shared.dataTask(with: urlRequest) { (data, rawResponse, error) in
            // If an error is occurred.
            if error != nil {
                closure(.failure(.unexpectedResponse))
                return
            }
            // If the data is empty.
            guard let data = data else {
                closure(.failure(.unexpectedResponse))
                return
            }
            // Decode the value.
            do {
                let decoder = JSONDecoder()
                let result = try decoder.decode(T.Response.self, from: data)
                closure(.success(result))
            } catch {
                let errorResult = String(data: data, encoding: .utf8)  //デバッグ用String化して中身見る
                print(errorResult as Any)
            }
        }
        task.resume()
    }
}
private func makeAccessSignWith(accessKey: String, timeStamp: String, method: String, path: String, queryParams: [String: Any], body: Data?) -> String? {

    let key = "シークレットキーを入力しています"
    var bytes: [UInt8] = []
    bytes += timeStamp.bytes
    bytes += method.bytes
    bytes += path.bytes
    func makeSign (queryParams: [String: Any]) -> String {
        let requestBody = "?" + queryParams
            .map { "\($0.key)=\($0.value)" }
            .joined(separator: "&")
        return requestBody
    }

    if !queryParams.isEmpty {
        let str = makeSign(queryParams: ViewController.PostNewOrderRequest.queryParameters)
        let data: Data? = str.data(using: .utf8)
        let hexStr:String = data!.map {String(format: "%.2hhx", $0)}.joined()
        var bytes = [UInt8]()
        for char in hexStr.utf8{
            bytes += [char]
        }
    }

    if body?.isEmpty == false {
        if let bodyParameter = body,
            let bodyString = String(data: bodyParameter, encoding: .utf8) {
            bytes += bodyString.utf8
        }
    }
    let signedString = try! HMAC(key: key, variant: .sha256).authenticate(bytes)
    return signedString.toHexString()
}
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

0

お疲れ様です。

このドキュメントを見ると[permanent API key]が必要のようです:Bitmex REST API

そして、Authenticating with an API Keyによると以下のヘッダーを送る必要があります:

api-expires
api-key
api-signature


masayoshi555さんが提供してくれたサンプルコードにはヘッダーはちょっと違う気がします。
ご確認お願いします。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/12/06 04:20

    vanderlvovさん
    回答ありがとうございます! "api-expires"などの名前も揃えないといけないと知らず。ご指摘頂き大変たすかりました。
    以前javascriptでは通信成功しているので、まずはそちらと変数名など合わせるように改造してみます。
    ちなみに、一旦、上記変更したところエラーはなくなり別のエラーに移行しました。
    進められそうです。ありがとうございます。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.97%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる