본문 바로가기
관리자

Programming-[Backend]/Spring Security

[스프링 시큐리티] 14. 인증 실패, 거부 처리 ; AuthenticationHandler, AccessDeniedHandler

728x90
반응형

 

1. 인증 실패 처리

 

인증 실패를 처리하는 과정은 이전 글의 인증 성공처리를 하는 부분과 매우 유사하다.

 

CustomAuthenticationHandler를 작성한다.

 

@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        String errorMessage = "Invalid Username or Password";

        if (exception instanceof BadCredentialsException) {
            errorMessage = "Invalid Username or Password";
        } else if (exception instanceof InsufficientAuthenticationException) {
            errorMessage = "Invalid Secret Key";
        }

        setDefaultFailureUrl("/login?error=true&exception=" + errorMessage);

        super.onAuthenticationFailure(request, response, exception);
    }
}

 

인증 성공 처리와 비슷하게 onAuthenticationFailure 메서드를 Override 하면된다. 다만 여기서는 실패 시 exception을 받아와서 처리해주기 때문에, exception의 타입에 따라서 errorMessage를 달리할 수 있다.

 

그리고 setDefaultFailureUrl 메서드에서 경로를 동적으로 지정하여 url을 바꾸어 프론트로 전달해줄 수 있다. 이런 경우 프론트에서 사용자에게 에러의 원인 등을 표시하기가 용이하다.

 

 

SecurityConfig에서는 유사하게 failureHandler만 추가해주면 된다.


private final AuthenticationFailureHandler customAuthenticationFailureHandler;

//중략

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
            .antMatchers("/", "/users", "user/login/**", "/login*").permitAll()
            .antMatchers("/mypage").hasRole("USER")
            .antMatchers("/manager").hasRole("MANAGER")
            .antMatchers("/admin").hasRole("ADMIN")
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .authenticationDetailsSource(authenticationDetailsSource)
            .loginPage("/login")
            .loginProcessingUrl("/login_proc")
            .defaultSuccessUrl("/")
            .successHandler(customAuthenticationSuccessHandler)
            .failureHandler(customAuthenticationFailureHandler)
            .permitAll()
    ;

}

 

 

Controller와 화면에서는 각각 아래와 같이 처리한다. 스프링 MVC 2편에서 배운 것처럼 @RequestParam은 생략해줘도 된다. model 객체에 error와 exception을 담아서 View로 전달해주면, view 에서 param.error, exception을 추출하여 화면에 보여주는 형태이다.

 

@GetMapping("/login")
public String login(@RequestParam(value = "error", required = false) String error,
               @RequestParam(value = "exception", required = false) String exception, Model model) throws Exception {

   model.addAttribute("error", error);
   model.addAttribute("exception", exception);

   return "login";
}

 

 

<div th:if="${param.error}" class="form-group">
    <span th:text="${exception}" class="alert alert-danger">잘못된 아이디나 암호입니다</span>
</div>

 

 

 


 

2. 접근 거부 처리 : Access Denied

 

인증은 되었으나 권한이 없어서 인가 예외를 발생시키는 경우에 대해 학습한다. 인가 예외는 인증 필터가 처리하는 것이 아니라 필터 중 맨 마지막의 AbstractSecurityInterceptor에 의해 발생된다(AccessDeniedException 발생). 그리고 이 exception의 처리는 ExceptionTranslationFilterAccessDeniedHandler에서 처리한다. 실습을 위해 아래와 같이 SecurityConfig에 exceptionHandling() 메서드와 accessDeniedHandler를 추가해준다.

 

※ 여태껏 .and()로 구분되는 것을 볼 수 있는데, 이것은 의미단위로 구분하는 구분자이다. 정확하게는 설정 정보를 xml 단위로 작성할 때, xml 태그의 구분 단위라고 보면 된다. 일단은 그냥 비슷한 기능을 하는 속성끼리 모아준다고 생각하자.

 

 

