★★:まあまあ

SPAにはWeb APIが用意されているのでSVF検索フィールドの値をCSV形式で出力して外部のシステムへ連携したり、SPAに格納したPDFのカスタムプロパティに値をセットしたりすることなど、手動では手間のかかる処理をSPAのWeb APIを利用することで自動的に実行できるようになります。

今回はこれからWeb APIの利用を検討、予定されている方に向けて、ログイン認証やセッション管理についての実装方法についてご説明します。

言語はJavaを前提にしておりますが、他の言語でも仕組みや考え方は同じです。

以下の解説を一通り実装したサンプルを本記事の末尾のリンクよりダウンロードいただけます。

※本記事は厳密にHTTPの仕様としての正しさよりも分かりやすさや実装のしやすさを優先した内容としております。

ログイン処理

まずはログインについてです。

マニュアル Web APIリファレンスの該当箇所は 認証Auth Login になります。

参考(マニュアル):認証 > Auth Login

  

対象の認証URL

http://<hostname>:44230/spa/service/auth/login

に対して ユーザーIDのパラメーター「user」、パスワードのパラメーター「password」に値をセットしてPOSTリクエストを行います。
※hostname、ポート番号、ユーザID、パスワードはご利用環境に合わせて読み替えてください。
※ユーザー「admin」のSPAオンプレ版の初期パスワードは「admin」、SPA Cloudの場合は開通時のメールにて案内が届いています。

以下の例ではHttpURLConnectionのインスタンスを生成し処理を行っています。

例1

/** URLベース */
private static final String URL_BASE = "https://XXXXXXXX.spa-cloud.com/spa/service";

/** URL ログイン */
private static final String URL_LOGIN = URL_BASE + "/auth/login";

/** ユーザID */
private static final String USERID = "admin";
/** パスワード */
private static final String PASSWORD = "admin";

~省略~

// HttpURLConnectionの作成
URL urlLogin = new URL(URL_LOGIN);
HttpURLConnection conn = (HttpURLConnection) urlLogin.openConnection();

try {
    // HttpURLConnectionの各種設定
    conn.setRequestMethod("POST");
    conn.setDoInput(true);
    conn.setDoOutput(true);
    conn.setUseCaches(false);
    conn.setAllowUserInteraction(false);
    conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

    // ユーザーIDとパスワードをセットする
    try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream(), "UTF-8"))) {
        writer.write("user=" + USERID);
        writer.write("&password=" + PASSWORD);
    }

}

※SPA CloudでIPアドレス制限をかけている場合は、実行端末からのリクエストが許可されたIPアドレスになっているかご確認ください。

Cookie/セッション管理

WEBアプリケーション開発の知識のある方ならご存じかと思いますが、WEBアプリケーションはステートレスな仕組み(常にサーバーとクライアント間のコネクションを保持しているわけではない)なので、ログイン後に別なリクエストを送りたい場合は「ログイン済みですよ」という値をリクエストに付与して送ってあげる必要があります。

SPAの場合はCookieJSESSIONIDという値を保持しこの機能を実現しています。

JSESSIONIDはログイン後のレスポンスのCookieから取得し、以降の処理では必ずこの値をCookieにセットしてリクエストする必用があります。

またSPA Cloudの場合JSESSIONIDの他にも必要なパラメーターがあり、JSESSIONIDと同様に対応する必用があります。

※2021/08/12現在、JSESSIONIDの他に AWSALB、AWSALBCORSの値もCookieにセットしてリクエスト送信が要なことを確認しています。将来的に追加や変更の可能性もありますので、以下に記載するような汎用的な実装を行うことをお勧めいたします。

そのため基本的な考え方としては、ログインのレスポンスからSet-Cookieで指定された値は、以降の処理ではリクエストのCookieにセットする必用があると認識しておく必要があります。

少し難しそうだなと感じられた方もいるかもしれませんね。確かにこのCookieの処理は全てを自前で実装しようとするとわりと手のかかる処理になります。

ただ一般的に現在よく利用されているメジャーなプログラム言語では簡単に処理できような機能が提供されていることが多かったり、専用のライブラリが提供されていたりするので、それらを利用するとあまり意識しなくても簡単に動作させることができます。

Javaの場合はjava.net.CookieManagerが該当し数行記載するだけで済みます。

//java.net.CookieManagerの利用

//以下の2行を追加するだけです。

//Cookieの処理
CookieManager cm = new CookieManager();
CookieHandler.setDefault(cm);

ただ開発環境の制約や事情によって自前で処理を実装する必用がある場合や、内部の挙動は把握しておきたいといった方に向けて、できるだけ内部処理が見えるよう仕組みと値の取得方法についてサンプルを交えたご説明を記載しておきます。ここは必要ないという人は、XSRF-TOKENの処理まで読み飛ばしてください。

まずはCookieの値の取得についてはまずレスポンスのSet-Cookie情報を確認すると考えやすいです。

ログイン処理レスポンスのSet-Cookieの例:

