본문 바로가기
SpringSecurity

인증 FormLogin

by 이상한나라의개발자 2023. 12. 12.

SpringSecurity에는 세부적인 보안 기능을 설정할 수 있는 API를 제공합니다.

 

 

 

스프링 시큐리티의 인증 플로우 

 


 

 

AuthenticationManager

 

 

 

 

AuthenticationProvider

 

 

1. 간단한 FormLogin 구현

 

1.1 Security Config 파일 작성

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final AuthenticationService authenticationService;

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring()
                .mvcMatchers("/images/**", "/favicon.ico","node_modules/**", "/static/**");
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
                .authorizeHttpRequests() // 인증 인가 설정
                .mvcMatchers("/login", "/account").permitAll()
                .anyRequest().authenticated();
        http
                .formLogin() // 폼 로그인 설정
                .defaultSuccessUrl("/main");
        http
                .logout() // 로그아웃 설정
                .logoutSuccessUrl("/login")
                .invalidateHttpSession(true);
        http
                .csrf().disable();

        return http.build();
    }

    // 인증 관리자 설정
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http, 
                                                       BCryptPasswordEncoder bCryptPasswordEncoder) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(authenticationService) // 사용자 정보를 가져오는 서비스 설정
                .passwordEncoder(bCryptPasswordEncoder)
                .and().build();
    }

    // 패스워드 인코더로 사용할 빈 등
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

 

1.2 UserDetailService 인터페이스를 상속 받은 클래스 작성

 

이 부부은 유저의 정보를 db에서 조회해 옵니다.

차후에는 권한 부분도 db에서 조회하여 넣어야 합니다.

 

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthenticationService implements UserDetailsService {

    private final AccountRepository accountRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account account = accountRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException(username));
            return new AccountContext(account, List.of(new SimpleGrantedAuthority("ROLE_USER")));
    }

    @Transactional
    public void createUser(Account account) {
        accountRepository.save(account);
    }
}

 

 

1.3 유저 정보를 담을 클래스 작성

 

실제로 AccountContext 말고 loadUserByUsername 에서 직접 유저를 리턴하고 사용해도 됩니다.

 

@Getter
public class AccountContext extends User {

    private final Account account;
    public AccountContext(Account account, Collection<? extends GrantedAuthority> authorities) {
        super(account.getUsername(), account.getPassword(), authorities);
        this.account = account;
    }
}

 

1.4 UserRepository

 

public interface UserRepository extends JpaRepository<Account, Long> {
    Optional<Account> findByUsername(String username);
}

 

1.5 로그아웃 처리

    @GetMapping("/logout")
    public String logout(HttpServletRequest request, HttpServletResponse response) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (isAuth(authentication)) {
            new SecurityContextLogoutHandler().logout(request, response, authentication);
        }
        return "redirect:/login";
    }

 

 

2. 복잡한 FormLogin 구현

 

이번 예제에서는 인증, 및 예외 처리를 별도로 구성하여 진행합니다.

 

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final CustomUserDetailService userDetailService;
    private final FormAuthenticationDetailSource authenticationDetailsSource;
    private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
    private final CustomAuthenticationFailureHandler customAuthenticationFailureHandler;

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http, PasswordEncoder passwordEncoder) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .authenticationProvider(authenticationProvider()).build();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        return new CustomAuthenticationProvider(userDetailService, passwordEncoder());
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring()
                .mvcMatchers("/css/**","/js/**","/images/**", "/favicon.ico","node_modules/**", "/static/**");
    }

    // 패스워드 인코더 설정
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // 인가
        http
                .authorizeRequests()
                .mvcMatchers("/login", "login*").permitAll()
                .antMatchers("/", "/users").permitAll()
                .antMatchers("/css/**","/js/**","/images/**", "/error/**").permitAll()
                .antMatchers("/mypage").hasRole("USER")
                .antMatchers("/messages").hasRole("MANAGER")
                .antMatchers("/config").hasRole("ADMIN")
                .anyRequest().authenticated()
        ;
        // 인증
        http
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/login_proc")
                .defaultSuccessUrl("/")
                .authenticationDetailsSource(authenticationDetailsSource) // 인증 성공후에 좀더 디테일한 검증 작업을 위한 객체
                .successHandler(customAuthenticationSuccessHandler)
                .failureHandler(customAuthenticationFailureHandler)
                .permitAll()
        ;
        // 로그아웃
        http
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login")
        ;
        http
                .exceptionHandling()
                .accessDeniedHandler(customAccessDeniedHandler());
        ;
        http
                .csrf()
        ;
        return http.build();
    }

    @Bean
    public AccessDeniedHandler customAccessDeniedHandler() {
        CustomAccessDeniedHandler customAccessDeniedHandler = new CustomAccessDeniedHandler();
        customAccessDeniedHandler.setErrorPage("/denied");
        return customAccessDeniedHandler;
    }
}

 

 

2.1 CustomUserDetailService 작성

