๐ค UserDetailsService ๊ตฌํ
๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๊ตฌํ ์ ์ ๋จผ์ UserDetailsService ์ธํฐํ์ด์ค๋ฅผ ์ค์ ํด์ผ ๋๋ค!
-> DB์์ ํ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํจ!
์ ๋ฐ๋ฏธ ํด๋์ค์์๋ UserDetailsManager๋ง ๋ฐฐ์ ๋๋ฐ,, ์ฑ ์์๋ UserDetailsService๋ฅผ ๊ตฌํํด์ ๋ฌด์จ ์ฐจ์ด์ธ์ง ์ฐพ์๋ด..
UserDetailsService VS. UserDetailsManager
UserDetailsService๋ Spring Security์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐ ์ฌ์ฉ๋๋ ์ธํฐํ์ด์ค.
์ฃผ์ ๋ฉ์๋: loadUserByUsername(String username)
์ฉ๋: ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ก๋ํ๊ธฐ ์ํด ์ฃผ๋ก ๋ก๊ทธ์ธ ๊ณผ์ ์์ ์ฌ์ฉ๋จ. ์ฆ, ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ์ ์ ๋ ฅํ ์ฌ์ฉ์ ์ด๋ฆ์ ๊ธฐ๋ฐ์ผ๋ก ํด๋น ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์กฐํํ๋ ์ญํ .
UserDetailsManager๋ UserDetailsService์ ํ์ฅ๋ ๋ฒ์ ์ผ๋ก, ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์กฐํํ๋ ๊ฒ ์ธ์๋ ์ฌ์ฉ์ ์ถ๊ฐ, ์์ , ์ญ์ ๋ฑ์ ์์ ์ ์ง์ํ๋ ์ธํฐํ์ด์ค!
์ฃผ์ ๋ฉ์๋:
• createUser(UserDetails user)
• updateUser(UserDetails user)
• deleteUser(String username)
• userExists(String username)
์ฉ๋: ์ฌ์ฉ์ ์ ๋ณด๋ฅผ CRUD(์์ฑ, ์ฝ๊ธฐ, ์ ๋ฐ์ดํธ, ์ญ์ ) ์์ ์ ์ง์ํ๋ฉฐ, ๋ ๋ณต์กํ ์ฌ์ฉ์ ๊ด๋ฆฌ ์์ ์ด ํ์ํ ๊ฒฝ์ฐ ์ฌ์ฉ๋จ
๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ๋๋ ๊ตฌํ์ฒด๋ JdbcUserDetailsManager์ InMemoryUserDetailsManager
-> JdbcUserDetailsManager๋ฅผ ๊ตฌํํ๋ ค ํ๋ค user, authority ํ
์ด๋ธ ํ์ํ๋ค ํด์.. ์ผ๋จ ๋์ค์ refactoring ํ๋ฉด์ ๊ณ ์ณ๋ณด๊ธฐ๋กํจ.. ๐ง
- Service ํจํค์ง ์๋ UserDetailsService๋ฅผ ๊ตฌํํ CustomUserDetailsService ํด๋์ค ์์ฑ
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
public final MemberRepository memberRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Member theMember = memberRepository.findByEmail(email);
if(theMember == null) {
throw new UsernameNotFoundException(email);
}
return User.builder()
.username(theMember.getEmail())
.password(theMember.getPassword())
.roles(theMember.getRole().toString())
.build();
}
}
- email๋ก ๋ก๊ทธ์ธ ํ๊ธฐ ๋๋ฌธ์ ํ๋ผ๋ฏธํฐ๋ฅผ email๋ก ๋๊ฒจ์ค.!
- ์กด์ฌํ์ง ์๋ ๊ณ์ ์ด๋ฉด ์์ธ์ฒ๋ฆฌ.
- User ๊ฐ์ฒด๋ก ๋ณํํ์ฌ ๋ฐํ
- ์๋ ์ฑ ์๋ MemberService ์์ ๋ฃ์ด ์คฌ๋๋ฐ, ๋๋ ์์ ๋ฐ๋ก ๋นผ์ค,, remember me ๊ธฐ๋ฅ ๊ตฌํ์๋ ์ฐ์ด๊ธฐ ๋๋ฌธ...!
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomUserDetailsService customUserDetailsService;
// ์์ฑ์ ์ฃผ์
์ ์ฌ์ฉํ์ฌ ์์กด์ฑ ์ฃผ์
public SecurityConfig(CustomUserDetailsService customUserDetailsService) {
this.customUserDetailsService = customUserDetailsService;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.formLogin(form ->
form.loginPage("/members/login")
.defaultSuccessUrl("/")
.usernameParameter("email")
.failureUrl("/members/login/error")
);
http.logout(logout ->
logout.logoutRequestMatcher(new AntPathRequestMatcher("/members/logout"))
.logoutSuccessUrl("/")
);
...
๐ก Spring Framework์์๋ @Autowired ๋ณด๋ค ์์ฑ์ ์ฃผ์ ์ ๊ถ์ฅํ๊ณ ์๋ค. ์ด๋ ์ฝ๋์ ๊ฐ๋ ์ฑ์ ๋์ด๊ณ , ์์กด์ฑ ๊ด๋ฆฌ์ ๋ช ํ์ฑ์ ๋ณด์ฅ.
- Spring Security์ ์ธ์ฆ ๊ณผ์ ์์ ์ฌ์ฉ์์ ๋ก๊ทธ์ธ ์์ฒญ์ด ๋ค์ด์ค๋ฉด, Spring Security๋ UserDetailsService์ loadUserByUsername ๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์กฐํ.
๋ก๊ทธ์ธ ํ์ด์ง ์์ฑ
- templates/member/memberLoginForm.html ์์ฑ
<form role="form" method="post" action="/members/login" novalidate>
<i class="bi bi-chat-square-heart-fill fs-1" style="color: #FFCFE2;"></i>
<h1 class="h3 mt-1 mb-3 fw-bold text-muted">Please sign in</h1>
<div class="form-floating">
<input type="email" name="email" class="form-control"
id="floatingInput" placeholder="name@example.com">
<label th:for="email">Email address</label>
</div>
<div class="form-floating">
<input type="password" class="form-control" name="password"
id="floatingPassword" placeholder="Password">
<label th:for="password">Password</label>
</div>
<div th:if="${loginErrorMsg}" class="error" th:text="${loginErrorMsg}"
style="color: red; font-size: 15px"></div>
<div class="form-check text-start my-3">
<input class="form-check-input" type="checkbox" id="rememberMe" name="rememberMe">
<label class="form-check-label" for="rememberMe">
Remember me
</label>
</div>
<button class="btn btn-light w-100 py-2 fw-bold text-muted" type="submit"
style="background-color: #FFCFE2;">Sign in
</button>
<p class="mt-3 text-body-secondary">© 2024</p>
</form>
- remember me ๋ฅผ ์ถ๊ฐํด์ ๋ธ๋ผ์ฐ์ ๋ฅผ ๊ป๋ค ์ผ๋ ๋ก๊ทธ์ธ ์ํ ์ ์งํ๋๋ก ์ค์ ํ ์์ !
๋ถํธ์คํธ๋ฉ ๋ชจ๋ฌ์ฐฝ์ ๋์์ ๋ก๊ทธ์ธํ๊ณ ์ถ์๋๋ฐ, ajax ์๋์ด ์ ์๋จ.. ์ฝ์์ ์๋ฌ๋ ์๋จ๊ณ ๋ด์ฉ๋ ์ ๋ถ๋ฌ์์ ธ์ ์ผ๋จ ํจ์ค..
๋ถํธ์คํธ๋ฉ ๋ฌธ์ ์ธ์ง๋ฅผ ์ ๋ชจ๋ฅด๊ฒ ๋ค,, ๋ค๋ฅธ ๋ธ๋ก๊ทธ ๊ธ ๋ค ๋ค์ ธ๋ดค๋๋ฐ๋ ์๋์ด ใ
,ใ
..data-bs-toggle๊ณผ data-bs-target ์์ฑ์ง์ ๋ ์ ๋๋ก ํ๊ณ ...onclick ์์ฑ ์ฌ์ฉํ๋ฉด ReferenceError: openLoginModal is not defined at HTMLAnchorElement.onclick ์๋ฌ๋ธ ๋ํ์ฅ ํํฐ,,์๊ฐ ๋๋ฌด ์ค๋๊ฑธ๋ ค ์ผ๋จ ๋์ค์ ๋ค์ ํด๋ณด๊ธฐ๋ก..ใ
MemberController ์ถ๊ฐ ์ฝ๋ ๊ตฌํ
- ํ์๊ฐ์ ์ฝ๋ ๊ตฌํํ ๊ณณ ์๋์ ์ถ๊ฐ
@GetMapping("/login")
public String loginMember() {
return "/member/memberLoginForm";
}
@GetMapping("/login/error")
public String loginError(Model model) {
model.addAttribute("loginErrorMsg", "Please enter a valid email or password");
return "/member/memberLoginForm";
}
- SecurityConfig์ ๋ก๊ทธ์ธ ์ฑ๊ณต์ ๋ฆฌ๋ค์ด๋ ํธ ๋ Url์ ์ค์ ํด์ค์ PostMapping์ ๋ฐ๋ก ์์ฑ x
-> ์ฐพ์๋ณด๋ ๋ก๊ทธ์ธ ๊ด๋ จ ์ค์ ์ SecurityConfig์์ ํ ๊ณณ์์ ๊ด๋ฆฌํ ์ ์์ด ๋ณด์ ์ค์ ๊ณผ ๊ด๋ จ๋ ๋ชจ๋ ๊ตฌ์ฑ ์์๋ฅผ ์ฝ๊ฒ ์ฐพ์ ์ ์์ด์ ์ด๋ ๊ฒ ์ฐ์ด๋๋ฏ!
- login/error ๊ฒฝ๋ก์ ๋ํ GET ์์ฒญ์ด ๋ค์ด์ค๋ฉด loginError ๋ฉ์๋๊ฐ ํธ์ถ -> ์ด ๋ฉ์๋๋ Model์ ์ค๋ฅ ๋ฉ์์ง๋ฅผ ์ถ๊ฐํ๊ณ /member/memberLoginForm ํ ํ๋ฆฟ์ ๋ฐํ -> ๋ ๋๋ง ํ loginErrorMsg๊ฐ ์กด์ฌํ๋ฉด <div> ์์๊ฐ ํ์๋๊ณ , ์ค๋ฅ ๋ฉ์์ง๊ฐ ์ฌ์ฉ์์๊ฒ ๋นจ๊ฐ์์ผ๋ก ๋ํ๋จ
Test Code ์์ฑ
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class MemberControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
MemberService memberService;
@Autowired
PasswordEncoder passwordEncoder;
public Member createMember(String email, String password) {
MemberFormDTO memberFormDTO = MemberFormDTO.builder()
.name("ํ๊ธธ๋")
.email(email)
.password(password)
.address("์์ธ์ ๋งํฌ๊ตฌ ํฉ์ ๋")
.build();
Member member = Member.createMember(memberFormDTO, passwordEncoder);
return memberService.saveMember(member);
}
@Test
@DisplayName("๋ก๊ทธ์ธ ์ฑ๊ณต ํ
์คํธ")
public void loginSuccessTest() throws Exception{
String email = "test@email.com";
String password = "1234";
this.createMember(email, password);
mockMvc.perform(formLogin().userParameter("email")
.loginProcessingUrl("/members/login")
.user(email).password(password))
.andExpect(SecurityMockMvcResultMatchers.authenticated());
}
@Test
@DisplayName("๋ก๊ทธ์ธ ์คํจ ํ
์คํธ")
public void loginFailTest() throws Exception {
String email = "test@email.com";
String password = "1234";
this.createMember(email, password);
mockMvc.perform(formLogin().userParameter(email)
.loginProcessingUrl("/members/login/error")
.user(email).password("12345"))
.andExpect(SecurityMockMvcResultMatchers.unauthenticated());
}
}
https://adjh54.tistory.com/347
https://docs.spring.io/spring-security/reference/servlet/test/mockmvc/result-matchers.html
MockMvc ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ผ๋ ๊ฒ๋ ์์๋ค๋ ๊ฒ์ ์ฒ์ ๊นจ๋ฌ์,,๐ฐ
ํ
์คํธ์ฉ์ผ๋ก ๋ง์ด ์ฐ์ธ๋ค๊ณ ํ๋ค!
perform() : MockMvc๋ฅผ ์ฌ์ฉํ์ฌ HTTP ์์ฒญ์ ์คํ
andExpect() : ์ปจํธ๋กค๋ฌ์ ์๋ต์ ๊ฒ์ฆ
Remember Me ๊ธฐ๋ฅ ๊ตฌํ
์ด์ ๋ถํธ์คํธ๋ฉ ๋์์ธ ๊ธ์ด์จ๊ฑฐ ์ฑ ์๋ ์ ๋์์๋ remember ๊ธฐ๋ฅ ๊ตฌํ ํด๋ณด์ ์ถ์ด์ ... ๐ค
http.rememberMe(rememberMe -> rememberMe
.rememberMeParameter("rememberMe") // default: remember-me, checkbox ๋ฑ์ ์ด๋ฆ๊ณผ ๋ง์ถฐ์ผํจ
.tokenValiditySeconds(3600) // ์ฟ ํค์ ๋ง๋ฃ์๊ฐ ์ค์ (์ด), default: 14์ผ
.alwaysRemember(false)); // ์ฌ์ฉ์๊ฐ ์ฒดํฌ๋ฐ์ค๋ฅผ ํ์ฑํํ์ง ์์๋ ํญ์ ์คํ, default: false
// .userDetailsService(customUserDetailsService)); // ๊ธฐ๋ฅ์ ์ฌ์ฉํ ๋ ์ฌ์ฉ์ ์ ๋ณด๊ฐ ํ์ํจ. ๋ฐ๋์ ์ด ์ค์ ํ์ํจ.
์ผ๋จ SecurityFilterChain ์์ ์ด ์ฝ๋๋ฅผ ๋ฃ์ด์คฌ๋ค!
๊ทผ๋ฐ ์๋์ด ์๋ผ์ ์์ง.. ํ๋๋ฐ html์ input ์ฝ๋์ id, name์ 'rememberMe'๋ก ์ค์ ์ ์งํ๊ณ ๊ธฐ๋ณธ์ ์ผ๋ก ์ค์ ๋์ด ์๋ value๊ฐ์ ์ญ์ ํ๋๊น ์๋์ด ๋๋๋ผ๋... ;
๋ฒ์ ์ ๊ทธ๋ ์ด๋ ํ๋ฉด์ SecurityFilterChain ์ฌ์ฉ์ userDetailService ์ค์ ์ํด์ค๋ ๋๋ค๊ณ ํ๋ค!!!
https://velog.io/@dailylifecoding/spring-security-remember-me
remember-me ์ฟ ํค๊ฐ ์์ฑ๋์ด์ผ ์ฑ๊ณต!
๋ด์ฉ ์ฐธ๊ณ : ์ฑ '์คํ๋ง ๋ถํธ ์ผํ๋ชฐ ํ๋ก์ ํธ with JPA'