2 분 소요

스프링 시큐리티를 적용해서 로그인/회원가입을 구현해보자.
먼저 스프링 시큐리티 설정을 적용하기 위해 SecurityConfig 클래스를 만들고 @Configuration을 적용하자.
그리고 보안 기능 활성화을 위해 @EnableWebSecurity까지 적용해주자.

@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig {

    private final UserDetailsService userDetailsService;

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests((authorizeHttpRequests) ->
                        authorizeHttpRequests
                                .requestMatchers("/login", "/signup").permitAll()
                                .anyRequest().hasRole("USER")
                )
                .formLogin((formLogin) ->
                        formLogin
                                .usernameParameter("email")
                                .passwordParameter("password")
                                .loginPage("/login")
                                .failureUrl("/login?failed")
                                .loginProcessingUrl("/login/process")
                );
        http
                .csrf((csrf) -> csrf.disable());
        return http.build();
    }

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

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}
  • filterChain()
    • authorizeHttpRequests.requestMatchers() : 주소마다 접근 권한을 설정해줄 수 있음
      • permitAll() : 모든 접근 허용(=권한 필요없음)
      • hasRole() : 해당 주소 접근 시 필요한 권한으로 USER, ADMIN 등이 있음
    • formLogin()
      • usernameParameter(), passwordParameter() : 로그인 ID/PW 파라미터 설정
      • loginPage() : 로그인이 필요할 때 돌아갈 주소 설정하는 기능으로, 설정하지 않으면 기본값 적용됨
  • passwordEncoder() : 비밀번호 암호화 기능 제공, 참고 링크
  • configureGlobal() : 아래 참고

configureGlobal()

로그인 기능을 구현하려면 AuthenticationManagerBuilder를 받아 인증 처리를 해야한다.
위에서 사용하는 userDetailsService는 타입이 UserDetailsService로 어떤 기준으로 로그인 처리를 할지에 대한 정보
를 설정해줘야 한다. 또한 주입받아서 사용하고 있음을 확인할 수 있는데, 다음 코드를 참고하자.

@Service
@AllArgsConstructor
public class CustomUserDetailService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String email) {
        Set<GrantedAuthority> grantedAuthorities = new HashSet<>();

        //이메일 체크
        Optional<User> optionalUser = userRepository.findByEmail(email);

        if(optionalUser.isPresent()) {
            User user = optionalUser.get();
            grantedAuthorities.add(new SimpleGrantedAuthority("USER"));
            return new PrincipalDetails(user);
        } else {
            // DB에 정보가 존재하지 않으므로 exception 호출
            throw new UsernameNotFoundException("user doesn't exist, email : " + email);
        }
    }
}
  • CustomUserDetailServiceUserDetailsService를 상속받아 loadUserByUsername()를 구현해야만 함
  • loadUserByUsername() : 파라미터를 기준으로 유저 정보를 불러와 반환
    • userRepository.findByEmail(String email) : 이메일 기준으로 유저 조회
  • PrincipalDetails 타입은 여기를 참고
  • @Service를 적용함으로써 컴포넌트 스캔 대상으로 지정돼 스프링 빈으로써 활용할 수 있음

findByEmail()

jpql을 짜서 적용해도 되지만 스프링 데이터 JPA를 적용하면 좀 더 간편하게 활용할 수 있다!

public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
    Optional<User> findByEmail(String email);
}


기본 로그인 화면 제거

위에서 http.formLogin()loginPage() 파라미터로 임의의 로그인 주소 설정 후 렌더링할 페이지를 설정해주면 된다.
예를 들면 나같은 경우 resources/templates/login.html을 따로 만들어줬고 로그인 주소 접근 시 login.html에 들어갈
수 있도록 컨트롤러에서 다음처럼 처리해줬다. 회원가입도 마찬가지다!

@GetMapping("/login")
public String login(Model model) {
    ...
    return "login";
}

@PostMapping("/login")
public String login(@ModelAttribute("UserDto") UserDto userDto) {
    boolean login = userService.login(userDto);
    if (login) return "home";
    return "login"; 
}

@GetMapping("/signup")
public String signUp(Model model) {
    ...
    return "signup";
}

@PostMapping("/signup")
public String signUp(@ModelAttribute("UserDto") UserDto userDto) {
    ...
    userService.save(userDto);
    return "redirect:/login";
}
  • thymeleaf 적용 필수
  • 로그인/회원가입 주소에 권한이 없어도 접근할 수 있도록 filterChain에서 permitAll() 필수

csrf.disable()

스프링 시큐리티에서는 기본적으로 CSRF 공격을 막는 보안 기능을 제공한다.
비활성화가 필요하다면 csrf.disable()을 호출하면 된다. 만약 REST API만 이용하는 서버라면 HTTP 통신이 이뤄지므로
stateless하고 서버에 인증 정보를 보관하지 않으며, 클라이언트는 API 요청 시 JWT 등을 활용해 인증할 수 있어야 한다.
즉, 서버에 인증 정보를 보관하지 않으므로 CSRF 기능을 비활성화해도 괜찮다.

Request method ‘POST’ not supported

나같은 경우 빠르게 회원가입 기능을 테스트해봐야 했었는데 CSRF 토큰을 넘겨주지 않아 컨트롤러에 설정한 API 함수가 동작을 하지
않고 요청에 맞는 메소드가 없다는 에러가 떠서 임시로 꺼뒀다.

카테고리:

업데이트:

댓글남기기