概要
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」を選択。
画面
プロダクトコード
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);
}
}
参考資料
公式ドキュメント
ByCryptのテストコードの書き方
リダイレクト先の書き方 Example8
プロダクトコードの書き方