はじめに
OAuth2.0の認証を行うリソースサーバーのコントローラーをテストする際に、Spring Boot 2.5.X で発生するエラーを踏んでしまい、解消に時間がかかったのでメモ。
REST APIのコントローラー部分のみのテストをしたかったので@WebMvcTest
を使用。
spring-security-testの@WithMockUser
を使用してテストしようとしたところ、テスト実行時にBeanCreationExceptionが発生。
エラー詳細
以下のログが出力されました。
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.5.1)
2022-04-27 21:41:51.414 INFO 21628 --- [ main] c.e.s.c.SampleRestControllerTest : Starting SampleRestControllerTest using Java 11.0.13 on chaffstun-PC with PID 21628 (started by chaffstun in C:\Users\chaffstun\Desktop\springsecurityoauth2demo\springsecurityoauth2demo)
2022-04-27 21:41:51.415 INFO 21628 --- [ main] c.e.s.c.SampleRestControllerTest : No active profile set, falling back to default profiles: default
2022-04-27 21:41:52.029 WARN 21628 --- [ main] o.s.w.c.s.GenericWebApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: 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 org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jwtDecoderByIssuerUri' defined in class path resource [org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration$JwtDecoderConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.oauth2.jwt.JwtDecoder]: Factory method 'jwtDecoderByIssuerUri' threw exception; nested exception is java.lang.IllegalArgumentException: Unable to resolve the Configuration with the provided Issuer of "https://idp.example.com/issuer"
2022-04-27 21:41:52.031 INFO 21628 --- [ main] ConditionEvaluationReportLoggingListener :
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2022-04-27 21:41:52.037 ERROR 21628 --- [ main] o.s.boot.SpringApplication : Application run failed
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 org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jwtDecoderByIssuerUri' defined in class path resource [org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration$JwtDecoderConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.oauth2.jwt.JwtDecoder]: Factory method 'jwtDecoderByIssuerUri' threw exception; nested exception is java.lang.IllegalArgumentException: Unable to resolve the Configuration with the provided Issuer of "https://idp.example.com/issuer"
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:658) ~[spring-beans-5.3.8.jar:5.3.8]
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:486) ~[spring-beans-5.3.8.jar:5.3.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1334) ~[spring-beans-5.3.8.jar:5.3.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1177) ~[spring-beans-5.3.8.jar:5.3.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:564) ~[spring-beans-5.3.8.jar:5.3.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:524) ~[spring-beans-5.3.8.jar:5.3.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.8.jar:5.3.8]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spr
以下略
ログよりJwtDecoderのインスタンスを生成するため、jwtDecoderByIssuerUriへアクセスしようとしているみたいですね。
実装例
検証にあたって作成した実装例。
依存関係
Spring Bootは2.5.9を使用。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>5.6.3</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.5.1</version>
</dependency>
</dependencies>
コントローラー
GETメソッドでhelloを返却するだけのものを用意。
package com.example.springsecurityoauth2demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SampleRestController {
@GetMapping("/")
public String hello() {
return "hello";
}
}
リソースサーバーの設定クラス
accessロールを持つ場合にアクセスを許可する。
クライアントクレデンシャルの場合を想定。
package com.example.springsecurityoauth2demo.controller;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.mvcMatchers("/")
.hasRole("access")
.anyRequest().authenticated())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}
}
application.yml
issuer-uriにアクセストークンの検証先を設定する。
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
テストクラス
本来であれば以下のテストで問題ないはずだったが、BeanCreationExceptionが発生してしまう。
ちなみにSpring Boot 2.6.0以降は修正されていて以下のテスト問題なく実行され、結果もグリーンになる。
package com.example.springsecurityoauth2demo.controller;
import static org.hamcrest.CoreMatchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(SampleRestController.class)
class SampleRestControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(roles = "access")
void accessロールを持つリクエストは許可される() throws Exception {
this.mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("hello")));
}
}
対処方法
はじめはテストの記載方法が間違っているのかもしれないと思い遠回りをしてしまったが、参考資料をもとにたどり着いた解消方法は以下の通り。
修正後のテストクラス
package com.example.springsecurityoauth2demo.controller;
import static org.hamcrest.CoreMatchers.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@WebMvcTest(SampleRestController.class)
class SampleRestControllerTest {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext context;
@MockBean
private JwtDecoder jwtDecoder;
@BeforeEach
void 前準備() {
this.mockMvc = MockMvcBuilders
.webAppContextSetup(context) // JwtDecoderのモックを適用したWebApplicationContextを設定
.apply(springSecurity())
.build();
}
@Test
@WithMockUser(roles = "access")
void accessロールを持つリクエストは許可される() throws Exception {
this.mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("hello")));
}
}
この実装でも問題ありませんが、Junit4の書き方が混ざっているようだったのでJunit5っぽく?書けないか試行錯誤してみると、以下の記載でも問題なく動作しました。
Junit5っぽく修正したテストクラス
package com.example.springsecurityoauth2demo.controller;
import static org.hamcrest.CoreMatchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(SampleRestController.class)
class SampleRestControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private JwtDecoder jwtDecoder;
@Test
@WithMockUser(roles = "access")
void accessロールを持つリクエストは許可される() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("hello")));
}
}
単純にJwtDecoderをMockBeanにしてやれば問題ありませんでした。
記事を書く際にエラーログをより詳細に確認しましたが、ログの内容からも上記の発想になりますね。
ソース
サンプルはこちらに置いてます。
おわりに
検証のためSpringInitalizrでサンプルを作成したところ、Spring Bootのバージョンが2.6.7になっていて普通にテストが通ったので少し焦りました笑。
さらに調べてみるとSpring Boot2.5.Xでしか発生していなかったので運が悪かったなと。