Spring Security の認証をテストしてみる

前回は認可をテストしてみたので、認可についてもテストしてみようかと思います。

認可については認証から切り離してテストすることができましたが、認証は認可と切り離さずテストする感じですね。というのも、このやり方は AuthenticationFilter ごと実行させることになるので認証認可に関係する部分はすべて動作します。

ただ、認証認可に関する処理を一つのテストで網羅してしまうと、一つのテストに複数のテスト対象が入り込んでしまい複雑になってしまいます。なので、認証のテストではログインページへのアクセスなど、認可処理が入り込まないエンドポイントなどを使用して、テストをシンプルに保つのがいいのではないかと思います。

では、実装していきましょう。

コードは前回の記事のものをベースに拡張していきます。

baubaubau.hatenablog.com

Configuration クラスにパスワードエンコーダーをBean登録します。これがないと AuthenticationProvider がパスワードの突合ができず、テストが失敗してしまいます。

package com.example.spring.security.test.config;

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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public UserDetailsService userDetailsService() {
        var userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(
                User.withUsername("alice")
                        .password("12345")
                        .roles("admin")
                        .build()
        );
        userDetailsManager.createUser(
                User.withUsername("bob")
                        .password("12345")
                        .roles("staff")
                        .build()
        );
        return userDetailsManager;
    }

    @SuppressWarnings("deprecation")
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin();
        http.authorizeRequests()
                .mvcMatchers("/admin/**").hasRole("admin")
                .mvcMatchers("/users/**").hasAnyRole("admin", "staff")
                .anyRequest().authenticated();
    }

}

で、テストコードがこちら。

package com.example.spring.security.test;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;

@SpringBootTest
@AutoConfigureMockMvc
public class AuthenticationTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void formLoginWithInvalidCredential() throws Exception {
        mockMvc.perform(formLogin().user("bob").password("11111"))
                .andExpect(unauthenticated());
    }

    @Test
    public void formLoginWithValidCredential() throws Exception {
        mockMvc.perform(formLogin().user("bob").password("12345"))
                .andExpect(authenticated().withRoles("staff"));
    }

}

formLogin() という RequestBuilder の実装を使うことでフォームログインを行います。この実装はSpringSecurityTestの機能の一つで、フォームログインを簡単にテストできるようにしてくれています。メソッドチェーンでユーザー名とパスワードを指定する感じですね。

https://docs.spring.io/spring-security/site/docs/5.4.2/reference/html5/#securitymockmvcrequestbuilders

authenticated() unauthenticated() でログインが成功/失敗したことを検証できます。続けて withRoles などでログイン後の認証情報も検証できるようです。

https://docs.spring.io/spring-security/site/docs/5.4.2/reference/html5/#securitymockmvcresultmatchers

ソースはこちらにアップしてあります。

github.com