[Spring Boot Tutorial] 8. AccessDeniedHandler 구현클래스로 인증&인가 Exception 핸들링

2019. 12. 12. 13:53Spring/Spring Boot Tutorial

반응형

[Spring Boot Tutorial] 7. JavaConfig 설정으로 Spring Security 커스터마이징에 이어서 진행됩니다.

이번 포스팅에서는 접근 권한 없는 페이지 접속에 관한 처리에 대해서 알아봅니다.
(다른 표현으로는 exceptionHandling 구성에 대한 설명이라고도 말할 수 있습니다.)


spring:
  security:
  user:
    name: admin
    password: admin
    roles:
    - ADMIN
    - VIEW

프로퍼티 파일에 설정했던 spring security 관련 설정에서, ROLE_ADMIN, ROLE_VIEW를 제거합니다.

그러고 나서 로그인을 시도하면 아래와 같은 에러페이지가 출력됩니다.

49

403 에러는 접근 권한 없는 url 요청 시 반환되는 응답코드입니다.

로그인 절차가 Spring Security의 인증(Authentication) 에 관한 처리였다면, Roles 보유에 따른 사용자의 접근 절차를 인가(Authorization) 에 관한 처리라고 보면 됩니다.

인가되지 않은(=권한을 가지지 않은) 사용자가 페이지에 접근할 때, 위와 같은 403 에러를 출력하는데,
만약, 403에러 페이지가 아닌 다른 처리를 하고 싶다면 어떻게 해야할까요?

이럴 때 이용하는 것이 바로 exceptionHandling 설정입니다.


먼저, demo페이지에서 이용되는 role에 관하여 정리하자면, 아래와 같습니다.

  • ROLE_VIEW: demo 사이트 접속 권한
  • ROLE_ADMIN: demo 사이트 중 회원 조회 권한

로그인 성공시, demo페이지에 접근하기 위해서는 ROLE_VIEW 권한이 있어야하며, demo페이지 중, 회원 탭에 접근하기 위해서는 ROLE_ADMIN 권한이 있어야 합니다.


보다 명확하게 실습하기 위해 '가맹점' 탭을 추가하였습니다.
url은 /v/stores 이고, 출력화면은 회원페이지와 유사합니다.

51

※ 자세한 코드는 GitHub를 참고해주세요.


본격적으로 인가 처리를 진행하기 앞서, 앞으로 진행할 절차에 대해 간략히 설명하자면 아래와 같습니다.

  1. ROLE_ADMIN 권한이 있을 경우에만 회원 탭 노출
    1-1) Spring Security JavaConfig에 인가 설정 추가
    1-2) view template에 sec xml namespace를 이용한 인가 설정 추가
  2. 보유 권한에 따른 페이지 이동
    2-1) AccessDeniedHandler 구현 클래스 설정
    2-2) redirect view페이지 및 컨트롤러 작성
    2-3) Spring Security JavaConfig에 accessDeniedHandler추가

1. ROLE_ADMIN 권한이 있을 경우에만 회원 탭 노출

회원 탭은 ROLE_ADMIN 권한을 보유한 사용자만 보이도록 하고 싶다면 어떻게 해야할까요?
front 부분에서 처리할 인가처리는 thymeleaf 의 sec xml namespace를 이용해서 처리합니다.

Spring security 맛보기 과정에서 인증(Authentication)된 사용자(=로그인 성공한 사용자)만 username을 출력되도록 할때에도 이 sec xml namespace를 썼었죠.

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
...
<th:block sec:authorize="isAuthenticated()">
  <span sec:authentication="principal.username"></span>님 반가워요!
</th:block>

위와 같이 인증된 사용자 일 경우에만 특정 element를 보여주도록 처리할 수도 있지만, Role을 보유한 사용자에만 (=인가된 사용자) 특정 element를 보여주도록 처리할 수도 있습니다.


프로퍼티 파일의 roles에서 ADMIN을 제거합니다. (ROLE_ADMIN 권한 삭제)

spring:
  security:
  user:
    name: admin
    password: admin
    roles:
    - VIEW

1-1) Spring Security JavaConfig에 인가 설정 추가

@Configuration
@EnableWebSecurity
public class WebSecurityConfig  extends WebSecurityConfigurerAdapter {