@Service
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         Account account = userRepository.findByUsername(username).orElseThrow(() -> new BadCredentialsException(username));
        return new AccountContext(account, List.of(new SimpleGrantedAuthority("ROLE_USER")));
    }
}

 

2.2 유저 정보를 담을 클래스 작성 AccountContext

 

@Getter
public class AccountContext extends User {

    private final Account account;
    public AccountContext(Account account, Collection<? extends GrantedAuthority> authorities) {
        super(account.getUsername(), account.getPassword(), authorities);
        this.account = account;
    }
}

 

2.3 WebAuthenticationDetails 

 

HTTP 요청에서 인증요청의 세부정보를 가져옵니다. 웹 환경에서 인증 요청시 추가적인 정보, 예를 들면 사용자가 어느 IP 주소에서 로그인을 시도 했는지 , 어느 세션에서 인증을 시도 했는지 또는 사용자가 특정 인증 파라미터를 가지고 있는지 등을 포함합니다.

 

이 정보는 보안 이벤트 로깅, 지역 제한 로그인, 세션 고정 공격 등과 같은 고급 보안 요구 사항을 처리하는데 유용합니다.

 

주요 메서드 정보는 아래와 같습니다.

 

- getRemoteAddress() : 현재 사용자의 IP 주소를 반환합니다.

- getSessionId() : 현재 HTTP 세션 ID를 반환합니다.

 

이 클래스는 인증 시도 동안 HttpServletRequest 객체를 기반으로 자동 생성되며, 생성된 WebAuthenticationDetails 객체는 Authentication 객체 내부의 details 속성에 할당 됩니다.

 

예를 들면, 사용자가 로그인을 시도할 때 발생하는 인증 프로세스 중에 Authentication 객체를 만들게 되면 해당 객체의 details 속성에는 WebAuthenticationDetails의 인스턴스가 포함될 수 있습니다.

 

웹 요청의 추가적인 세부 정보를 검증한다면 WebAuthenticationDetails를 확장하는 사용자 클래스를 생성하고 이를 스프링 시큐리티와 통합 하여 사용 하면 됩니다.

 

@Component
public class FormAuthenticationDetailSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {

    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new FormWebAuthenticationDetails(context);
    }
}


@Getter
public class FormWebAuthenticationDetails extends WebAuthenticationDetails {

    private final String secretKey;

    public FormWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        secretKey = request.getParameter("secret_key");
    }
}

 

2.4 AuthenticationProvider 

AuthenticationProvider는 인증 요청을 처리하는 방법을 정의 하는 인터페이스 입니다. 다양한 인증 방식 LDAP, DB 등을 사용해 인증을 구현할 수 있습니다.

 

또한, UsernamePasswordAuthenticationToken 는 스프링 시큐리티에서 제공하는 클래스로, 폼 로그인 기반의 인증에서 가장 일반적으로 사용되는 Authentication 구현체입니다.

 

Authentication authentication(Authentication authentication) throws AuthenticationException

 

제공되는 인터페이스를 통해 자신만의 인증메커니즘을 구현할 수 있습니다. 구현 후 SecurityConfig -> AuthenticationManager에 등록하여 사용합니다.

 

private final CustomUserDetailService userDetailService;

@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, PasswordEncoder passwordEncoder) throws Exception {
    return http.getSharedObject(AuthenticationManagerBuilder.class)
            .authenticationProvider(authenticationProvider()).build();
}

@Bean
public AuthenticationProvider authenticationProvider() {
    return new CustomAuthenticationProvider(userDetailService, passwordEncoder());
}

 

 

 

전체소스

 

