Spring Boot セキュリティ関連

API通信をAESで暗号化してみた

今回は、以前作成したREST API通信のプログラムを、共通鍵暗号方式の1つであるAES(Advanced Encryption Standard)を利用して暗号化するようにしてみたので、そのサンプルプログラムを共有する。

共通鍵暗号方式については、以下のサイトを参照のこと。
https://www.infraexpert.com/study/security4.html

前提条件

下記記事のサンプルプログラムの作成と前提条件のデータ作成が完了していること。

Spring BootのWEB画面上でREST APIを使ってリストを取得してみた前回は、クライアント側からサーバー側で、REST API接続によりユーザーデータを取得しクライアント側の画面に表示していたが、今回は、R...

サーバー側のプログラムの作成と実行

作成したサーバー側(demo_server)サンプルプログラムの構成は以下の通り。

なお、上記の赤枠は、前提条件のプログラムから変更したプログラムである。

build.gradleの内容は以下の通りで、暗号化を行うための設定を追加している。

plugins {
	id 'org.springframework.boot' version '2.1.7.RELEASE'
	id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	//lombokの設定を追加
	compileOnly 'org.projectlombok:lombok:1.18.10'
	annotationProcessor 'org.projectlombok:lombok:1.18.10'
	//oracleを利用するための設定を追加
	compile files('lib/ojdbc6.jar')
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	//暗号化を行うための設定を追加
	compile group: 'commons-codec', name: 'commons-codec', version: '1.8'
}



application.ymlの内容は以下の通りで、暗号化に必要な項目を追加している。 暗号化キー(key)は、16バイトの文字列を設定している。

# ポート番号:8085で起動
server:
  port: 8085
# DB接続情報
spring:
  datasource:
    url: jdbc:oracle:thin:@localhost:1521:xe
    username: USER01
    password: USER01
    driverClassName: oracle.jdbc.driver.OracleDriver
# 暗号化項目を設定
encrypt:
  algorithm: AES
  charset: utf-8
  key: 1234567890abcdef



フリーランスエンジニアのエージェントは就業中でも無料で登録できるITエンジニアには、フリーランスという働き方がある。 フリーランスとは、会社や団体などに所属せず、仕事に応じて自由に契約する人のこ...

コントローラクラスの内容は以下の通りで、encryptメソッドでAESによる暗号化を行っている。なお、暗号化に必要な項目値は、application.ymlから取得している。

package com.example.demo;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Sort;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.List;

import static org.springframework.data.domain.Sort.Direction.ASC;

@RestController
public class DemoController {

    /**
     * 暗号化アルゴリズム
     */
    @Value("${encrypt.algorithm}")
    private String encryptAlgorithm;

    /**
     * 暗号化時に利用する文字コード
     */
    @Value("${encrypt.charset}")
    private String encryptCharset;

    /**
     * 暗号化時に利用する秘密鍵
     */
    @Value("${encrypt.key}")
    private String encrypyKey;

    /**
     * ユーザーデータテーブル(user_data)へアクセスするリポジトリ
     */
    @Autowired
    private UserDataRepository repository;

    /**
     * ユーザーデータを全件取得する
     * @return ユーザーデータリスト(JSON形式)
     */
    @GetMapping("/getUserDataList")
    public String getUserDataList(){
        List<UserData> userDataList = repository.findAll(new Sort(ASC, "id"));
        // ユーザーデータが取得できなかった場合は、null値を返す
        if(userDataList == null || userDataList.size() == 0){
            return null;
        }
        for(UserData userData : userDataList){
            // 性別を表示用(男,女)に変換
            userData.setSex("1".equals(userData.getSex()) ? "男" : "女");
            // 名前・メモ・性別を暗号化
            userData.setName(encrypt(userData.getName()));
            userData.setMemo(encrypt(userData.getMemo()));
            userData.setSex(encrypt(userData.getSex()));
        }
        // 取得したユーザーデータをJSON文字列に変換し返却
        return getJsonData(userDataList);
    }

