Always Encryptedで暗号化したカラムは、Always Encryptedにより暗号化されたカラムを復号化できる設定を追加すると共に、Azure Key Vaultに対する認証を行うための設定を追加することで、復号化した値を取得することができる。
今回は、Always Encryptedで暗号化したカラムを復号化し表示してみたので、そのサンプルプログラムを共有する。
前提条件
下記記事に従って、dbo.USER_PASSテーブルのカラム「pass_encrypted」を暗号化していること。
また、Azure Portal上に、以下の記事に従ってKey Vaultとサービスプリンシパルを作成済であること。
なお、サービスプリンシパルの、Key Vaultのアクセスポリシーが以下のようになっていること。
さらに、下記記事の実装が完了していること。
作成したサンプルプログラム(App Service側)の内容
作成したサンプルプログラム(App Service側)の構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。
コントローラクラスの内容は以下の通りで、後述のgetUserPass関数を呼び出すように修正している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.client.RestTemplate; import com.fasterxml.jackson.databind.ObjectMapper; @Controller public class DemoController { /** RestTemplateオブジェクト */ @Autowired private RestTemplate restTemplate; /** ObjectMapperオブジェクト */ @Autowired private ObjectMapper objectMapper; /** application.propertiesからdemoAzureFunc.urlBaseの値を取得 */ @Value("${demoAzureFunc.urlBase}") private String demoAzureFuncBase; /** * メイン画面を初期表示する. * @param model Modelオブジェクト * @return メイン画面 */ @GetMapping("/") public String index(Model model) { return "main"; } /** * 暗号化されたパスワードを取得する. * @param model Modelオブジェクト * @return メイン画面 */ @PostMapping("/getUserPass") public String getUserPass(Model model) { // Azure FunctionsのgetUserPass関数を呼び出すためのヘッダー情報を設定する HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); // Azure FunctionsのgetUserPass関数を呼び出すための引数を設定する MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers); // Azure FunctionsのgetUserPass関数を呼び出す ResponseEntity<String> response = restTemplate.exchange( demoAzureFuncBase + "getUserPass", HttpMethod.POST , entity, String.class); // Azure Functionsの呼出結果を、Modelオブジェクトに設定する try { GetUserPassResult getUserPassResult = objectMapper.readValue(response.getBody(), GetUserPassResult.class); if (getUserPassResult != null && getUserPassResult.getUserPass() != null) { model.addAttribute("getUserPass", getUserPassResult.getUserPass()); } } catch (Exception ex) { throw new RuntimeException(ex); } return "main"; } } |
また、先ほどのgetUserPass関数を呼び出した結果に関する、dbo.USER_PASSテーブルからの取得結果をクラスの内容は以下の通り。
1 2 3 4 5 6 7 8 9 10 11 | package com.example.demo; import lombok.Data; @Data public class GetUserPassResult { /** USER_PASSテーブルからの取得結果 */ private UserPass userPass; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package com.example.demo; import lombok.Data; @Data public class UserPass { /** ID */ private String id; /** パスワード */ private String pass; /** パスワード(暗号化後) */ private String passEncrypted; } |
さらに、メイン画面の内容は以下の通りで、「getUserPassの値を取得」ボタン押下により、コントローラクラスの「getUserPass」メソッドを呼び出すようにしている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <!DOCTYPE html> <html lang="ja" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>メイン画面</title> </head> <body> <form method="post" th:action="@{/getUserPass}"> <input type="submit" value="getUserPassの値を取得" /><br/><br/> <div th:if="${getUserPass}"> <span th:text="${getUserPass}">getUserPassで取得した値</span> </div> </form> </body> </html> |
その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/azure/tree/master/always-encrypted-decrypt/demoAzureApp
作成したサンプルプログラム(Azure Functions側)の内容
作成したサンプルプログラム(Azure Functions側)の構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。
<2021年4月13日 追記>
spring-cloud-function-dependenciesのバージョンは、2021年3月16日にリリースしたバージョン3.1.2を利用すると、1つのAzure Functions内に複数のファンクションを含む場合の不具合が解消できている。
その場合、Handlerクラスの継承するクラスを「AzureSpringBootRequestHandler」クラスから「FunctionInvoker」クラスに変更する。
spring-cloud-function-dependenciesの3.1.2を利用した実装サンプルは、以下の記事を参照のこと。
pom.xmlの追加内容は以下の通りで、Microsoft SQL Server 用 JDBC Driver(mssql-jdbc)のバージョンを「8.4.1.jre8」に変更し、必要なライブラリを追加している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <!-- SQL Serverを利用するための設定 --> <dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>8.4.1.jre8</version> </dependency> <dependency> <groupId>com.microsoft.azure</groupId> <artifactId>adal4j</artifactId> <version>1.6.5</version> </dependency> <dependency> <groupId>com.microsoft.rest</groupId> <artifactId>client-runtime</artifactId> <version>1.7.4</version> </dependency> <dependency> <groupId>com.microsoft.azure</groupId> <artifactId>azure-keyvault</artifactId> <version>1.2.4</version> </dependency> |
なお、Microsoft SQL Server 用 JDBC Driver(mssql-jdbc)に依存するライブラリについては、以下のサイトを参照のこと。
https://docs.microsoft.com/ja-jp/sql/connect/jdbc/feature-dependencies-of-microsoft-jdbc-driver-for-sql-server?view=sql-server-ver15
application.propertiesの内容は以下の通りで、DBのURLに、Always Encryptedで暗号化されたカラムの復号化を可能にする「columnEncryptionSetting=Enabled」という設定を追加すると共に、サービスプリンシパルを利用してKey Vaultにアクセスするための設定を追加している。
1 2 3 4 5 6 7 8 9 10 | # DB接続設定 spring.datasource.url=jdbc:sqlserver://azure-db-purinit.database.windows.net:1433;database=azureSqlDatabase;columnEncryptionSetting=Enabled #spring.datasource.url=jdbc:sqlserver://azure-db-purinit.database.windows.net:1433;database=azureSqlDatabase spring.datasource.username=purinit@azure-db-purinit spring.datasource.password=(DBのパスワード) spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver # Key Vaultへの設定 keyVaultClientId=cdbb5434-e455-4885-b2e6-911f78c4264b keyVaultClientKey=(サービスプリンシパルのパスワード) |
なお、上記設定のkeyVaultClientId、keyVaultClientKeyには、以下のサービスプリンシパル作成時のコマンド実行時に出力されたappId、passwordの値を指定している。
また、Azure Functionsのメインクラスは以下の通りで、オブジェクト生成後にKey Vaultの認証の接続設定を登録する処理を追加すると共に、後述のサービスクラスを呼び出しを追加している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | package com.example; import java.sql.SQLException; import java.util.HashMap; import java.util.Map; import java.util.function.Function; import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import com.example.model.GetUserPassParam; import com.example.model.GetUserPassResult; import com.example.service.GetUserPassService; import com.microsoft.sqlserver.jdbc.SQLServerColumnEncryptionAzureKeyVaultProvider; import com.microsoft.sqlserver.jdbc.SQLServerColumnEncryptionKeyStoreProvider; import com.microsoft.sqlserver.jdbc.SQLServerConnection; @SpringBootApplication public class DemoAzureFunction { /** keyVaultのクライアントID */ @Value("${keyVaultClientId}") private String keyVaultClientId; /** keyVaultのクライアントキー */ @Value("${keyVaultClientKey}") private String keyVaultClientKey; /** Key Vaultの認証の接続設定が終わっているかどうかを判定するフラグ */ private static boolean keyVaultProviderFlg = false; /** USER_PASSデータ取得サービスクラスのオブジェクト */ @Autowired private GetUserPassService getUserPassService; public static void main(String[] args) throws Exception { SpringApplication.run(DemoAzureFunction.class, args); } /** * Key Vaultの認証の接続設定を登録する * @throws SQLException SQL例外 */ @PostConstruct public void postConstruct() throws SQLException { if (!keyVaultProviderFlg) { SQLServerColumnEncryptionAzureKeyVaultProvider akvProvider = new SQLServerColumnEncryptionAzureKeyVaultProvider( keyVaultClientId, keyVaultClientKey); Map<String, SQLServerColumnEncryptionKeyStoreProvider> keyStoreMap = new HashMap<String, SQLServerColumnEncryptionKeyStoreProvider>(); keyStoreMap.put(akvProvider.getName(), akvProvider); SQLServerConnection.registerColumnEncryptionKeyStoreProviders(keyStoreMap); keyVaultProviderFlg = true; } } /** * USER_PASSテーブルのデータを取得し結果を返却する関数 * @return USER_PASSテーブルデータ取得サービスクラスの呼出結果 */ @Bean public Function<GetUserPassParam, GetUserPassResult> getUserPass() { return getUserPassParam -> getUserPassService.getUserPass(getUserPassParam); } } |
さらに、UserPassテーブルにアクセスするエンティティクラス・Mapperクラス・Mapper XMLの内容は以下の通りで、Always Encryptedを利用しない場合と同じ内容になっている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package com.example.mybatis.model; import lombok.Data; @Data public class UserPass { /** ID */ private String id; /** パスワード */ private String pass; /** パスワード(暗号化後) */ private String passEncrypted; } |
1 2 3 4 5 6 7 8 9 10 11 | package com.example.mybatis; import org.apache.ibatis.annotations.Mapper; import com.example.mybatis.model.UserPass; @Mapper public interface UserPassMapper { UserPass selectOne(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.mybatis.UserPassMapper"> <resultMap id="userPassResultMap" type="com.example.mybatis.model.UserPass" > <result column="id" property="id" jdbcType="VARCHAR" /> <result column="pass" property="pass" jdbcType="VARCHAR" /> <result column="passEncrypted" property="passEncrypted" jdbcType="VARCHAR" /> </resultMap> <select id="selectOne" resultMap="userPassResultMap"> SELECT id, pass, pass_encrypted AS passEncrypted FROM user_pass WHERE id = 1 </select> </mapper> |
また、UserPassテーブルにアクセスするサービスクラスの内容は、以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | package com.example.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.example.model.GetUserPassParam; import com.example.model.GetUserPassResult; import com.example.mybatis.UserPassMapper; @Service public class GetUserPassService { /** USER_PASSテーブルのデータを取得するMapperオブジェクト */ @Autowired private UserPassMapper userPassMapper; /** * USER_PASSテーブルのデータを取得し結果を返却する * @param getUserPassParam 検索条件Param * @return 結果情報オブジェクト */ public GetUserPassResult getUserPass(GetUserPassParam getUserPassParam) { GetUserPassResult getUserPassResult = new GetUserPassResult(); getUserPassResult.setUserPass(userPassMapper.selectOne()); return getUserPassResult; } } |
ハンドラークラスの内容は以下の通りで、Azure App Serviceのコントローラクラスから呼ばれるgetUserPass関数である。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | package com.example; import java.util.Optional; import org.springframework.cloud.function.adapter.azure.AzureSpringBootRequestHandler; import com.example.model.GetUserPassParam; import com.example.model.GetUserPassResult; import com.microsoft.azure.functions.ExecutionContext; import com.microsoft.azure.functions.HttpMethod; import com.microsoft.azure.functions.HttpRequestMessage; import com.microsoft.azure.functions.HttpResponseMessage; import com.microsoft.azure.functions.HttpStatus; import com.microsoft.azure.functions.annotation.AuthorizationLevel; import com.microsoft.azure.functions.annotation.FunctionName; import com.microsoft.azure.functions.annotation.HttpTrigger; public class GetUserPassHandler extends AzureSpringBootRequestHandler<GetUserPassParam, GetUserPassResult> { /** * HTTP要求に応じて、DemoAzureFunctionクラスのgetUserPassメソッドを呼び出し、 * その戻り値をボディに設定したレスポンスを返す * @param request リクエストオブジェクト * @param context コンテキストオブジェクト * @return レスポンスオブジェクト */ @FunctionName("getUserPass") public HttpResponseMessage execute( @HttpTrigger(name = "request", methods = HttpMethod.POST , authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request, ExecutionContext context) { // handleRequestメソッド内でDemoAzureFunctionクラスのgetUserPassメソッドを呼び出し、 // その戻り値をボディに設定したレスポンスを、JSON形式で返す return request.createResponseBuilder(HttpStatus.OK) .body(handleRequest(new GetUserPassParam(), context)) .header("Content-Type", "text/json").build(); } } |
さらに、Azure FunctionsのメインクラスのgetUserPassメソッドの引数・戻り値は以下のクラスで定義している。
1 2 3 4 5 6 7 8 9 10 | package com.example.model; public class GetUserPassParam { @Override public String toString() { return "GetUserPassParam []"; } } |
1 2 3 4 5 6 7 8 9 10 11 12 | package com.example.model; import com.example.mybatis.model.UserPass; import lombok.Data; @Data public class GetUserPassResult { /** USER_PASSテーブルからの取得結果 */ private UserPass userPass; } |
その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/azure/tree/master/always-encrypted-decrypt/demoAzureFunc
サンプルプログラムの実行結果
サンプルプログラムの実行結果は、以下の通り。
1) まずは、ローカル環境で、Always Encryptedによる暗号化を行わなかった場合の実行結果は、以下の通り。
App Service側のapplication.propertiesは以下の通り。
Azure Functions側のapplication.propertiesは以下の通り。
上記の状態で、ローカル環境でdemoAzureFuncアプリを「mvn azure-functions:run」コマンドで起動した後で、Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)」とアクセスし、「getUserPassの値を取得」ボタンを押下した実行結果は、以下の通り。
上記結果を確認すると、下記データがそのまま表示されることが確認できる。
2) 次に、ローカル環境で、Always Encryptedによる暗号化を行った場合の実行結果は、以下の通り。
App Service側のapplication.propertiesは以下の通り。
Azure Functions側のapplication.propertiesは以下の通り。
上記の状態で、ローカル環境でdemoAzureFuncアプリを「mvn azure-functions:run」コマンドで起動した後で、Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)」とアクセスし、「getUserPassの値を取得」ボタンを押下した実行結果は、以下の通り。
上記結果を確認すると、下記データが復号化され表示されることが確認できる。
3) さらに、Azure環境で、Always Encryptedによる暗号化を行った場合の実行結果は、以下の通り。
App Service側のapplication.propertiesは以下の通り。
Azure Functions側のapplication.propertiesは以下の通り。
上記状態で、「mvn azure-functions:deploy」コマンドによって、Azure Functions上にサンプルプログラムをデプロイする。
なお、Azure Functionsにデプロイする過程は、以下の記事の「Azure FunctionsへのSpring Bootを利用したJavaアプリケーションのデプロイ」を参照のこと。
また、 「mvn azure-webapp:deploy」コマンドによって、Azure App Service上にサンプルプログラムをデプロイする。
なお、Azure App Serviceにデプロイする過程は、以下の記事の「App ServiceへのSpring Bootを利用したJavaアプリケーションのデプロイ」を参照のこと。
その後、Azure App ServiceのURL「https://azureappdemoservice.azurewebsites.net/」とアクセスした場合の実行結果は、以下の通り。
なお、上記URLは、下記Azure App ServiceのURLから確認できる。
その後、「getUserPassの値を取得」ボタンを押下すると、以下の画面が表示され、Always Encryptedで暗号化したカラムが復号化し表示されることが確認できる。
要点まとめ
- Always Encryptedで暗号化したカラムを復号化するには、JDBC接続文字列に「columnEncryptionSetting=Enabled」を追加すると共に、サービスプリンシパルを経由してAzure Key Vaultに対する認証を行うための設定を追加すればよい。