よくあるスマホアプリの、バックエンド側実装です。
まあ、自分用のカンペですね。

概要

  • クライアントからのメッセージを受け取り、レスポンスを返す
  • HTTP(S)で通信する
  • リクエスト、レスポンスはjsonを暗号化して行う

イメージ

atu-spring-json-request-response-image

  • 上段
    クライアントがリクエストを行い、サーバが受け取る
  • 下段
    サーバがレスポンスを行い、クライアントが受け取る

この記事では画像の右側、サーバ側の処理を説明していきます。

また、チートを防止するため、通信内容は暗号化します。

開発環境

  • Java 7
  • Spring 4
  • Maven 3
  • jackson 2.4
  • commons-codec 1.9

jacksonはJSONのコンバートに利用します。
commons-codecはBase64エンコード/デコードに利用します。

通信インターフェース

リクエスト

  • Content-Typeはapplication/hal+json
  • POSTメソッド
  • Request bodyに暗号化したJSON文字列をセット

サンプルリクエストフォーマット

「カードを合成するAPI」というイメージです。

{
    "baseCardId"      : 1,
    "materialCardIds" : [11, 12, 13, 14, 15]
}

サンプルのため簡易にしております。

過去の開発事例では、JSONをメタ情報API固有の情報を取り扱う、2つのパートに整理しておりました。
メタ情報は、例として次のようなものがあげられます。

  • クライアント側のアプリバージョン
  • OS (iOS / Android)
  • UserAgent
  • 言語

レスポンス

  • Content-Typeはapplication/hal+json
  • HTTPのstatus codeは、特別なエラーがない限り200とする

サンプルレスポンスフォーマット

{
    "status"       : 0,
    "message"       : "",
    "updateCard"    : {
        "cardId" : 1,
        "exp"    : 888,
        :
    },
    "deleteCardIds" :  [11, 12, 13, 14, 15]
}

レスポンス内容は次の内容です。

パラメーター 内容
status そもそもの通信が成功したかどうかを表すパラメーター
message エラー時の通知メッセージ
その他 変化があったカード情報

暗号化・復号

暗号化方式にはいろいろありますが、ここでは共通鍵暗号であるAESを利用します。

サーバ、クライアントで同じ鍵(共通鍵)を保持し、暗号化、複合化の際に指定します。

エンコード手順

  • 1.オブジェクトを用意する
  • 2.オブジェクトをJSONにコンバートする
  • 3.暗号化を行う
  • 4.暗号化によって得られたバイト配列をbase64でエンコードする

デコード手順

  • 1.受け取ったbase64文字列をデコードする
  • 2.デコードしたバイト配列をJSONに復号する
  • 3.JSONをオブジェクトにコンバートする

メモ

パッディング指定に注意

暗号化する場合、ブロック長に満たない部分をパディング方式で補完しますが、以下のようにパディングにも複数の方式があります。

  • AES/ECB/ISO10126Padding
  • AES/ECB/PKCS5Padding
  • AES/CBC/ISO10126Padding
  • AES/CBC/PKCS5Padding

言語によってはデフォルトが異なる場合がありますので、パディング形式も揃えてください。
このサンプルではAES/CBC/PKCS5Paddingとしています。

暗号化キーを可変に

暗号化キーは可変とし、どこからか差し込めるうようにしておくとベターです。

特殊文字の取り扱い

10年ほど前はゲームといえばバイナリフォーマットで通信するものでしたが、通信インフラや端末の処理性能が飛躍的に向上しておりますので、HTTPでただの文字列を通信フォーマットとして採用することが当たり前になりました。

しかし、文字列はどこまで行ってもただの文字列に過ぎません。

JSONでも、リテラルとして使用できないものがありますので、エスケープ処理が必要になる場合があります。
こちらも、サーバ、クライアントが異なる言語となる場合に、文字列の解釈に差異が出て問題が出るケースがあります。

私の過去事例では、カンマなどの一部記号や日本語はunicodeシーケンスに変換することで回避しました。

[Java][Spring]jacksonでマルチバイト文字をエスケープする

実装

CryptUtil