    /**
     * 引数の文字列を暗号化する
     * @param data 任意の文字列
     * @return 暗号化後の文字列
     */
    private String encrypt(String data) {
        if(StringUtils.isEmpty(data)){
            return "";
        }
        String retVal = null;
        try {
            // 暗号化キーオブジェクトを生成
            SecretKeySpec key = new SecretKeySpec(
                    encrypyKey.getBytes(encryptCharset), encryptAlgorithm);
            // Cipherオブジェクトを生成し、暗号化キーオブジェクトで初期化
            // 暗号化キーは、鍵長を16バイト・24バイト・32バイトのいずれかに設定する必要がある
            Cipher cipher = Cipher.getInstance(encryptAlgorithm);
            cipher.init(Cipher.ENCRYPT_MODE, key);
            // 引数の文字列を暗号化
            byte[] byteResult = cipher.doFinal(data.getBytes(encryptCharset));
            // Base64へエンコードし、暗号化文字列を返却
            retVal = Base64.encodeBase64String(byteResult);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return retVal;
    }

    /**
     * 引数のオブジェクトをJSON文字列に変換する
     * @param data オブジェクトのデータ
     * @return 変換後JSON文字列
     */
    private String getJsonData(Object data){
        String retVal = null;
        ObjectMapper objectMapper = new ObjectMapper();
        try{
            retVal = objectMapper.writeValueAsString(data);
        } catch (JsonProcessingException e) {
            System.err.println(e);
        }
        return retVal;
    }

}

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-api-aes/demo_server

上記プログラムを作成し、Spring Bootアプリケーションを起動し、「http://localhost:8085/getUserDataList」にアクセスした結果は以下の通り。
サンプルプログラムの実行結果_サーバー側
名前(name)、性別(sex)、メモ(memo)を暗号化しているので、その内容が暗号化されていることが確認できる。

クライアント側のプログラムの作成と実行

作成したクライアント側(demo)サンプルプログラムの構成は以下の通り。

なお、上記の赤枠は、前提条件のプログラムから変更したプログラムである。

build.gradleの内容は以下の通りで、暗号化を行うための設定を追加している。

plugins {
	id 'org.springframework.boot' version '2.1.7.RELEASE'
	id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	//lombokの設定を追加
	compileOnly 'org.projectlombok:lombok:1.18.10'
	annotationProcessor 'org.projectlombok:lombok:1.18.10'
	//暗号化を行うための設定を追加
	compile group: 'commons-codec', name: 'commons-codec', version: '1.8'
}

application.ymlの内容は以下の通りで、暗号化に必要な項目を追加している。 暗号化キー(key)は、サーバー側と同じ文字列を設定している。

# ポート番号:8084で起動
server:
  port: 8084
# 暗号化項目を設定
encrypt:
  algorithm: AES
  charset: utf-8
  key: 1234567890abcdef

コントローラクラスの内容は以下の通りで、decryptメソッドでAESで暗号化した内容を復号化している。

package com.example.demo;

import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.client.RestTemplate;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.List;

@Controller
public class DemoController {

    /**
     * 暗号化アルゴリズム
     */
    @Value("${encrypt.algorithm}")
    private String encryptAlgorithm;

    /**
     * 暗号化時に利用する文字コード
     */
    @Value("${encrypt.charset}")
    private String encryptCharset;

    /**
     * 暗号化時に利用する秘密鍵
     */
    @Value("${encrypt.key}")
    private String encrypyKey;

    /**
     * RestTemplateオブジェクト
     */
    @Autowired
    private RestTemplate restTemplate;

    /**
     * ユーザーデータを取得し、初期表示画面に遷移する
     * @param model Modelオブジェクト
     * @return 初期表示画面へのパス
     */
    @GetMapping("/")
    public String index(Model model){
        // ユーザーデータリストをAPIで取得する
        ResponseEntity<List<UserData>> response = restTemplate.exchange(
                "http://localhost:8085/getUserDataList", HttpMethod.GET,
                null, new ParameterizedTypeReference<List<UserData>>() {});
        List<UserData> userDataList = response.getBody();
        // 各ユーザーデータを編集し、Modelに設定する
        for(UserData userData : userDataList){
            // 名前・メモ・性別・生年月日を復号化
            userData.setName(decrypt(userData.getName()));
            userData.setMemo(decrypt(userData.getMemo()));
            userData.setSex(decrypt(userData.getSex()));
        }
        model.addAttribute("userDataList", userDataList);
        return "index";
    }

    /**
     * 引数の文字列を復号化する
     * @param data 任意の文字列
     * @return 復号化後の文字列
     */
    private String decrypt(String data){
        if(StringUtils.isEmpty(data)){
            return "";
        }
        String retVal = null;
        try {
            // 引数の文字列をBase64へデコード
            byte[] byteText = Base64.decodeBase64(data);
            // 暗号化キーオブジェクトを生成
            SecretKeySpec key = new SecretKeySpec(
                    encrypyKey.getBytes(encryptCharset), encryptAlgorithm);
            // Cipherオブジェクトを生成し、暗号化キーオブジェクトで初期化
            // 暗号化キーは、鍵長を16バイト・24バイト・32バイトのいずれかに設定する必要がある
            Cipher cipher = Cipher.getInstance(encryptAlgorithm);
            cipher.init(Cipher.DECRYPT_MODE, key);
            // 引数の文字列を復号化
            byte[] byteResult = cipher.doFinal(byteText);
            // 復号化後の文字列を返却
            retVal = new String(byteResult, encryptCharset);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return retVal;
    }
}

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-api-aes/demo

サーバー・クライアント両方のSpring Bootアプリケーションを起動し、「http:// (ホスト名):8084」とアクセスすると、以下の画面が表示されることが確認できる。
サンプルプログラムの実行結果_クライアント側

要点まとめ

  • Cipherクラスを利用すると、共通鍵暗号方式の1つであるAESを利用した処理を簡単に実装できる。
  • AESで利用する暗号化キーは、鍵長を16バイト・24バイト・32バイトのいずれかに設定し、暗号化/復号化で同じ暗号化キーを利用する必要がある。