๐Ÿค“  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">&copy; 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

 

[Java] Spring Boot MockMvc ์ดํ•ดํ•˜๊ธฐ : ํ…Œ์ŠคํŠธ ํ๋ฆ„ ๋ฐ ์‚ฌ์šฉ์˜ˆ์ œ

ํ•ด๋‹น ๊ธ€์—์„œ๋Š” MockMvc์— ๋Œ€ํ•ด ์ดํ•ดํ•˜๊ณ  ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ํ™•์ธํ•ด ๋ด…๋‹ˆ๋‹ค.   ๐Ÿ’ก [์ฐธ๊ณ ] ์ด์ „์— ์ž‘์„ฑํ•œ Test ๊ด€๋ จ ๊ธ€๋“ค์„ ์ฝ์œผ์‹œ๋ฉด ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค.๋ถ„๋ฅ˜๋งํฌJUnit 5 ์ด๋ก  ๋ฐ ๊ตฌ์„ฑ ์š”์†Œhttps://adjh54.

adjh54.tistory.com

https://docs.spring.io/spring-security/reference/servlet/test/mockmvc/result-matchers.html

 

SecurityMockMvcResultMatchers :: Spring Security

At times it is desirable to make various security related assertions about a request. To accommodate this need, Spring Security Test support implements Spring MVC Test’s ResultMatcher interface. In order to use Spring Security’s ResultMatcher implement

docs.spring.io

 

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

 

[Spring Security] Remember Me ์ธ์ฆ

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๊ฐ€ ์ œ๊ณตํ•˜๋Š” Remember Me ๊ธฐ๋Šฅ์„ ์•Œ์•„๋ณด์ž.

velog.io

 

 

remember-me ์ฟ ํ‚ค๊ฐ€ ์ƒ์„ฑ๋˜์–ด์•ผ ์„ฑ๊ณต!

 

 

 

 

 

๋‚ด์šฉ ์ฐธ๊ณ  : ์ฑ… '์Šคํ”„๋ง ๋ถ€ํŠธ ์‡ผํ•‘๋ชฐ ํ”„๋กœ์ ํŠธ with JPA' 

+ Recent posts