Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

今回作るもの

スクリーンショット 2018-03-09 14.56.50.png

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

サンプルコードはGitHubにアップしてあります。

環境

  • JDK 8
  • Spring Boot 2.0.0.RELEASE
  • Spring Cloud Finchley.M8

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

Config Server自体については、槙さんのブログを参照してください。ただし、正式リリース前の記事なので、いくつか現在と違う点もあるので注意してください。

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で雛形を作りましょう。「Group」「Artifact」を適当に決め、「Dependencies」で「Config Server」「Security」を追加します。

スクリーンショット 2018-03-09 13.08.27.png

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

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

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

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

@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に設定を記述します。

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

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

クライアントを作る

(1) Spring Initializrで雛形を作りましょう。「Group」「Artifact」を適当に決め、「Dependencies」で「Web」「Config Client」「Actuator」「Security」を追加します。

スクリーンショット 2018-03-09 13.09.59.png

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

@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に設定を記述します。

# このクライアントのアプリケーション名
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を付加します。

@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の値が取得できます。

$ 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の値が取得できます。

$ 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の値が取得できます。

$ 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の値が取得できます。

$ 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にアクセスしましょう。

$ 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システムプロパティなどで指定可能です)

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!」です。

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

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

message=Hello client!!!!!!!!!!!!!!!!!!!!!!!!!

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

$ 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から値を取得してクライアントに渡します。

$ 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にアクセスした際)に行われ、その際に変更後の新しい設定値が使われます。

$ curl -X GET -u user:password http://localhost:8081/hello | jq
{
  "common": "common",
  "message": "Hello client!!!!!!!!!!!!!!!!!!!!!!!!!"
}
suke_masa
Java / Spring / Microservices / Kubernetes(CKAD) / IntelliJ IDEA
https://www.casareal.co.jp/ls
casareal
システム開発/評価・検証支援/品質改善支援サービスと現場に即した実践的なIT研修サービスを提供しています。
https://www.casareal.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away