暗号化、複合化を行うクラスです。

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class CryptUtil {

    private CryptUtil() {}

    public static byte[] encrypt(byte[] binary, String aesKey) throws Exception {
        return CryptUtil.exec(Cipher.ENCRYPT_MODE, binary, aesKey);
    }

    private static byte[] exec(int cipherMode, byte[] binary, String aesKey) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(cipherMode, new SecretKeySpec(aesKey.getBytes(), "AES"));
        return cipher.doFinal(binary);
    }

    public static byte[] decrypt(byte[] binary, String aesKey) throws Exception {
        return CryptUtil.exec(Cipher.DECRYPT_MODE, binary, aesKey);
    }
}

aesKeyは16バイトの文字列とします。

また、本当はいろいろな例外がスローされるのですが、throws Exceptionで誤魔化してしまっていますね。
ゴメンナサイ。

スローされうる例外は以下の通りです。

  • NoSuchAlgorithmException
  • NoSuchPaddingException
  • InvalidKeyException
  • IllegalBlockSizeException
  • BadPaddingException

リクエスト

Jsonフォーマットに合わせて構造体を用意し、jacksonを使ってオブジェクトにリクエストを差し込んでいきます。
まず、リクエストインターフェースに合わせたクラスを用意します。

リクエストに対応するPOJO

@Data
public class RequestBean implements Serializable {
    private static final long serialVersionUID = 1L;

    private Integer baseCardId;
    private List<Integer> materialCardIds;
}

復号しJSONにコンバート

コントローラーで@RequestBosy requestBodyして受け取ったPOSTのリクエストボディを復号します。

String json;
try {
    // baase64デコードで得たバイト配列を複合
    byte[] encryptedBytes = Base64.decodeBase64(requestBody);
    byte[] decryptedBytes = CryptUtil.decrypt(encryptedBytes, environment.getAesKey());
    // 復号したバイト配列を文字列化
    json = new String(decryptedBytes, "UTF-8");
} catch (Exception e) {
     // 例外処理
}

共通かぎは環境ごとに定義し、DIしたクラスから受け取る実装です。
環境定義については、以下の記事の実装を採用しております。

[Java][Spring]BeanFactoryを使った環境設定

バイト配列をUTF-8のJSONに変換する際UnsupportedEncodingExceptionがスローされます。
復号できる文字列ならばチートの可能性も低いので握りつぶしてしまっても問題ない感じですね。

データバインディング

次にjacksonを用いて、復号したJSONをクラスに差し込みます。

RequestBean request = null;
try {
    ObjectMapper mapper = new ObjectMapper();
    request = mapper.readValue(json, RequestBean.class);
} catch (IOException e) {
    // 例外処理
}

マッパーを生成してreadValueするだけですから、簡単ですね。

レスポンス

こちらも、レスポンスに相当するクラスを用意します。

レスポンスに対応するPOJO

@Data
public class ResponseBean implements Serializable {
    private static final long serialVersionUID = 1L;

    private Integer       status;
    private String        message;
    private Card          updateCard;
    private List<Integer> deleteCardIds;
}

JSON化と暗号化

上のクラスにデータをセットし、JSON化します。

// レスポンスBeanをnewし、レスポンスする内容をSet
ResponseBean response = new ResponseBean();
response.setStatus(SUCCESS);
:
:

String responsString;
try {
    // レスポンスBeanをJSONに
    ObjectMapper mapper = new ObjectMapper();
    String json = mapper.writeValueAsString(response);

    // JSONを暗号化し、さらにbase64エンコードする
    byte[] encryptedBytes = CryptUtil.encrypt(json.getBytes(), environment.getAesKey());
    responsString = Base64.encodeBase64String(encryptedBytes);
} catch (Exception e) {
    // 例外処理
}

幸せになるために

  • 常に通信が成功するとは限らない
  • チートされるのがデフォルトと心得る
  • システムエラーとユーザ起因のエラーをしっかり区別する

終わりに

こういった通信の基盤となるプログラムは一度作ってしまえばそうそう手を入れることはありませんね。
こういったところでうまく工数を削減できるようにすると、イイと思います。

以上、この記事はtomita@atuwebがお届けしました。


2016年08月20日:特殊文字のエスケープについて別記事へのリンクを追加、コードの一部を修正

スポンサーリンク
ad_336
ad_336
  • このエントリーをはてなブックマークに追加
  • Evernoteに保存Evernoteに保存
スポンサーリンク
ad_336
コメントの入力は終了しました。