Set-Cookie: AWSALB=Bfn4Wb9c0lcEaShYUQQOOc6SwCcGrUFZt1pDMU1rB+NnZEl5HUAKvxUa

F8vc7oC3Ot1X9VOdAYGo8X4iN6G4k4qxlrgpixZyDlZa0B5feY4L6AGwngGO3sjxQEgL; Expires=Fri, 13 Aug 2021 05:46:50 GMT; Path=/; AWSALBCORS=Bfn4Wb9c0lcEaShYUQQOOc6SwCcGrUFZt1pDMU1rB+NnZEl5HUA

KvxUaF8vc7oC3Ot1X9VOdAYGo8X4iN6G4k4qxlrgpixZyDlZa0B5feY4L6AGwngGO3sjxQEgL; Expires=Fri, 13 Aug 2021 05:46:50 GMT; Path=/; SameSite=None; JSESSIONID=DDD68FF0AD88B958F4D7AB6922E46C49; Path=/; Secure; HttpOnly; XSRF-TOKEN=UZ70-SOXO-FTM4-DYMX-R9JM-3RN2-KBRO-LP7W; Path=/; Secure

レスポンスのSet-Cookieブロックにこの値が含まれるのですが、キー名=値の形式で「;」を区切りに複数のCookieがセットされています。

キー名=値;キー名=値;キー名=値;キー名=値;

そのため;」で文字列を分解し、必要なキー名で始まる=の後の文字を取得することになります。

CookieからJSESSIONIDとXSRF-TOKENの値を取得する実装例としては以下のようになります。

例3

// セッションIDを取り出す(次のリクエスト以降で常にセット)
Map<String, List<String>> headers = conn.getHeaderFields();
StringBuffer sbCookie = new StringBuffer();
for (Map.Entry<String, List<String>> entry:headers.entrySet()) {
    String key = entry.getKey();
    //レスポンスヘッダからCookieを取り出す
    if (key != null && key.equals("Set-Cookie")) {
        List<String> list = entry.getValue();
        for(Iterator<String>itr = list.iterator(); itr.hasNext();){
            String cookie = itr.next();
            sbCookie.append(cookie).append("; ");
            String[] array = cookie.split(";");
            for (String str : array) {
                //JSESSIONIDの値を取り出す
                if (str.matches("JSESSIONID=.*")) {
                    String[] kvp = str.split("=");
                    jsessionId = kvp[1];
                }
                //XSRF-TOKENの値を取り出す
                if (str.matches("XSRF-TOKEN=.*")) {
                    String[] kvp = str.split("=");
                    xsrfToken = kvp[1];
                }
            }

        }
    }
}
cookieAll = sbCookie.toString();
System.out.println("cookieAll = " + cookieAll);

System.out.println("JSESSIONID = " + jsessionId);
System.out.println("XSRF-TOKEN = " + xsrfToken);

自前で処理を実装する場合は、ログイン以降のリクエストではこのJSESSIONIDをはじめとしたSet-Cookieされた必要な値をCookieにセットした上でリクエストを処理する必用があります。

// JSESSIONIDをセット(Cookieに)
conn.setRequestProperty("Cookie", "JSESSIONID=" + jsessionId + ";");

XSRF-TOKENの処理

SPACloudではCSRF対策としてXSRF-TOKENの処理が必要になります。

ログインに成功するとレスポンスのCookieからXSRF-TOKENの値が取得できるので、以降のリクエストではこの値をHTTPヘッダーにセットする必用があります。

CSRF対策の詳細については他のサイト等でご確認してください。
オンプレミス版SPAではXSRF-TOKENはデフォルトでオフになっています。その場合はこの対応は不要です。
JSESSIONIDCookieにセットするのに対して、XSRF-TOKENはリクエストヘッダーにセットすることに注意が必要です。
※GETリクエストの場合はリクエストにセットする必用はありません。

XSRF-TOKENの値の取得の実装例としては上に表示されている例3の緑色の箇所に記載しましたのでそちらをご確認ください。

取得したXSRF-TOKENの値をリクエストにセットする際には以下のようにx-xsrf-tokenと、x-requested-withを指定します。

// XSRF-TOKENとX-Requested-Withをセット(リクエストのヘッダに)
conn.setRequestProperty("x-xsrf-token", xsrfToken);
conn.setRequestProperty("x-requested-with", "XMLHttpRequest");

ログイン以降のリクエスト実装例

指定フォルダー以下のファイル一覧の取得API(Documents List)での実装を例に、ログイン以降のリクエスト処理についてCookieの処理やXSRF-TOKENの利用方法について確認していきます。

参考(マニュアル):ファイル操作 > Documents List(Ver. 11)

以下のURL

http://<hostname>:44230/spa/service/documents_v11/<id>/list 

に対してPOSTリクエストを実行します。

<id>は仮に「16120」としますが、このIDはフォルダーに付与されたフォルダーIDで、このフォルダーID配下のファイル、リンク、フォルダーの情報を取得できます。以下の例4ではファイルIDを取得しています。

