SpringSecurity5.1.0でOAuth2のRefreshTokenがサポートされたので、
使ってみようとしたらNoClassDefFoundErrorに遭遇したので、
調べた内容のメモを残しておきます。

発生したエラーがこちらです。

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springSecurityFilterChain' defined in class path resource [org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [javax.servlet.Filter]: Factory method 'springSecurityFilterChain' threw exception; nested exception is java.lang.NoClassDefFoundError: org/springframework/security/core/userdetails/UserDetailsPasswordService
        at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:590) ~[spring-beans-5.0.9.RELEASE.jar:5.0.9.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1247) ~[spring-beans-5.0.9.RELEASE.jar:5.0.9.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1096) ~[spring-beans-5.0.9.RELEASE.jar:5.0.9.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:535) ~[spring-beans-5.0.9.RELEASE.jar:5.0.9.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:495) ~[spring-beans-5.0.9.RELEASE.jar:5.0.9.RELEASE]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:317) ~[spring-beans-5.0.9.RELEASE.jar:5.0.9.RELEASE]

エラーを見れば、
5.1.0にしたはずが5.0.9のjar云々が出てきているので、
なんとなく想像できますが、jarの依存関係の問題です。

このエントリでは、
SpringBootでHelloWorldを作り、認証を組み込む流れで説明していきます。

SpringBootでHelloWorld

まず、以下のように3つファイルを作成して、単純なHelloWorldアプリを作ります。

  • build.gradle
  • src/main/java/sample/Application.java
  • src/main/java/sample/HelloController.java

build.gradle

plugins {
    id 'java'
    id 'io.spring.dependency-management' version '1.0.6.RELEASE'
    id 'org.springframework.boot' version '2.0.5.RELEASE'
}
repositories {
    jcenter()
}
dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
}

src/main/java/sample/Application.java

package sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

src/main/java/sample/HelloController.java

package sample;

import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.*;
import org.springframework.web.bind.annotation.*;

@RestController
public class HelloController {
  @RequestMapping("/")
  public String index() {
      return "Hello World!";
  }
}

上記のファイルを作成した後、
bootRunで実行して、
ブラウザでhttp://localhost:8080/にアクセスすると、
HelloWorldを表示したページを閲覧できます。

実行コマンド

$ gradle bootRun

SpringSecurityによる基本認証の追加

SpringSecurityでの基本認証を追加するため、
次のように、build.gradleにSpringSecurityを追加し、
その設定をWebSecurityConfigとして作成します。

build.gradle

plugins {
    id 'java'
    id 'io.spring.dependency-management' version '1.0.6.RELEASE'
    id 'org.springframework.boot' version '2.0.5.RELEASE'
}
repositories {
    jcenter()
}
dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
    compile 'org.springframework.security:spring-security-web:5.1.0.RELEASE'
    compile 'org.springframework.security:spring-security-config:5.1.0.RELEASE'
}

src/main/java/sample/WebSecurityConfig.java

package sample;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig implements WebMvcConfigurer {

  protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .anyRequest().authenticated()
        .and()
        .formLogin()
        .loginPage("/login")
        .permitAll();
  }

  @Bean
  public UserDetailsService userDetailsService() throws Exception {
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(
        User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build());
    return manager;
  }
}

この状態でbootRunするとエントリ冒頭のエラーが発生します。

以下のようにpackageの依存関係を見てみると、
org.springframework.security:spring-security-coreが
「5.0.8.RELEASE」に変更されていることがわかります。

$ gradle dependencies
※省略※
+--- org.springframework.security:spring-security-web:5.1.0.RELEASE
|    +--- org.springframework.security:spring-security-core:5.1.0.RELEASE -> 5.0.8.RELEASE
|    |    +--- org.springframework:spring-aop:5.0.9.RELEASE (*)
|    |    +--- org.springframework:spring-beans:5.0.9.RELEASE (*)
|    |    +--- org.springframework:spring-context:5.0.9.RELEASE (*)
|    |    +--- org.springframework:spring-core:5.0.9.RELEASE (*)
|    |    \--- org.springframework:spring-expression:5.0.9.RELEASE (*)
|    +--- org.springframework:spring-aop:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
|    +--- org.springframework:spring-beans:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
|    +--- org.springframework:spring-context:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
|    +--- org.springframework:spring-core:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
|    +--- org.springframework:spring-expression:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
|    \--- org.springframework:spring-web:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
\--- org.springframework.security:spring-security-config:5.1.0.RELEASE
     +--- org.springframework.security:spring-security-core:5.1.0.RELEASE -> 5.0.8.RELEASE (*)
     +--- org.springframework:spring-aop:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
     +--- org.springframework:spring-beans:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
     +--- org.springframework:spring-context:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
     \--- org.springframework:spring-core:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
