2019. 12. 12. 13:53ㆍSpring/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
를 제거합니다.
그러고 나서 로그인을 시도하면 아래와 같은 에러페이지가 출력됩니다.
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
이고, 출력화면은 회원페이지와 유사합니다.
※ 자세한 코드는 GitHub를 참고해주세요.
본격적으로 인가 처리를 진행하기 앞서, 앞으로 진행할 절차에 대해 간략히 설명하자면 아래와 같습니다.
- ROLE_ADMIN 권한이 있을 경우에만
회원
탭 노출
1-1) Spring Security JavaConfig에 인가 설정 추가
1-2) view template에 sec xml namespace를 이용한 인가 설정 추가 - 보유 권한에 따른 페이지 이동
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)
변경사항이 적용된 demo페이지 입니다.
ROLE_ADMIN 권한을 가지고 있지 않기 때문에 회원 메뉴가 보이지 않습니다.
메뉴가 사라졌으니, ROLE_ADMIN
권한이 없는 사용자는 메뉴를 통한 회원 페이지 접속은 하지 못하게 되었습니다. 그러나, url을 통한 직접적인 접속이 이뤄진다면 어떻게 될까요?
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 에 파일을 생성했습니다.
@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
'Spring > Spring Boot Tutorial' 카테고리의 다른 글
[Spring Boot Tutorial] 10. 회원가입 화면 만들기 (6) | 2020.03.11 |
---|---|
[Spring Boot Tutorial] 9. JDBC 기반 Spring Security 인증&인가 (0) | 2020.01.31 |
[Spring Boot Tutorial] 7. JavaConfig 설정으로 Spring Security 커스터마이징 (2) | 2019.11.29 |
[Spring Boot Tutorial] 6. Thymeleaf layout 설정하기 (3) | 2019.10.30 |
[Spring Boot Tutorial] 5. Thymeleaf 템플릿 엔진 이용하기 (0) | 2019.10.21 |