/*
UserDetail Service 를 통해 인증된 사용자의 정보를 가지고 추가적인 인증 작업을 진행하는 클래스
 */
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {

    private final CustomUserDetailService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    // 인증에 대한 검증
    // id , password 외에 인증에 대한 업무적인 검증을 진행
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = (String) authentication.getCredentials();
        AccountContext accountContext = (AccountContext) userDetailsService.loadUserByUsername(username);
         if ( !passwordEncoder.matches(password, accountContext.getAccount().getPassword()) ) {
            throw new BadCredentialsException("Bad Credential");
        }

        FormWebAuthenticationDetails formWebAuthenticationDetails = (FormWebAuthenticationDetails) authentication.getDetails();

        if (isSecret(formWebAuthenticationDetails)) {
            throw new InsufficientAuthenticationException("Invalid Secret");
        }
        return new UsernamePasswordAuthenticationToken(accountContext.getAccount(), null, accountContext.getAuthorities());
    }
    private static boolean isSecret(FormWebAuthenticationDetails formWebAuthenticationDetails) {
        return formWebAuthenticationDetails.getSecretKey() == null || !formWebAuthenticationDetails.getSecretKey().equals("secret");
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 전달 되어진 authentication 객체가 UsernamePasswordAuthenticationToken.class 인지 확인
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

 

 

2.5 SimpleUrlAuthenticationSuccessHandler

 

스프링시큐리티의 인증성공 핸들러로 사용자가 인증에 성공후 절적한 URL로 리다이렉트 하는 기능을 제공합니다.

 

1. 기본 대상 URL 설정 : 사용자가 인증에 성공하면 이 핸들러는 사용자가 지정한 기본 URL로 리다이렉트 합니다.

  - setDefaultTargetUrl(String defaultTargetUrl) : 사용자를 리다이렉트할 기본 URL을 설정합니다.

 

2. 항상 기본 URL 사용

  - setAlwaysUseDefaultTargetUrl(boolean alwaysUseDefaultTargetUrl): 이 값을 true로 설정하면 인증 성공 후 항상      defaultTargetUrl로 리다이렉트합니다. 그렇지 않으면, 인증 전에 접근하려고 했던 원래의 URL(저장된 URL)로 리다이렉트합니다.

 

3. 리다이렉트 전략

  - setRedirectStrategy(RedirectStrategy redirectStrategy) : 사용할 리다이렉트 전략을 설정 기본적으로 defaultRedirectStrategy 사용

 

 

/*
인증 성공 후 처리를 담당하는 핸들러
여기서 인증 성공 후 처리는 인증 성공 후에 사용자가 접근하고자 했던 페이지로 이동하는 것을 의미한다.
 */
@Component
public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final RequestCache requestCache = new HttpSessionRequestCache();
    private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        setDefaultTargetUrl("/");
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if ( savedRequest != null ) {
            String redirectUrl = savedRequest.getRedirectUrl();
            redirectStrategy.sendRedirect(request, response, redirectUrl);
        }
        else {
            redirectStrategy.sendRedirect(request, response, getDefaultTargetUrl());
        }
        super.onAuthenticationSuccess(request, response, authentication);
    }
}

 

2.6 SimpleUrlAuthenticationFailureHandler

 

스프링시큐리티의 인증 실패 핸들러입니다, 사용자가 인증에 실패 했을경우 특정 페이지로 리다이렉트 기능을 주로 제공합니다.

 

@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        // 예외 메시지를 화면에 보이도록 처리한다.
        String errMessage = "Invalid Username or Password";

        if ( exception instanceof BadCredentialsException ) {
            errMessage = "Invalid Username or Password";
        } else if (exception instanceof InsufficientAuthenticationException) {
            errMessage = "Invalid Secret Key";
        } else if ( exception instanceof InternalAuthenticationServiceException) {
            errMessage = "Invalid Username or Password";
        } else if ( exception instanceof DisabledException ) {
            errMessage = "Locked";
        } else if ( exception instanceof CredentialsExpiredException) {
            errMessage = "Expired password";
        } else if ( exception instanceof AccountExpiredException) {
            errMessage = "Expired account";
        } else if ( exception instanceof LockedException) {
            errMessage = "Locked";
        }

        System.out.println("exception.getMessage() = " + exception.getMessage());

        setDefaultFailureUrl("/login?error=true&exception=" + errMessage + exception.getMessage());
        super.onAuthenticationFailure(request, response, exception);
    }
}

 

위 코드에서, 사용자가 인증에 실패하면 '/login?error=true&exception='  로 리다이렉트 되며 사용자는 페이지를 통해 오류 메시지를 확인할 수 있습니다.

SimpleUrlAuthenticationFailureHandler 는 기본적인 실패 로직에 적합합니다. 더 복잡한 로직이 필요할 경우는 AuthenticationFailureHandler 인터페이스를 직접 구현하는 것을 고려 해야 합니다.

 

2.7 AccessDeniedHandler

스프링시큐리티 인터페이스로, 현재 인증된 사용자가 요청한 리소스에 대한 권한이 없을 경우 발생하는 AccessDeniedException을 처리하는데 사용됩니다. 즉, 사용자는 로그인 했지만 특정 리소스에 접근 권한이 없을 경우 이 핸들러가 작동합니다.

 

스프링시큐리티에는 AccessDeniedHandlerImpl를 제공하며, 이는 권한이 없는 사용자를 특정 URL로 리다이렉트 하는 기능을 포함합니다.

 

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .exceptionHandling()
            .accessDeniedHandler(accessDeniedHandler())
        ... // 다른 구성 코드
}

@Bean
public AccessDeniedHandler accessDeniedHandler() {
    AccessDeniedHandlerImpl handler = new AccessDeniedHandlerImpl();
    handler.setErrorPage("/403"); // 여기에는 사용자에게 보여줄 403 에러 페이지 URL을 설정
    return handler;
}

// 사용자가 직접 구성의 경우
@Getter
@Setter
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private String errorPage;

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        String deniedUrl = errorPage + "?exception=" + accessDeniedException.getMessage();
        response.sendRedirect(deniedUrl);
    }
}
 

'SpringSecurity' 카테고리의 다른 글

JWT  (1) 2023.12.12
인가  (0) 2023.12.12