  ...

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
      .antMatchers("/", "/login").permitAll()
      .antMatchers("/v/users").access("hasRole('ROLE_ADMIN')")
      .antMatchers("/v", "/v/**").access("hasRole('ROLE_VIEW')")
      .anyRequest().authenticated()
      .and()
        .formLogin().loginPage("/login").defaultSuccessUrl("/v")
        .usernameParameter("username").passwordParameter("password")
      .and()
        .logout().invalidateHttpSession(true).deleteCookies("JSESSIONID")
      .and()
        .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
  }
}

Spring Security JavaConfig에 /v/users url은 ROLE_ADMIN 롤을 가진 사람만 접근할 수 있도록 설정을 추가합니다.


1-2) view template에 sec xml namespace를 이용한 인가 설정 추가

<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:th="http://www.thymeleaf.org"
	xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
	xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
...

<div class="collapse navbar-collapse" id="nav_menu_area">
  <ul class="navbar-nav" id="nav_menu" style="margin: 0 auto; width: 1140px;">
    <li class='nav-item'  th:classappend="${currentPage eq 'home'} ? 'active' : ''" th:attr="data-href=@{/v}"></li>
    <li sec:authorize="hasRole('ROLE_ADMIN')" class='nav-item' th:classappend="${currentPage eq 'user'} ? 'active' : ''" th:attr="data-href=@{/v/users}">회원</li>
    <li class='nav-item' th:classappend="${currentPage eq 'store'} ? 'active' : ''" th:attr="data-href=@{/v/stores}">가맹점</li>
  </ul>
</div>

레이아웃의 메뉴 요소에서 회원 메뉴는 ROLE_ADMIN 권한을 가진 사용자에게만 허가하도록 sec xml namespace 설정을 추가합니다.(ln 10)


52

변경사항이 적용된 demo페이지 입니다.
ROLE_ADMIN 권한을 가지고 있지 않기 때문에 회원 메뉴가 보이지 않습니다.


메뉴가 사라졌으니, ROLE_ADMIN 권한이 없는 사용자는 메뉴를 통한 회원 페이지 접속은 하지 못하게 되었습니다. 그러나, url을 통한 직접적인 접속이 이뤄진다면 어떻게 될까요?

53

ROLE_VIEW권한을 가지고 있지 않은 사용자가 로그인 시도했을 때 403에러 페이지가 출력되었던 것 처럼 똑같은 에러 페이지를 출력하네요.

WebSecurityConfigurerAdapter 추상클래스를 상속한 Spring Security JavaConfig 파일에서 /v/users url은 ROLE_ADMIN 권한을 보유한 사용자만 인가되도록 설정했기 때문입니다.

접근권한 없는 페이지에 접속했을시 행해질 동작에 대해 직접 커스터마이징 해보도록 합시다.


2. 보유권한에 따른 페이지 이동

  • ROLE_ADMIN 권한이 없는 사용자가 회원 페이지에 접속할시 탭으로 이동
  • ROLE_VIEW 권한이 없는 사용자가 로그인 시도할 시 로그아웃 처리

access 권한 없는 페이지에 접속했을 때 발생되는 AccessDeniedException을 처리해줄 exceptionHandling을 추가합니다.


2-1) AccessDeniedHandler 구현 클래스 설정

AccessDeniedHandler를 구현하는 클래스를 생성합니다.
override할 handle 메서드에 보유 권한별 동작을 정의할 수 있습니다.

@Component
public class WebAccessDeniedHandler implements AccessDeniedHandler {

    private static final Logger logger = LoggerFactory.getLogger(WebAccessDeniedHandler.class);

    @Override
    public void handle(HttpServletRequest req, HttpServletResponse res, AccessDeniedException ade)
            throws IOException, ServletException {        
        res.setStatus(HttpStatus.FORBIDDEN.value());

        if(ade instanceof AccessDeniedException) {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication != null &&
                    ((User) authentication.getPrincipal()).getAuthorities().contains(new SimpleGrantedAuthority("ROLE_VIEW"))) {
                req.setAttribute("msg", "접근권한 없는 사용자입니다.");
                req.setAttribute("nextPage", "/v");;
            } else {
                req.setAttribute("msg", "로그인 권한이 없는 아이디입니다.");
                req.setAttribute("nextPage", "/login");
                res.setStatus(HttpStatus.UNAUTHORIZED.value());
                SecurityContextHolder.clearContext();
            }
        } else {
            logger.info(ade.getClass().getCanonicalName());			
        }		
        req.getRequestDispatcher("/err/denied-page").forward(req, res);
    }
}