※省略※

以下のURLなどを見てもらうと分かりますが。
SpringBoot 2.0.5.RELEASEが、SpringSecurityの5.0.8.RELEASEに依存していることが原因です。

https://mvnrepository.com/artifact/org.springframework.boot/spring-boot/2.0.5.RELEASE

Dependency management pluginの働き

前項のように、
「SpringBoot 2.0.5.RELEASE」の依存関係にあわせて、
org.springframework.security:spring-security-coreが
「5.0.8.RELEASE」に変更される仕組みなのですが。

これは、build.gradleにおまじない的に書いている
io.spring.dependency-management
が、担っています。

Dependency management plugin | GitHub
https://github.com/spring-gradle-plugins/dependency-management-plugin

これは自分の作成するプロジェクトで、
依存するライブラリのバージョンを個別に指定しなくても、
ライブラリの依存関係のメタ情報から解決してくれるというプラグインです。

言葉で書くとわかりにくいですが、
pluginsのところで、SpringBootのバージョンを書いておけば、

    id 'org.springframework.boot' version '2.0.5.RELEASE'

dependenciesのところで、
Spring関連のライブラリを取り込む時にバージョン指定する必要が無くなる。

    compile 'org.springframework.boot:spring-boot-starter-web'

ということを実現するプラグインです。

# 以下のブログに詳しく説明されているので、参考にしてください。
#
# Gradle で BOM を使いたいときには Spring チームの出している Dependency management plugin を使うのがよさそう | なにか作る
# http://create-something.hatenadiary.jp/entry/2015/05/08/063000

Dependency management pluginが、 spring-security-coreのバージョンを
spring-boot 2.0.5.RELEASEの依存関係から解決してしまうので、
spring-security-core 5.0.8.RELEASEに変更されてしまうという訳です。
(直接バージョン指定したspring-security-webは 5.1.0.RELEASEになる)

Dependency management pluginへのパラメータ指定

この依存バージョンの問題ですが、
Dependency management pluginのパラメータに、
バージョンを指定することで解決出来ます。

以下のようにbuild.gradleを変更します。

build.gradle

plugins {
    id 'java'
    id 'io.spring.dependency-management' version '1.0.6.RELEASE'
    id 'org.springframework.boot' version '2.0.5.RELEASE'
}
repositories {
    jcenter()
}
ext['spring-security.version'] = '5.1.0.RELEASE'
dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
    compile 'org.springframework.security:spring-security-web'
    compile 'org.springframework.security:spring-security-config'
}

ここでpackageの依存関係を見てみると、
org.springframework.security:spring-security-coreが
「5.1.0.RELEASE」となっていることが確認出来ます。

$ gradle dependencies
※省略※
+--- org.springframework.security:spring-security-web -> 5.1.0.RELEASE
|    +--- org.springframework.security:spring-security-core:5.1.0.RELEASE
|    |    +--- org.springframework:spring-aop:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
|    |    +--- org.springframework:spring-beans:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
|    |    +--- org.springframework:spring-context:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
|    |    +--- org.springframework:spring-core:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
|    |    \--- org.springframework:spring-expression:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
|    +--- org.springframework:spring-aop:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
|    +--- org.springframework:spring-beans:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
|    +--- org.springframework:spring-context:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
|    +--- org.springframework:spring-core:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
|    +--- org.springframework:spring-expression:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
|    \--- org.springframework:spring-web:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
\--- org.springframework.security:spring-security-config -> 5.1.0.RELEASE
     +--- org.springframework.security:spring-security-core:5.1.0.RELEASE (*)
     +--- org.springframework:spring-aop:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
     +--- org.springframework:spring-beans:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
     +--- org.springframework:spring-context:5.1.0.RELEASE -> 5.0.9.RELEASE (*)
     \--- org.springframework:spring-core:5.1.0.RELEASE -> 5.0.9.RELEASE (*)

この状態でbootRunすると、SpringSecurityのログイン機能が有効になります。

Dependency management pluginを使う場合で、
依存するライブラリのバージョンを上げたい時は、
dependenciesで直接バージョンを指定せずに
パラメータでバージョンを設定した方が、
関連ライブラリのバージョンが揃って問題が起こりにくいのではないかと思われます。

# 最も問題が起こりにくいのは、
# SpringBootのバージョンが上がって、
# 依存するライブラリの最新バージョンが取り込まれるのを待つことですが。

ちなみに、
spring-security.versionなどのパラメータは、
MavenのBOMなので、このあたりで定義されているプロパティが使用できます。
https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-dependencies/pom.xml