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

概要

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

リクエスト、レスポンスのモデル

スマホアプリとバックエンドのリクエスト、レスポンスモデル
スマホアプリとバックエンドのリクエスト、レスポンスモデル
  • 上段
    クライアントがリクエストを行い、サーバが受け取る
  • 下段
    サーバがレスポンスを行い、クライアントが受け取る

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

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

開発環境

  • 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」というイメージです。

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

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

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

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

レスポンス

  • Content-Type はapplication/hal+json
  • HTTP の status codeは、特別なエラーがない限り 200 とする
サンプルレスポンスフォーマット
 1{
 2    "status"       : 0,
 3    "message"       : "",
 4    "updateCard"    : {
 5        "cardId" : 1,
 6        "exp"    : 888,
 7        :
 8    },
 9    "deleteCardIds" :  [11, 12, 13, 14, 15]
10}

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

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

暗号化・復号

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

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

エンコード手順

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

デコード手順

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

メモ

パッディング指定に注意

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

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

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

暗号化キーを可変に

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

特殊文字の取り扱い

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

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

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

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

[Java] Jackson でマルチバイト文字をエスケープする
JavaのJSONライブラリJacksonでマルチバイト文字をエスケープするコードサンプルです。
Atuweb 開発 Log

実装

CryptUtil

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

 1import javax.crypto.Cipher;
 2import javax.crypto.spec.SecretKeySpec;
 3
 4public class CryptUtil {
 5
 6    private CryptUtil() {}
 7
 8    public static byte[] encrypt(byte[] binary, String aesKey) throws Exception {
 9        return CryptUtil.exec(Cipher.ENCRYPT_MODE, binary, aesKey);
10    }
11
12    private static byte[] exec(int cipherMode, byte[] binary, String aesKey) throws Exception {
13        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
14        cipher.init(cipherMode, new SecretKeySpec(aesKey.getBytes(), "AES"));
15        return cipher.doFinal(binary);
16    }
17
18    public static byte[] decrypt(byte[] binary, String aesKey) throws Exception {
19        return CryptUtil.exec(Cipher.DECRYPT_MODE, binary, aesKey);
20    }
21}

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

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

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

  • NoSuchAlgorithmException
  • NoSuchPaddingException
  • InvalidKeyException
  • IllegalBlockSizeException
  • BadPaddingException

リクエスト

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

リクエストに対応するPOJO
1@Data
2public class RequestBean implements Serializable {
3    private static final long serialVersionUID = 1L;
4
5    private Integer baseCardId;
6    private List<Integer> materialCardIds;
7}
復号しJSONにコンバート

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

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

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

[Java][Spring]BeanFactoryを使った環境設定
SpringFrameworkでBeanFactoryを使った環境設定についてメモしました。
Atuweb 開発 Log

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

データバインディング

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

1RequestBean request = null;
2try {
3    ObjectMapper mapper = new ObjectMapper();
4    request = mapper.readValue(json, RequestBean.class);
5} catch (IOException e) {
6    // 例外処理
7}

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

レスポンス

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

レスポンスに対応するPOJO
1@Data
2public class ResponseBean implements Serializable {
3    private static final long serialVersionUID = 1L;
4
5    private Integer       status;
6    private String        message;
7    private Card          updateCard;
8    private List<Integer> deleteCardIds;
9}
JSON化と暗号化

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

 1// レスポンスBeanをnewし、レスポンスする内容をSet
 2ResponseBean response = new ResponseBean();
 3response.setStatus(SUCCESS);
 4:
 5:
 6
 7String responsString;
 8try {
 9    // レスポンスBeanをJSONに
10    ObjectMapper mapper = new ObjectMapper();
11    String json = mapper.writeValueAsString(response);
12
13    // JSONを暗号化し、さらにbase64エンコードする
14    byte[] encryptedBytes = CryptUtil.encrypt(json.getBytes(), environment.getAesKey());
15    responsString = Base64.encodeBase64String(encryptedBytes);
16} catch (Exception e) {
17    // 例外処理
18}

幸せになるために

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

終わりに

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

以上、