Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その26 )( Gradle を 4.10.2 → 4.10.3 へ、Spring Boot を 2.0.6 → 2.0.7 へバージョンアップする。。。が Spring Security の bug のため Spring Boot は 2.0.6 へ戻す )
概要
記事一覧はこちらです。
- 今回の手順で確認できるのは以下の内容です。
- Gradle を 4.10.2 → 4.10.3 へ、Spring Boot を 2.0.x 系の最新バージョンである 2.0.7 へバージョンアップします。。。が Spring Security 5.0.10.RELEASE の bug で build が通らなかったので Spring Boot は 2.0.6 のままにします。
- ライブラリは出来るだけ最新バージョンにします。
参照したサイト・書籍
Gradle Build Tool - Releases
https://gradle.org/releases/Spring Security 5.0 解剖速報
https://www.slideshare.net/TakuyaIwatsuka/spring-security5reportAuthenticationFailureBadCredentialsEvent published twice
https://github.com/spring-projects/spring-security/issues/6281
目次
- gradle を 4.10.2 → 4.10.3 へバージョンアップする
- build.gradle を変更する
- AuthenticationFailureBadCredentialsEvent が2回発生する原因を調査する
- build.gradle を変更する2
- 最後に
手順
gradle を 4.10.2 → 4.10.3 へバージョンアップする
build.gradle の wrapper タスクの記述を以下のように変更します。
wrapper {
gradleVersion = "4.10.3"
distributionType = Wrapper.DistributionType.ALL
}
gradleVersion = "4.10.2"
→gradleVersion = "4.10.3"
に変更します。
コマンドプロンプトから gradlew wrapper --gradle-version=4.10.3
、gradlew --version
コマンドを実行します。
gradle/wrapper/gradle-wrapper.properties は以下の内容になります。
distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists
Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新した後、clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します(画面キャプチャはなし)。
build.gradle を変更する
build.gradle の以下の点を変更します。
buildscript { ext { group "ksbysample" version "2.0.7-RELEASE" } repositories { mavenCentral() maven { url "https://repo.spring.io/release/" } maven { url "https://plugins.gradle.org/m2/" } } } plugins { id "java" id "eclipse" id "idea" id "org.springframework.boot" version "2.0.7.RELEASE" id "io.spring.dependency-management" version "1.0.6.RELEASE" id "groovy" id "checkstyle" id "com.github.spotbugs" version "1.6.8" id "pmd" id "net.ltgt.errorprone" version "0.0.16" id "de.undercouch.download" version "3.4.3" id "com.gorylenko.gradle-git-properties" version "2.0.0-beta1" } sourceCompatibility = 1.8 targetCompatibility = 1.8 wrapper { gradleVersion = "4.10.3" distributionType = Wrapper.DistributionType.ALL } [compileJava, compileTestGroovy, compileTestJava]*.options*.encoding = "UTF-8" [compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = [ "-Xlint:all,-options,-processing,-path" , "-Xep:RemoveUnusedImports:WARN" , "-Xep:InsecureCryptoUsage:OFF" , "-Xep:ParameterName:OFF" ] // for Doma 2 // JavaクラスとSQLファイルの出力先ディレクトリを同じにする processResources.destinationDir = compileJava.destinationDir // コンパイルより前にSQLファイルを出力先ディレクトリにコピーするために依存関係を逆転する compileJava.dependsOn processResources springBoot { buildInfo() } idea { module { inheritOutputDirs = false outputDir = file("$buildDir/classes/main/") } } configurations { // for Doma 2 domaGenRuntime } checkstyle { configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml") toolVersion = "8.16" sourceSets = [project.sourceSets.main] } spotbugs { toolVersion = "3.1.10" ignoreFailures = true effort = "max" spotbugsTest.enabled = false } tasks.withType(com.github.spotbugs.SpotBugsTask) { reports { xml.enabled = false html.enabled = true } } pmd { toolVersion = "6.10.0" sourceSets = [project.sourceSets.main] ignoreFailures = true consoleOutput = true ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml") ruleSets = [] } repositories { mavenCentral() } dependencyManagement { imports { // mavenBom は以下の URL のものを使用する // https://repo.spring.io/release/org/springframework/boot/spring-boot-starter-parent/2.0.7.RELEASE/ // bomProperty に指定可能な property は以下の URL の BOM に記述がある // https://repo.spring.io/release/org/springframework/boot/spring-boot-dependencies/2.0.7.RELEASE/spring-boot-dependencies-2.0.7.RELEASE.pom mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) { // Spring Boot の BOM に定義されているバージョンから変更する場合には、ここに以下のように記述する // bomProperty "thymeleaf.version", "3.0.9.RELEASE" bomProperty "groovy.version", "2.5.4" } } } dependencies { def jdbcDriver = "org.postgresql:postgresql:42.2.5" def spockVersion = "1.2-groovy-2.5" def domaVersion = "2.20.0" def lombokVersion = "1.18.4" def errorproneVersion = "2.3.1" def powermockVersion = "2.0.0-RC.4" def spotbugsVersion = "3.1.10" // dependency-management-plugin によりバージョン番号が自動で設定されるもの // Appendix F. Dependency versions ( https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-dependency-versions.html ) 参照 implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-thymeleaf") { exclude group: "org.codehaus.groovy", module: "groovy" } implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity5") implementation("org.thymeleaf.extras:thymeleaf-extras-java8time") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-freemarker") implementation("org.springframework.boot:spring-boot-starter-mail") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-data-redis") implementation("org.springframework.boot:spring-boot-starter-amqp") implementation("org.springframework.boot:spring-boot-starter-actuator") runtimeOnly("org.springframework.boot:spring-boot-devtools") implementation("org.springframework.session:spring-session-core") implementation("org.springframework.session:spring-session-data-redis") implementation("org.springframework.retry:spring-retry") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") implementation("org.apache.commons:commons-lang3") implementation("org.codehaus.janino:janino") implementation("io.micrometer:micrometer-registry-prometheus") implementation("redis.clients:jedis") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") testImplementation("org.yaml:snakeyaml") testImplementation("org.mockito:mockito-core") runtimeOnly("org.springframework.boot:spring-boot-properties-migrator") // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの runtimeOnly("${jdbcDriver}") implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2") implementation("org.simpleframework:simple-xml:2.7.1") implementation("com.univocity:univocity-parsers:2.7.6") implementation("com.google.guava:guava:27.0.1-jre") implementation("org.flywaydb:flyway-core:5.2.4") testImplementation("org.dbunit:dbunit:2.6.0") testImplementation("com.icegreen:greenmail:1.5.9") testImplementation("org.assertj:assertj-core:3.11.1") testImplementation("com.jayway.jsonpath:json-path:2.4.0") testImplementation("org.jsoup:jsoup:1.11.3") testImplementation("cglib:cglib-nodep:3.2.10") testImplementation("org.spockframework:spock-core:${spockVersion}") testImplementation("org.spockframework:spock-spring:${spockVersion}") // for lombok annotationProcessor("org.projectlombok:lombok:${lombokVersion}") compileOnly("org.projectlombok:lombok:${lombokVersion}") testCompileOnly("org.projectlombok:lombok:${lombokVersion}") // for Doma annotationProcessor("org.seasar.doma:doma:${domaVersion}") implementation("org.seasar.doma:doma:${domaVersion}") domaGenRuntime("org.seasar.doma:doma-gen:${domaVersion}") domaGenRuntime("${jdbcDriver}") // for Error Prone ( http://errorprone.info/ ) errorprone("com.google.errorprone:error_prone_core:${errorproneVersion}") compileOnly("com.google.errorprone:error_prone_annotations:${errorproneVersion}") // PowerMock testImplementation("org.powermock:powermock-module-junit4:${powermockVersion}") testImplementation("org.powermock:powermock-api-mockito2:${powermockVersion}") // for SpotBugs compileOnly("com.github.spotbugs:spotbugs:${spotbugsVersion}") compileOnly("net.jcip:jcip-annotations:1.0") compileOnly("com.github.spotbugs:spotbugs-annotations:${spotbugsVersion}") testImplementation("com.google.code.findbugs:jsr305:3.0.2") } ..........
- buildscript block 内で
version "2.0.6-RELEASE"
→version "2.0.7-RELEASE"
に変更します。 - plugins block の以下の点を変更します。
id "org.springframework.boot" version "2.0.6.RELEASE"
→id "org.springframework.boot" version "2.0.7.RELEASE"
id "com.github.spotbugs" version "1.6.5"
→id "com.github.spotbugs" version "1.6.8"
- checkstyle タスクで
toolVersion = "8.14"
→toolVersion = "8.16"
に変更します。 - spotbugs タスクで
toolVersion = "3.1.8"
→toolVersion = "3.1.10"
に変更します。 - pmd タスクで
toolVersion = "6.9.0"
→toolVersion = "6.10.0"
に変更します。 - dependencies block の以下の点を変更します。
def domaVersion = "2.19.3"
→def domaVersion = "2.20.0"
def spotbugsVersion = "3.1.8"
→def spotbugsVersion = "3.1.10"
implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity4")
→implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity5")
。Tomcat 起動時にAuto-configuration for thymeleaf-extras-springsecurity4 is deprecated in favour of thymeleaf-extras-springsecurity5
という WARN ログが出力されていました。implementation("com.google.guava:guava:27.0-jre")
→implementation("com.google.guava:guava:27.0.1-jre")
testImplementation("com.icegreen:greenmail:1.5.8")
→testImplementation("com.icegreen:greenmail:1.5.9")
testImplementation("cglib:cglib-nodep:3.2.9")
→testImplementation("cglib:cglib-nodep:3.2.10")
Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
clean タスク実行 → Rebuild Project 実行 → build タスクを実行してみると、テストが1件失敗しました。
もう少し詳細な情報が欲しいので、Project Tool Window で src/test を選択した後コンテキストメニューを表示して「Run 'All Tests'」を選択します。
テストが失敗したのは「ログインを5回失敗すればアカウントはロックされる」のテストで、ログインの失敗回数が1回だけのはずが2回カウントされているためでした。
debug 実行してみるとログイン前は失敗回数が 0 回ですが、
1度ログインに失敗しただけなのに失敗回数が2回になっていました。
もう少し調査して分かったことは ksbysample.webapp.lending.security.AuthenticationFailureBadCredentialsEventListener#onApplicationEvent が2回呼び出されている(AuthenticationFailureBadCredentialsEvent が2回発生している)ということでした。build.gradle を変更前に戻して試してみると AuthenticationFailureBadCredentialsEventListener#onApplicationEvent は1回しか呼び出されません。
AuthenticationFailureBadCredentialsEvent が2回発生する原因を調査する
2回実行されるのが問題と分かっている ksbysample.webapp.lending.security.AuthenticationFailureBadCredentialsEventListener#onApplicationEvent メソッドの最初の位置に breakpoint を設定してから、「ログインを5回失敗すればアカウントはロックされる」のテストを debug 実行して、breakpoint で止まった後に IntelliJ IDEA の Debug Window でスタックトレースをたどって通過している位置を確認しながら何が起きているのかを調べます。
そうして分かったことは、まず org.springframework.security.authentication.ProviderManager#authenticate が呼び出されますが、この時は for (AuthenticationProvider provider : getProviders()) { ... }
では例外が発生せず lastException も null のままです。getProviders()
で AnonymousAuthenticationProvider, RememberMeAuthenticationProvider が返ってきますが、この2つは if (!provider.supports(toTest)) { continue; }
の処理で continue されます。
その下に if (result == null && parent != null) { ... }
という if 文がありますが、parent が null ではなく ProviderManager のインスタンスがセットされているので、その中の result = parentResult = parent.authenticate(authentication);
が実行されます。
org.springframework.security.authentication.ProviderManager#authenticate が呼び出されてますが、今度は for (AuthenticationProvider provider : getProviders()) { ... }
で例外が発生して lastException に org.springframework.security.authentication.BadCredentialsException
のインスタンスがセットされます。getProviders()
で DaoAuthenticationProvider が3つ返ってきますが(3つは何かおかしいので後で見直します)、全て result = provider.authenticate(authentication);
が実行されて AuthenticationException が発生します。
その下の if 文3つはどれも条件が一致せず、次の prepareException(lastException, authentication);
が実行されます。
org.springframework.security.authentication.ProviderManager#prepareException の中の eventPublisher.publishAuthenticationFailure(ex, auth);
が実行されると eventPublisher
に DefaultAuthenticationEventPublisher のインスタンスがセットされているので AuthenticationFailureBadCredentialsEvent が発生して ksbysample.webapp.lending.security.AuthenticationFailureBadCredentialsEventListener#onApplicationEvent が呼び出されてログインの失敗回数が +1 されます。
その後で org.springframework.security.authentication.ProviderManager#authenticate の最後の throw lastException;
が実行されると、最初の org.springframework.security.authentication.ProviderManager#authenticate に戻り、org.springframework.security.authentication.BadCredentialsException の例外が throw されているので catch (AuthenticationException e) { lastException = e; }
の処理が実行されて lastException に org.springframework.security.authentication.BadCredentialsException
のインスタンスがセットされます。
その下の if 文2つはどれも条件が一致せず、次の prepareException(lastException, authentication);
が実行されます。
org.springframework.security.authentication.ProviderManager#prepareException の中の eventPublisher.publishAuthenticationFailure(ex, auth);
が実行されると先程と同じく eventPublisher
に DefaultAuthenticationEventPublisher のインスタンスがセットされているので AuthenticationFailureBadCredentialsEvent が発生して ksbysample.webapp.lending.security.AuthenticationFailureBadCredentialsEventListener#onApplicationEvent が呼び出されてログインの失敗回数が +1 されます。
Spring Boot を 2.0.6 → 2.0.7 へ上げる前に戻して何が違うのか確認してみたところ、2回目の org.springframework.security.authentication.ProviderManager#prepareException の中の eventPublisher.publishAuthenticationFailure(ex, auth);
が実行された時に eventPublisher
にセットされているのが ProviderManager$NullEventPublisher のインスタンスで、この場合 ksbysample.webapp.lending.security.AuthenticationFailureBadCredentialsEventListener#onApplicationEvent が呼び出されていませんでした(ログインの失敗回数も +1 されません)。
なぜ eventPublisher
にセットされているインスタンスが変わるのか Spring Security の 5.0.9.RELEASE と 5.0.10.RELEASE の差異を追ってみると、org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#getHttp で authenticationBuilder.authenticationEventPublisher(eventPublisher);
という行が追加されていたことが原因でした。
変更が入った commit は Set AuthenticationEventPublisher on each AuthenticationManagerBuilder で、Fixes gh-6009
のリンクをクリックすると AuthenticationSuccessEvent not published for oauth2Login() #6009 の Issue へ飛んで、更に別の AuthenticationFailureBadCredentialsEvent published twice #6281 の Issue が見つかりました。修正されていて次のバージョンに入るようです。
build が通らなくなるので Spring Boot は 2.0.6 に戻します。Gradle やライブラリ関連はバージョンアップしたままにします。
build.gradle を変更する2
build.gradle の以下の点を変更します。
buildscript { ext { group "ksbysample" version "2.0.6-RELEASE" } repositories { mavenCentral() maven { url "https://repo.spring.io/release/" } maven { url "https://plugins.gradle.org/m2/" } } } plugins { id "java" id "eclipse" id "idea" id "org.springframework.boot" version "2.0.6.RELEASE" id "io.spring.dependency-management" version "1.0.6.RELEASE" id "groovy" id "checkstyle" id "com.github.spotbugs" version "1.6.8" id "pmd" id "net.ltgt.errorprone" version "0.0.16" id "de.undercouch.download" version "3.4.3" id "com.gorylenko.gradle-git-properties" version "2.0.0-beta1" } ..........
- buildscript block 内で
version "2.0.7-RELEASE"
→version "2.0.6-RELEASE"
に戻します。 - plugins block の以下の点を変更します。
id "org.springframework.boot" version "2.0.7.RELEASE"
→id "org.springframework.boot" version "2.0.6.RELEASE"
Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると "BUILD SUCCESSFUL" のメッセージが出力されました。
最後に
2.0 系の 2.0.7 とかなり後のバージョンなのでバージョンアップも簡単にできるはずと考えていましたが、そんなことはありませんでした! そうか、こんなこともあるんですね。。。
履歴
2019/01/05
初版発行。