フォルダーIDはSPAにログインし対象のフォルダーのプロパティを開くことで確認が可能です。

例4

/** URL 文書リスト */
private static final String URL_DOCUMENT_LIST = URL_BASE + "/documents_v11/" + "16120" + "/list";

~省略~

//指定したフォルダーID直下のファイルの一覧名称/IDを取得する
// HttpURLConnectionの作成
URL urlDocList = new URL(URL_DOCUMENT_LIST);
conn = (HttpURLConnection) urlDocList.openConnection();

List<String> documentIds = new ArrayList<String>();

try {
    // HttpURLConnectionの各種設定
    conn.setRequestMethod("POST");
    conn.setDoInput(true);
    conn.setDoOutput(true);
    conn.setUseCaches(false);
    conn.setAllowUserInteraction(false);
    conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

    // XSRF-TOKENとX-Requested-Withをセット(リクエストのヘッダーに)
    conn.setRequestProperty("x-xsrf-token", xsrfToken);
    conn.setRequestProperty("x-requested-with", "XMLHttpRequest");

    // JSON形式で出力する指示
    conn.setRequestProperty("Accept", "application/json");

~省略~

    // 正常終了時
    // 正常終了した場合はJSONを取得できる (jacksonライブラリを利用した場合)
    ObjectMapper mapper = new ObjectMapper();
    JsonNode document = mapper.readTree(conn.getInputStream());
    //ファイル名取得
    for (JsonNode n : document.get("documentList")) {
        if (n.get("type").asText().equals("document")) {
            String docid = n.get("id").asText();
            documentIds.add(docid);
            System.out.println("documentid =" + docid);
        }
    }

} finally {
    conn.disconnect();
}

いかがだったでしょうか。ざっくりと説明させていただきましたが、確認いただきたいポイントは以下の3つです。

1. セッション管理用のCookie JSESSIONIDの処理はCookieManager が自動で行ってくれるので特に記載は必要ありません。

2. XSRF-TOKENとX-Requested-Withをリクエストのヘッダーにセットしています。

3. 取得した値はjson形式のレスポンスで返ってくるのでjacksonライブラリを使用して処理しています。

このポイントを理解すれば他のAPIでも同様に処理を行うことができます。

エラー処理

開発中にリクエストを送って何かパラメーターの指定が間違っていた、運用が始まって意図せずエラーが発した等、原因の調査材料としてエラーの内容を確認できるよう実装しておくことも重要ですよね。

SPAのWeb APIではレスポンスからSPAのエラーコードや、エラーメッセージを取得することができるので、この値を取得しコンソールやログに出力処理するようにすると良いと思います。

エラー時のレスポンスヘッダー(認証エラーの例)

X-Spa-Error-Code : -1;
X-Spa-Error-Message : User+authentication+failed.;

HTTPのステータスコードも出力しておくとSPA以外の要因でエラーになった場合でもある程度の状況把握ができるかもしれませんね。

以下の例2ではHTTPステータスコードでエラーの有無を判定し、SPAのエラーコード、エラーメッセージをコンソール出力しています。

例2

// 正常に終了することができなかった場合
if (conn.getResponseCode() != 200) {
    // HTTPステータスとSPA独自のエラーコードを取得できる
    int errCode = conn.getHeaderFieldInt("X-Spa-Error-Code", -1);
    System.out.println("HTTP ErrorCode[" + conn.getResponseCode() + "]");
    System.out.println("SPA ErrorCode[" + errCode + "]");
    String errMsg = conn.getHeaderField("X-Spa-Error-Message");
    if (errMsg != null) {
      System.out.println("SPA Error Message[" + URLDecoder.decode(errMsg, "UTF-8") + "]");
    }
    throw new Exception(conn.getResponseMessage());
}

※X-Spa-Error-MessageについてはURLエンコードされているのでデコードして利用してください。

SPAのエラーコードだけ見ても何が起きたのか理解できないケースもあると思いますが、この値については利用するAPI毎にマニュアルに説明があります。

例えば文書を検索するAPI Search Document V15ではエラーコード-300でアクセス権の問題、-705で検索結果のヒット数が多すぎることが確認できます。以下のリンクから確認してみてくださいね。

参考(マニュアル):検索 > Search Documents(Ver. 15)

最後に

少し長くなりましたがログインとCookieの処理/セッション管理の仕組み、XSRF-TOKENの処理について説明しました。

今回ご説明した一連の処理を実行できるサンプルのソースファイルを以下のリンクからダウンロードいただけますので参考にしてみてくださいね。

WebApiSample.javaをダウンロード

※右クリックして「名前を付けてリンク先を保存」等の機能でローカルに保存した上でご確認ください。ファイルの文字コードはUTF-8です。そのまま開くと文字化けして表示される場合があります。

※本記事の情報は、2021年08月12日現在のものです。(SPA V10.5.1 / SPA Cloud 2021 June Update)