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関数を呼び出すように修正している。
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テーブルからの取得結果をクラスの内容は以下の通り。
package com.example.demo;
import lombok.Data;
@Data
public class GetUserPassResult {
/** USER_PASSテーブルからの取得結果 */
private UserPass userPass;
}package com.example.demo;
import lombok.Data;
@Data
public class UserPass {
/** ID */
private String id;
/** パスワード */
private String pass;
/** パスワード(暗号化後) */
private String passEncrypted;
}さらに、メイン画面の内容は以下の通りで、「getUserPassの値を取得」ボタン押下により、コントローラクラスの「getUserPass」メソッドを呼び出すようにしている。
<!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」に変更し、必要なライブラリを追加している。
<!-- 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にアクセスするための設定を追加している。
# 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の認証の接続設定を登録する処理を追加すると共に、後述のサービスクラスを呼び出しを追加している。
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を利用しない場合と同じ内容になっている。
package com.example.mybatis.model;
import lombok.Data;
@Data
public class UserPass {
/** ID */
private String id;
/** パスワード */
private String pass;
/** パスワード(暗号化後) */
private String passEncrypted;
}package com.example.mybatis;
import org.apache.ibatis.annotations.Mapper;
import com.example.mybatis.model.UserPass;
@Mapper
public interface UserPassMapper {
UserPass selectOne();
}<?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テーブルにアクセスするサービスクラスの内容は、以下の通り。
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関数である。
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メソッドの引数・戻り値は以下のクラスで定義している。
package com.example.model;
public class GetUserPassParam {
@Override
public String toString() {
return "GetUserPassParam []";
}
}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に対する認証を行うための設定を追加すればよい。





