SpringBoot SpringSecurity　認証、認可（単体テスト有り）

# 概要
Spring Securityのプロダクトコードとテストコードを記事にします。
実装する機能は認証、認可です。

# 環境
OS:Windows10
IDE: IntelliJ community or CodeSpace
SpringBoot:2.7.0
Java:17
その他フレームワーク:SpringSecurity、JUnit5、SpringSecurityTest
github:https://github.com/RYA234/spring_boot_memo
ブランチは「Security」を選択。





# 画面

ログイン画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/b5947c47-5cae-1108-90cf-a0922f670dce.png)


ログイン成功後
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/78816f04-57ce-2c22-b975-ac4d6b8c9802.png)


ログアウト後
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/f91dc893-dfda-9f73-83fc-8f71b5a409ec.png)

ログイン失敗時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/27a5df15-7424-de91-69f1-df6480fcff41.png)


# プロダクトコード
SpringSecurityはクラスに@Configurationと@EnableWebSecurityアノテーションを追加しています。
```SecurityConfig.java
package com.example.spring_boot_memo.SpringSecurity.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.password.PasswordEncoder;


@EnableWebSecurity
@Configuration
public class SecurityConfig  extends WebSecurityConfigurerAdapter {



    @Autowired
    public PasswordEncoder passwordEncoder;

    /** セキュリティの各種設定 */
    @Override
    protected void configure(HttpSecurity http) throws Exception{

        // ログイン不要ページ
        http
                .authorizeRequests()
                .antMatchers("/index").permitAll()
                .antMatchers("/login").permitAll()
                .antMatchers("/check/access").permitAll()
                .anyRequest().authenticated();
        // ログイン処理
        http
                .formLogin()
                .loginProcessingUrl("/login")
                .loginPage("/login")
                .failureUrl("/login?error")
                .usernameParameter("username")
                .passwordParameter("password")
                .defaultSuccessUrl("/menu/menu", true);
        http.csrf().disable();
    }
    // ログイン認証に使用するユーザー情報を設定
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        auth
                .inMemoryAuthentication()
                .withUser("User") 
                .password(passwordEncoder.encode("PASS"))
                .roles("GENERAL");
    }


}



```



パスワードの形式はBCryptを使っています。
```BeanConfig.java
package com.example.spring_boot_memo.SpringSecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class BeanConfig {
    @Bean
    public PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}
}


```

# テストコード


``` SecurityTest.java
package com.example.spring_boot_memo;

import static org.junit.Assert.assertEquals;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.logout;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.security.NoSuchAlgorithmException;
import org.junit.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.runner.RunWith;
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.security.crypto.password.PasswordEncoder;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringRunner.class)
@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        classes = SpringBootMemoApplication.class)
@AutoConfigureMockMvc
public class SecurityTest {
    @Autowired
    private MockMvc mvc;
    @Autowired
    PasswordEncoder passwordEncoder;

    @Test
    @DisplayName("認証成功後にアクセスページにログインアクセスできるか確認")
    @WithMockUser
    public void whenGetCustomers_thenStatus200() throws Exception {
        mvc.perform(get("/menu/menu"))
                .andExpect(status().isOk());

    }

    @Test
    @DisplayName("認証成功後にログアウトのエンドポイントへリクエストすると、/logoutへリダイレクトする")
    @WithMockUser
    public void Logout() throws Exception {
        mvc.perform(logout("/logout"))
        .andExpect(header().string("Location", "/login?logout"))
      .andExpect(redirectedUrlTemplate("/login?logout"))
        .andExpect(status().is3xxRedirection())
        ;

    }



    @Test
    @DisplayName("認証されてないリクエストのとき、loginページにリダイレクトすることを検証")
    public void AccessCheckInNoAuth() throws Exception {
        mvc.perform(get("/check/OK"))
                .andExpect(header().string("Location","http://localhost/login"));
        mvc.perform(get("/check/not_access"))
                .andExpect(header().string("Location","http://localhost/login"));
        mvc.perform(get("/menu/menu"))
                .andExpect(header().string("Location","http://localhost/login"));
        mvc.perform(get("/login"))
                .andExpect(status().isOk());

    }
    @Test
    @DisplayName("比較-エンコード後の生のパスワードとストレージからのエンコードされたパスワード")
    public void passwordBCryptMatchCheck() throws NoSuchAlgorithmException{
        String  expected = "PASS"; 
        String actual = passwordEncoder.encode(expected);
        Boolean isActual = passwordEncoder.matches(expected,actual);
        assertEquals(true, isActual);
    }
}


```


# 参考資料


公式ドキュメント

https://spring.pleiades.io/spring-security/site/docs/5.2.1.RELEASE/reference/html/test.html

ByCryptのテストコードの書き方

https://github.com/springframeworkguru/ssc-brewery/blob/password-bcrypt/src/test/java/guru/sfg/brewery/web/controllers/PasswordEncodingTests.java

リダイレクト先の書き方　Example8

https://www.javatips.net/api/org.springframework.test.web.servlet.result.mockmvcresultmatchers.header

プロダクトコードの書き方

https://qiita.com/ryo0527/items/dbdbb2a476cc74e9116b