인증, 인가 거부된 요청시 이동될 페이지는 /err/denied-page 입니다.
msg와 nextPage attribute를 이용하여 alert를 띄우며 다음 페이지도 이동됩니다.

로그인 권한이 없는(=인증되지 않은) 사용자의 경우 401 http status code를 (ln 20) 응답하며 강제로 로그아웃 처리되도록 SecurityContextHolder를 clear한 후(ln 21), /login페이지로 redirect 합니다. (ln 19)

접근 권한이 없는(=인가되지 않은) 사용자의 경우 403 http status code를 응답하면서 (ln 9)
/v 페이지로 redirect 합니다.
(ROLE_ADMIN 권한을 가지고 있지 않은 사용자가 회원 메뉴에 접속했을 때에 관한 처리를 해주는 부분입니다.)

forward 방식으로 /err/denied-page로 이동시키고, 그 곳에서 javascript로 msg를 alert를 띄운 후, nextPage에 담긴 페이지로 redirect 시킵니다.


++ tip
바로 redirect를 할 경우, attribute를 담으려면 ?msg=메세지와 같이 url에 파라미터를 담아야 하는 불편함이 있으며,
바로 목적지 url로 forward만 할경우, url의 변동이 없다는 점이 있으므로 위와 같은 형태로 짜는 것을 권합니다.


2-2) redirect view페이지 및 컨트롤러 작성

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DEMO</title>
</head>
<body>
<script th:inline="javascript">
window.onload = function(){
/*<![CDATA[*/
    var nextPage = [[@{__${nextPage}__}]], msg = [[${msg}]];
    if(nextPage) {
        alert(msg);
        location.href = nextPage;
    } else {
        location.href = [[@{/login}]];
    }
/*]]>*/
};
</script>
</body>
</html>

redirect할 nextPage와 알림창에 띄울 msg를 변수에 받는 redirect 페이지입니다.
err 폴더 내의 deniedPage.html 에 파일을 생성했습니다.


55

54


@GetMapping(value = "/err/denied-page")
public String accessDenied(){
    return "err/deniedPage";
}

컨트롤러에 /login/denied-page url 요청시 위에 생성한 html 페이지로 이동하도록 메서드를 추가합니다.


2-3) Spring Security JavaConfig에 accessDeniedHandler추가

@Configuration
@EnableWebSecurity
public class WebSecurityConfig  extends WebSecurityConfigurerAdapter {

    private final WebAccessDeniedHandler webAccessDeniedHandler;

    @Autowired
    public WebSecurityConfig(WebAccessDeniedHandler webAccessDeniedHandler) {
        this.webAccessDeniedHandler = webAccessDeniedHandler;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/static/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/", "/login").permitAll()
            .antMatchers("/v/users", "/v/users/**").access("hasRole('ROLE_ADMIN')")
            .antMatchers("/v", "/v/**").access("hasRole('ROLE_VIEW')")
            .anyRequest().authenticated()
        .and()
            .formLogin().loginPage("/login").defaultSuccessUrl("/v", true)
            .usernameParameter("username").passwordParameter("password")
        .and()
            .logout().invalidateHttpSession(true).deleteCookies("JSESSIONID")
        .and().exceptionHandling().accessDeniedHandler(webAccessDeniedHandler)
        .and()
            .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}

WebAccessDeniedHandler를 Spring Security 관련 JavaConfig 생성자에 @Autowired 합니다.
그리고, accessDeniedHandler에 핸들러를 추가합니다. (ln 29)


GitHub에서 demo 프로젝트를 다운받아 볼 수 있습니다.


++ keyword

  • set up Javascript variables from Spring Model using Thymeleaf
  • Thymeleaf javascript 변수
  • Spring Boot 인증, 인가
  • Spring Boot Authorization, Authentication
  • how to use inline javascript in Thymeleaf
728x90
반응형