스프링 부트로 블로그 서비스 개발하기

회원가입 기능 - 스프링 시큐리티 커스텀 UserDetailsService + 도커 MySQL 적용하기

exena 2025. 11. 26. 21:58

기존에 게시판 서비스를 만들 때 스프링 시큐리티를 사용한 내용은

https://nimble-ship-2fb.notion.site/f944774f674842c6bb8124000fc3eca2?source=copy_link

이곳에 정리해두었다. 그 결과물이 되는 코드로 시작해보자. 

@Configuration
@EnableWebSecurity
@Profile("!test")
public class WebSecurityConfig {

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable()) // ✅ POST 테스트 위해 (개발용)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(HttpMethod.POST, "/members").permitAll()
                        .requestMatchers("/css/**", "/js/**").permitAll()
                        .anyRequest().authenticated()
                )
                // 스프링 시큐리티 기본 로그인 화면 사용
                .formLogin(Customizer.withDefaults());
                // 만약 로그인 화면을 따로 만들었다면
                // .formLogin(form->form
                //         .loginPage("컨트롤러에서 화면을 요청받는 url")
                //         .usernameParameter("로그인 폼에 들어가는 로그인 ID 태그의 id")
                //         .passwordParameter("로그인 폼에 들어가는 비밀번호 태그의 id")
                //         .permitAll());

        return http.build();
    }
}

WebSecurityConfig

여기서 패스워드 암호화 빈을 선언한다.

여기서 회원가입 요청을 받는 부분은 로그인 없이 요청을 받을 수 있도록 permitAll에 넣어준다.

@RequiredArgsConstructor
public class AccountContext implements UserDetails {

    @Getter
    private final Long id;
    private final String loginId;
    private final String password;
    private final Boolean enabled;

    private final List<GrantedAuthority> authorities;

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return loginId;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities; //리스트를 getter로 반환하는것이 싫다면 return Collections.unmodifiableList(authorities);
    }

}

AccountContext

@Service("userDetailsService")
@RequiredArgsConstructor
@Profile("!test")
public class CustomUserDetailsService implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Member member = memberRepository.findByEmail(username);

        if(member == null){
            throw new UsernameNotFoundException("UsernameNotFoundException");
        }
        List<GrantedAuthority> authorities = member.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority(role.getName()))
                .collect(Collectors.toList());
        return new AccountContext(member.getId(), member.getEmail(), member.getPasswordHash(), true, authorities);
    }

}

CustomUserDetailsService

이 파일들은 security라는 폴더 안에 넣었다.

도커에서 mysql 컨테이너를 생성할 때 필요한 정보는 compose.yaml 파일에 적어놨다.

다시 말하지만 3306:3306 이라고 적어야 외부에서도 3306으로 접근이 가능하다.

 

cmd에서 docker ps를 입력하면 현재 작동하는 컨테이너의 정보가 나온다.

이후 mysql cli 가 설치되어있다면 mysql -h 127.0.0.1 -P 3306 -u DB계정이름 -p 라고 적으면 되고

없다면 docker exec -it 컨테이너이름 mysql -u DB계정이름 -p라고 적으면 비밀번호 입력 후 db에 접속이 된다.

테이블 내부의 정보를 보려면 DESC 혹은 DESCRIBE 명령어를 쓰면 된다.

자동생성이 잘 된 것을 볼 수 있다.

 

https://luji.tistory.com/99

 

httpie Windows에서 설치하기

httpie란? 파이선에서 개발된 유틸리티로 http 개발이나 디버깅 용도로 사용된다. 사용성이 쉬우면서 json이 내장되어있다. 가독성이 뛰어나며 기타 장점들이 있음. 보통 리눅스나 맥에선 yum, apt, bre

luji.tistory.com

이제 회원가입 요청을 보내보기 위해서 httpie를 설치한다. 포스트맨을 쓸 수도 있다.

테스트 삼아서 "/"로 GET요청을 보내봤는데 로그인 URL이 응답으로 전송되는걸 볼 수 있다.

스프링 시큐리티가 작동해서 그런 듯 하다.

맨 위의 securityFilterChain에서 csrf.disable을 해놨고 회원가입 요청은 permitAll에 넣어놨다면 회원가입 요청은 시큐리티가 튕겨내지 않을 것이다. DTO 객체와 함께 200 응답이 돌아오는 것을 볼 수 있다.

회원을 생성했다면 로그인을 시도해보자.

나는 테스트를 하느라고 같은 이름의 회원을 중복생성했더니 로그인이 안되서 ddl-auto를 create-drop으로 해서 기존 데이터를 날린 뒤에 다시 회원을 생성했다.

로그인이 잘 되는것을 볼 수 있다.

잘 되는걸 확인했으니까 필터체인의 .csrf(csrf -> csrf.disable()) 부분은 없애주자.