Spring Boot 2.0でSpring Cloud Config Serverをセキュリティ付きで作る

# 今回作るもの

<img width="1012" alt="スクリーンショット 2018-03-09 14.56.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/58650/7f233e86-d6f7-579f-8370-e47ee0183128.png">

curlとClient間、ClientとConfig Server間は、BASIC認証をかけています。本来はたぶんOAuth 2.0の方がいいんでしょうけど、それはまた別に機会に。

サンプルコードは[GitHub](https://github.com/MasatoshiTada/boot2-config-server-sample)にアップしてあります。

# 環境
- JDK 8
- Spring Boot 2.0.0.RELEASE
- Spring Cloud Finchley.M8

執筆時点では、Spring Cloud Finchleyはまだ正式リリース版が出ていません。今後、仕様が変わる可能性もあるので、注意してください。

Config Server自体については、[槙さんのブログ](https://blog.ik.am/entries/290)を参照してください。ただし、正式リリース前の記事なので、いくつか現在と違う点もあるので注意してください。

<!--------------------------------------------------------------------------->
# Configリポジトリの作成

リポジトリとしては、GitやSVNが使えます。今回はGitHubを使います。
URLはコチラ -> https://github.com/MasatoshiTada/app-config
フォークして使ってください！

## application.properties
全アプリケーション・全プロファイルで利用される設定です。

```application.properties
message=Hello application!
common=common
```

## application-xxx.properties
全アプリケーションで利用されますが、特定のプロファイルでのみ利用される設定です（"xxx"にはプロファイル名が入ります）。

```application-dev.properties
message=Hello application-dev!
common=common-dev
```

```application-qa.properties
message=Hello application-qa!
common=common-qa
```

## client.properties
名前が「client」であるアプリケーションでのみ、全プロファイルで利用される設定です。

```client.properties
message=Hello client!
server.port=8081
# 設定のリフレッシュで使用する/refreshエンドポイントを有効化する
management.endpoints.web.exposure.include=refresh
# Spring Securityのログレベルをtraceにして、内部の実行の様子を見やすくする
logging.level.org.springframework.security=trace
```

> 3行目の`management.endpoints.web.exposure.include=refresh`は、Spring Boot 2.0からの新しい設定です。Spring Boot 2.0では、この設定に記述されたエンドポイントのみ有効化されます（カンマ区切りで複数指定可）。

## client-xxx.properties
名前が「client」であるアプリケーションでのみ、特定のプロファイルでのみ利用される設定です（"xxx"にはプロファイル名が入ります）。

```client-dev.properties
message=Hello client-dev!
server.port=8082
```

```client-qa.properties
message=Hello client-qa!
server.port=8083
```

## foo.properties
名前が「foo」であるアプリケーションでのみ、全プロファイルで利用される設定です。

```foo.properties
message=Hello foo!
server.port=9001
```

## foo-xxx.properties
名前が「foo」であるアプリケーションでのみ、特定のプロファイルでのみ利用される設定です（"xxx"にはプロファイル名が入ります）。

```foo-dev.properties
message=Hello foo-dev!
server.port=9002
```

```foo-qa.properties
message=Hello foo-qa!
server.port=9003
```

## 設定の優先順位
別のファイルに同じ名前の設定値があった場合、下記の順で優先されます（上のものほど優先されます）。

1. アプリケーション名-プロファイル名.properties
2. アプリケーション名.properties
3. application-プロファイル名.properties
4. application.properties

<!--------------------------------------------------------------------------->
# Config Serverを作る

(1) [Spring Initializr](http://start.spring.io)で雛形を作りましょう。「Group」「Artifact」を適当に決め、「Dependencies」で「Config Server」「Security」を追加します。

![スクリーンショット 2018-03-09 13.08.27.png](https://qiita-image-store.s3.amazonaws.com/0/58650/9ec7f5a0-8b8b-0c9b-1764-c25249effad8.png)


(2) main()メソッドがあるクラスに`@EnableConfigServer`を付加します。

```java
@SpringBootApplication
@EnableConfigServer // コレを付ける
public class ConfigServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}
```

(3) Spring Security用のJava Configクラスを作ります。任意のユーザー名・パスワードでユーザーを1つ作り、BASIC認証を有効化します。

```java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // "client"というユーザーを作成する
        // 本番環境ではNoOpPasswordEncoderを使わず、代わりにBCryptPasswordEncoderを使ってください
        auth.inMemoryAuthentication()
                .passwordEncoder(NoOpPasswordEncoder.getInstance())
                .withUser("client").password("password").roles("CLIENT");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // BASIC認証を有効化して、全URLを認証済みでないとアクセスできないようにする
        http.httpBasic();
        http.authorizeRequests()
                .anyRequest().authenticated();
    }
}
```

>コメントにも書いてありますが、NoOpPasswordEncoderはパスワードをまったくエンコードしないため、本番では使わないでください（既にSpring Security 5では非推奨になっています）。今回はあくまで学習用として使っています。

(4) application.propertiesに設定を記述します。

```properties
# GitリポジトリのURL
spring.cloud.config.server.git.uri=https://github.com/MasatoshiTada/app-config.git

# このConfig Serverのポート番号
server.port=8888
```

<!--------------------------------------------------------------------------->
# クライアントを作る

(1) [Spring Initializr](http://start.spring.io)で雛形を作りましょう。「Group」「Artifact」を適当に決め、「Dependencies」で「Web」「Config Client」「Actuator」「Security」を追加します。

![スクリーンショット 2018-03-09 13.09.59.png](https://qiita-image-store.s3.amazonaws.com/0/58650/1cb20e1f-d351-9ce2-5514-742f7f93ff35.png)


(2) Spring Security用のJava Configクラスを作ります。任意のユーザー名・パスワードでユーザーを2つ作ります（ロールも分けて、任意のロール名を付けてください）。

```java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // "ACTUATOR"と"USER"という2つのロールのユーザーを作る
        // 本番ではNoOpPasswordEncoderは使わないで！
        auth.inMemoryAuthentication()
                .passwordEncoder(NoOpPasswordEncoder.getInstance())
                .withUser("actuator").password("password").roles("ACTUATOR").and()
                .withUser("user").password("password").roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // BASIC認証の有効化
        http.httpBasic();
        // ActuatorエンドポイントはACTUATORロールのみアクセス可能、
        // その他はログイン済みであればどのロールでもアクセス可能
        http.authorizeRequests()
                .requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ACTUATOR")
                .anyRequest().authenticated();

        // 本番ではCSRFは無効化しないで！
        http.csrf().disable();
    }
}
```

> 今回はサンプルが複雑になりすぎるのを避けるために、CSRFを無効化しています。本番環境では無効化せず、curlでのアクセスの前に何らかの手段でトークンを取得するなどしてください。

(3) bootstrap.propertiesに設定を記述します。

```properties
# このクライアントのアプリケーション名
spring.application.name=client

# Config ServerのURL
spring.cloud.config.uri=http://localhost:8888/

# Config Serverにアクセスする際のユーザー名・パスワード
# （Config ServerのJava Configで作成したもの）
spring.cloud.config.username=client
spring.cloud.config.password=password
```

> bootstrap.propertiesは、起動時の設定を記述するもので、Spring Cloud環境でのみ使われます。

(4) Config Serverから取得した値を返す、適当なコントローラーを作ります。クラスには`@RefreshScope`を付加します。

```java
@RefreshScope  // "POST /actuator/refresh"されると、このクラスのインスタンスが破棄される
@RestController
@RequestMapping("/hello")
public class HelloController {

    private final String message;
    private final String common;

    // Config Serverから取得した値をコンストラクタで取得
    public HelloController(@Value("${message}") String message, @Value("${common}") String common) {
        this.message = message;
        this.common = common;
    }

    @GetMapping
    public Map<String, String> hello() {
        HashMap<String, String> map = new HashMap<>();
        map.put("message", message);
        map.put("common", common);
        return map;
    }
}
```

<!--------------------------------------------------------------------------->
# Config Server起動と、curlでの設定取得

(1) Config Serverのみ起動します。
(2) `/application/default`にアクセスすると、application.propertiesの値が取得できます。

```bash
$ curl -X GET -u client:password http://localhost:8888/application/default | jq
{
  "name": "application",
  "profiles": [
    "default"
  ],
  "label": null,
  "version": "e87b3c3ff0239394963e3bddf0a2982db5064339",
  "state": null,
  "propertySources": [
    {
      "name": "https://github.com/MasatoshiTada/app-config.git/application.properties",
      "source": {
        "message": "Hello application!",
        "common": "common"
      }
    }
  ]
}
```

(2) `/application/dev`にアクセスすると、application.propertiesとapplication-dev.propertiesの値が取得できます。

```bash
$ curl -X GET -u client:password http://localhost:8888/application/dev | jq
{
  "name": "application",
  "profiles": [
    "dev"
  ],
  "label": null,
  "version": "e87b3c3ff0239394963e3bddf0a2982db5064339",
  "state": null,
  "propertySources": [
    {
      "name": "https://github.com/MasatoshiTada/app-config.git/application-dev.properties",
      "source": {
        "message": "Hello application-dev!",
        "common": "common-dev"
      }
    },
    {
      "name": "https://github.com/MasatoshiTada/app-config.git/application.properties",
      "source": {
        "message": "Hello application!",
        "common": "common"
      }
    }
  ]
}
```

> 記載しませんが、`/application/qa`にアクセスすると、application.propertiesとapplication-qa.propertiesの値が取得できます。

(3) `/client/default`にアクセスすると、application.propertiesとclient.propertiesの値が取得できます。

```bash
$ curl -X GET -u client:password http://localhost:8888/client/default | jq
{
  "name": "client",
  "profiles": [
    "default"
  ],
  "label": null,
  "version": "e87b3c3ff0239394963e3bddf0a2982db5064339",
  "state": null,
  "propertySources": [
    {
      "name": "https://github.com/MasatoshiTada/app-config.git/client.properties",
      "source": {
        "message": "Hello client!",
        "server.port": "8081",
        "management.endpoints.web.exposure.include": "refresh",
        "logging.level.org.springframework.security": "trace"
      }
    },
    {
      "name": "https://github.com/MasatoshiTada/app-config.git/application.properties",
      "source": {
        "message": "Hello application!",
        "common": "common"
      }
    }
  ]
}
```

> 記載しませんが、`/foo/default`にアクセスすると、application.propertiesとfoo.propertiesの値が取得できます。


(4) `/client/dev`にアクセスすると、application.properties・application-dev.properties・client.properties・client-dev.propertiesの値が取得できます。

```bash
$ curl -X GET -u client:password http://localhost:8888/client/dev | jq
{
  "name": "client",
  "profiles": [
    "dev"
  ],
  "label": null,
  "version": "e87b3c3ff0239394963e3bddf0a2982db5064339",
  "state": null,
  "propertySources": [
    {
      "name": "https://github.com/MasatoshiTada/app-config.git/client-dev.properties",
      "source": {
        "message": "Hello client-dev!",
        "server.port": "8082"
      }
    },
    {
      "name": "https://github.com/MasatoshiTada/app-config.git/application-dev.properties",
      "source": {
        "message": "Hello application-dev!",
        "common": "common-dev"
      }
    },
    {
      "name": "https://github.com/MasatoshiTada/app-config.git/client.properties",
      "source": {
        "message": "Hello client!",
        "server.port": "8081",
        "management.endpoints.web.exposure.include": "refresh",
        "logging.level.org.springframework.security": "trace"
      }
    },
    {
      "name": "https://github.com/MasatoshiTada/app-config.git/application.properties",
      "source": {
        "message": "Hello application!",
        "common": "common"
      }
    }
  ]
}
```

> 記載しませんが、
> - `/client/qa`にアクセスすると、application.properties・application-qa.properties・client.properties・client-qa.propertiesの値が取得できます。
> - `/foo/dev`にアクセスすると、application.properties・application-dev.properties・foo.properties・foo-dev.propertiesの値が取得できます。
> - `/foo/qa`にアクセスすると、application.properties・application-qa.properties・foo.properties・foo-qa.propertiesの値が取得できます。

上記の確認が終わっても、Config Serverは起動したままにしておいてください。

<!--------------------------------------------------------------------------->
# クライアントからの値の取得

(1) 何のプロファイルも指定しないで、クライアントを起動してください。
(2) クライアントは起動時に、`spring.application.name`で指定したアプリケーション名・実行中のプロファイル名をもとに、前章で試したURLでConfig Serverにアクセスします（今回だと`/client/default`）。また、このアクセスの時にBASIC認証を使っています（ユーザー名`client`、パスワード`password`）。
(3) curlで`localhost:8081/hello`にアクセスしましょう。

```bash
$ curl -X GET -u user:password http://localhost:8081/hello | jq
{
  "common": "common",
  "message": "Hello client!"
}
```

"common"はapplication.propertiesの値です。"message"はapplication.propertiesにも定義されていますが、優先順位のルールからclient.propertiesの値が使われています。
起動しているポート番号8081も、client.propertiesの値ですね。

(4) クライアントをいったん停止し、devプロファイルで再起動します。プロファイルは「--」を付けたコマンドライン引数で指定しています（他にも、環境変数やJVMシステムプロパティなどで指定可能です）

```bash
client $ mvn clean package
client $ java -jar target/client-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev
client $ curl -X GET -u user:password http://localhost:8082/hello | jq
{
  "common": "common-dev",
  "message": "Hello client-dev!"
}
```

"common"はapplication-dev.propertiesの値です。"message"はclient-dev.propertiesの値です。
起動しているポート番号8082も、client-dev.propertiesの値ですね。

<!--------------------------------------------------------------------------->
# 実行中に値を変更する

(1) いま実行中のクライアントは停止し、プロファイル指定なしでもう一度起動してください。
(2) curlで`/hello`エンドポイントにアクセスします。messageの値は「Hello client!」です。

```bash
$ curl -X GET -u user:password http://localhost:8081/hello | jq
{
  "common": "common",
  "message": "Hello client!"
}
```

(3) GitHub上で、client.propertiesのmessageの値を書き換えてください。

```bash
message=Hello client!!!!!!!!!!!!!!!!!!!!!!!!!
```

(4) もう一度、curlで`/hello`エンドポイントにアクセスします。messageの値は「Hello client!」のままで変わっていません。まだ、GitHubで変更した値をクライアント側に反映していないためです。

```bash
$ curl -X GET -u user:password http://localhost:8081/hello | jq
{
  "common": "common",
  "message": "Hello client!"
}
```

(5) curlで`/actuator/refresh`にアクセスします。すると、クライアントがConfig Serverにアクセスし、Config ServerがGitHubから値を取得してクライアントに渡します。

```bash
$ curl -X POST -u actuator:password http://localhost:8081/actuator/refresh | jq
[
  "config.client.version",
  "message"
]
```

> `-u`で指定しているユーザー名が`actuator`であることに注意してください。これは、クライアントのJava Configで作成したユーザーで、`/actuator/**`にアクセスする権限を持つ`ACTUATOR`ロールのユーザーです。

(6) リフレッシュされた際に、`@RefreshScope`が付加されたBean（今回は`HelloController`）のインスタンスが破棄されます。インスタンスの再作成は、リフレッシュ後にそのBeanが初めて必要になった際（今回だとcurlで`/hello`にアクセスした際）に行われ、その際に変更後の新しい設定値が使われます。

```bash
$ curl -X GET -u user:password http://localhost:8081/hello | jq
{
  "common": "common",
  "message": "Hello client!!!!!!!!!!!!!!!!!!!!!!!!!"
}
```