본문 바로가기
개발

스프링 시큐리티로 안전한 로그인 시스템 구축하기

by new-fp 2024. 11. 14.
728x90

스프링 시큐리티로 안전한 로그인 시스템 만들기

안녕하세요! 개발과 기술을 사랑하는 여러분, 오늘은 Spring Security를 활용해 안전한 로그인 시스템을 구현하는 방법을 소개하겠습니다. 이 가이드는 로그인 기능을 커스터마이징하는 여러 단계로 구성되어 있으며, 이제 막 Spring을 배우기 시작한 초급자부터 중급 개발자에게도 유용할 것입니다. 그럼 시작해볼까요?

들어가며

웹 애플리케이션에서 사용자의 인증은 매우 중요한 부분입니다. 보안이 뒷받침되지 않은 로그인 기능은 사용자 데이터를 위협할 수 있죠. Spring Security는 이러한 인증 과정을 쉽게 관리할 수 있는 강력한 도구입니다. 이번 포스트에서는 기본적인 로그인 절차를 소개하고, 이를 기반으로 커스텀 인증 시스템을 만들어 보겠습니다.

1단계: 사용자 정의 인터페이스 구현

먼저, 사용자의 정보를 담는 UserEntity 클래스를 만듭니다. 이 클래스는 UserDetails 인터페이스를 구현하고 사용자에 대한 필요한 메서드를 정의합니다.

@Data
@Entity(name="USER_INFO")
@NoArgsConstructor
public class UserEntity implements UserDetails {
    @Id
    private String userId;
    private String userPw;
    private String userName;

    @Builder
    public UserEntity(String userId, String userPw, String userName) {
        this.userId = userId;
        this.userPw = userPw;
        this.userName = userName;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singleton(() -> "ROLE_USER");
    }

    @Override
    public String getPassword() {
        return this.userPw;
    }

    @Override
    public String getUsername() {
        return this.userId;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

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

2단계: 사용자 조회 메소드 추가

가입한 사용자를 조회하는 메소드를 UserRepository 인터페이스에서 정의합니다.

@Repository
public interface UserRepository extends JpaRepository<UserEntity, String> {
    UserEntity findByUserId(String username);
}

3단계: UserDetailsService 구현

CustomUserDetailsService 클래스를 만들어 사용자를 조회하고 로드하는 메소드 loadUserByUsername를 구현합니다.

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity user = userRepository.findByUserId(username);
        if (user == null) throw new UsernameNotFoundException("User not exist");
        return user;
    }
}

4단계: AuthenticationProvider 구현

이제 실제 인증 로직을 구현할 CustomAuthenticationProvider를 작성합니다. 사용자의 인증 정보를 확인하고 필요한 경우 예외를 발생시킵니다.

@Component
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
    private final CustomUserDetailsService customUserDetailsService;
    private final PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        UserEntity user = (UserEntity) customUserDetailsService.loadUserByUsername(username);

        if (!passwordEncoder.matches(password, user.getUserPw())) {
            throw new BadCredentialsException("Password is invalid");
        }

        return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

5단계: 로그인 핸들러 구현

로그인 성공 또는 실패 시 처리할 핸들러를 구현합니다.

@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        response.sendRedirect("/");
    }
}

@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        response.sendRedirect("/login?error=invalid");
    }
}

6단계: Security Config 수정

마지막으로 Spring Security의 설정을 수정해서 커스터마이즈된 인증 체계를 적용합니다.

@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
    private final CustomAuthenticationFailureHandler customAuthenticationFailureHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .successHandler(customAuthenticationSuccessHandler)
                .failureHandler(customAuthenticationFailureHandler)
                .permitAll()
                .and()
                .logout().permitAll();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

7단계: 로그인 페이지 생성

로그인 폼을 담는 HTML 파일을 작성합니다. 아이디와 비밀번호를 입력받는 폼을 포함하고 있습니다.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>로그인</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<main class="form-signin w-100 m-auto">
    <form method="post" action="/authenticate">
        <h1 class="h3 mb-3 fw-normal text-center">로그인</h1>
        <div class="form-floating">
            <input type="text" class="form-control" name="username" placeholder="아이디">
            <label>아이디</label>
        </div>
        <div class="form-floating">
            <input type="password" class="form-control" name="password" placeholder="패스워드">
            <label>패스워드</label>
        </div>
        <button class="w-100 btn btn-lg btn-primary" type="submit">로그인</button>
    </form>
</main>
</body>
</html>

결론

이번 포스트에서는 Spring Security를 통해 안전하고 커스터마이즈 가능한 로그인 시스템을 구현하는 방법을 살펴보았습니다. 점차 복잡해지는 웹 애플리케이션의 보안을 강화하기 위해 이러한 시스템을 적용하는 것은 필수적입니다. 여러분도 이러한 방법을 통해 자신만의 로그인 시스템을 만들어 보세요!

의문이 있으신 분들은 댓글로 질문해 주세요. 다음 포스트에서 다시 만나요!

728x90