📘

Springでリソースサーバのテストコード書く

2023/08/11に公開

spring-security-oauth2-resource-server を利用してリソースサーバを作成した際に、認可が必要なAPIのテストコードを書く方法です。

記事の環境

  • SpringBoot 3.1.2
  • WireMock(wiremock-jre8-standalone) 2.35.0

テスト対象となるコード

まずテスト対象のリソースサーバのコードになります。

APIとしてscopeにmessage.readがついているもののみを許可します。

HelloController.java
@RestController
@RequestMapping("/")
public class HelloController {
  @GetMapping
  @PreAuthorize("hasAuthority('SCOPE_message.read')")
  public String index(Principal principal) {
    return principal.getName();
  }
}

すべてのエンドポイントは認証必須とし、OAuth2のJWTを利用した認可を行うように設定します。

SecurityConfig
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
    return http.build();
  }
}

認可サーバはlocalhost:8080 に存在するものとします。

application.properties
spring.security.oauth2.resourceserver.jwt.issuer-uri: http://localhost:8080
server.port=8081

テストコード

テストとしては、認可サーバをモックすることでテスト可能にします。

まずJWTを生成する際の暗号を行うBeanを作成します。

SecurityTestConfig.java
@Configuration
public class SecurityTestConfig {
  @Bean
  public JWKSource<SecurityContext> jwkSource() {
    KeyPair keyPair = generateRsaKey();
    RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
    RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
    RSAKey rsaKey = new RSAKey.Builder(publicKey)
        .privateKey(privateKey)
        .keyID(UUID.randomUUID().toString())
        .build();
    JWKSet jwkSet = new JWKSet(rsaKey);
    return new ImmutableJWKSet<>(jwkSet);
  }

  private static KeyPair generateRsaKey() {
    KeyPair keyPair;
    try {
      KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
      keyPairGenerator.initialize(2048);
      keyPair = keyPairGenerator.generateKeyPair();
    }
    catch (Exception ex) {
      throw new IllegalStateException(ex);
    }
    return keyPair;
  }

  @Bean
  public JwtEncoder jwtEncoderGenerator(JWKSource<SecurityContext> jwkSource) {
    return new NimbusJwtEncoder(jwkSource);
  }
}

次はテストコードです。

HelloControllerTest.java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@WireMockTest(httpPort = 8080)
class HelloControllerTest {
  @Autowired
  private JWKSource<SecurityContext> jwtSource;
  @Autowired
  private JwtEncoder jwtEncoder;

  @Autowired
  private WebTestClient webTestClient;

  @Test
  void scopeが正しい場合はsubjectを返却() {
    stubFor(get("/.well-known/openid-configuration")
        .willReturn(okJson("""
            {
              "issuer": "http://localhost:8080",
              "jwks_uri": "http://localhost:8080/oauth2/jwks"
            }
            """)));

    JWKSelector selector = new JWKSelector(new JWKMatcher.Builder().build());
    JWKSet jwkSet = null;
    try {
      jwkSet = new JWKSet(this.jwtSource.get(selector, null));
    } catch (KeySourceException e) {
      throw new RuntimeException(e);
    }
    stubFor(get("/oauth2/jwks").willReturn(okJson(jwkSet.toString())));

    JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256).build();
    JwtClaimsSet claimsSet = JwtClaimsSet.builder()
        .subject("taro")
        .issuer("http://localhost:8080")
        .claim("scope", "message.read")
        .build();

    Jwt jwt = this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claimsSet));
    String token = jwt.getTokenValue();

    webTestClient.get()
        .uri("/")
        .headers(headers -> headers.setBearerAuth(token))
        .exchange()
        .expectStatus().isOk()
        .expectBody(String.class).isEqualTo("taro");
  }
}

わかりやすくするために1メソッドにまとめましたが、実際には共通化するなどして様々なテストケースで利用できるようにしてください。

重要な部分としてはJwtClaimsSet の生成箇所でsubjectを変更したり、scopeを設定したりすることで、テストケースごとに必要なJWTを生成してください。

Discussion