설정 파일 처리

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


@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
//                .antMatchers("/mypage").hasRole("USER")
                .antMatchers("/messages").hasRole("ROLE_MANAGER")
//                .antMatchers("/config").hasRole("ADMIN")
                .antMatchers("/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/login_proc")
                .defaultSuccessUrl("/")
                .authenticationDetailsSource(authenticationDetailsSource)
                .successHandler(customAuthenticationSuccessHandler)
                .failureHandler(customAuthenticationFailureHandler)
                .and()
                .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler())
        ;
    }

 

@Bean AccessDeniedHandler 부분은 아래 구현체 다음으로 설명한다.

 

 

AccessDeniedHandler

AccessDeniedException을 처리하는 accessDeniedHandler의 구현체를 작성한다.

public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private String errorPage;

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

    public void setErrorPage(String errorPage) {
        this.errorPage = errorPage;
    }
}

 

 

handle 메서드를 Override 하였다. 그리고 Url에 e.getMessage()를 통해서 에러가 발생한 원인을 클라이언트쪽으로 전달해주게 된다. AccessDeniedException의 default값은 "Access is Denied" 이다.

 

 

@Autowired 대신 @Bean 활용

CustomAccessDeniedHandler를 SecurityConfig 파일에서 @Autowired로 의존성 주입을 하는 것이 아니라 @Bean으로 만들어 주었다. 이것은 accessDeniedHandler를 그대로 불러와서 사용하는 것이 아니라 일단은 default 생성자로 생성하고, errorPage라는 필드값에 "/denied" 라는 url을 set 하는 로직을 직접 적용하기 때문이다.

 

다시 말해서, CustomAccessDeniedHandler에는 생성자는 없고 수정자인 setErrorPage만 있는데 여기에 errorPage를 파라미터로 하는 생성자를 만들면 된다. 그리고 SecurityConfig 설정 정보에서는 @Autowired를 통해서 CustomAccessDeniedHandler를 불러오면서 errorPage("/denied")를 인자로 넣어주면 되는 것이다.

 

 

View 설정

마지막으로 /denied url로 설정한 view 부분을 작성한다.

 

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head th:replace="layout/header::userHead"></head>
<body>
<div th:replace="layout/top::header"></div>

<div class="container">
    <div class="row align-items-start">
        <nav class="col-md-2 d-none d-md-block bg-light sidebar">
            <div class="sidebar-sticky">
                <ul class="nav flex-column">
                    <li class="nav-item">
                        <div style="padding-top:10px;" class="nav flex-column nav-pills" aria-orientation="vertical">
                            <a th:href="@{/}" style="margin:5px;" class="nav-link text-primary">홈</a>
                            <a th:href="@{/mypage}" style="margin:5px;" class="nav-link text-primary">마이페이지</a>
                            <a th:href="@{/messages}" style="margin:5px;" class="nav-link text-primary">메시지</a>
                            <a th:href="@{/config}" style="margin:5px;" class="nav-link text-primary">환경설정</a>
                        </div>
                    </li>
                </ul>
            </div>
        </nav>
        <div style="padding-top:50px;" class="col">
            <div class="container text-center">
                <h1><span th:text="${username}" class="alert alert-danger">접근 권한 없음</span></h1>
                <br />
                <h3 th:text="${exception}"></h3>
            </div>
        </div>
    </div>

</div>
<div th:replace="layout/footer::footer"></div>
</body>
</html>

 

맨 마지막 줄의 exception을 handler에서 전달받게 된다.

 

 

결과

 

.exceptionHandling() 과 .accessDeniedHandler() 처리가 되지 않으면 아래와 같이 Whitelabel Error Page가 보인다.

 

적용 후에는 View와 url이 변경되는 것을 확인할 수 있다.

 


 

참조

 

1) 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 정수원님 강의

https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0

728x90
반응형