스프링 시큐리티 - 접근 거부 핸들링
웹 요청 보안
ROLE_USER 권한만 가진 사용자가 http://localhost:8080/admin를 요청할 때 /WEB-INF/views/403.jsp 페이지로 포워딩하는 방법
security.xml
<http> <access-denied-handler error-page="/403" /> <intercept-url pattern="/users/bye_confirm" access="permitAll"/> <intercept-url pattern="/users/welcome" access="permitAll"/> <intercept-url pattern="/users/signUp" access="permitAll"/> <intercept-url pattern="/users/login" access="permitAll"/> <intercept-url pattern="/images/**" access="permitAll"/> <intercept-url pattern="/css/**" access="permitAll"/> <intercept-url pattern="/js/**" access="permitAll"/> <intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')"/> <intercept-url pattern="/users/**" access="hasAnyRole('ROLE_ADMIN','ROLE_USER')"/> <intercept-url pattern="/bbs/**" access="hasAnyRole('ROLE_ADMIN','ROLE_USER')"/> <!-- 생략 -->
<access-denied-handler error-page="/403" />만으로 권한이 없는 사용자를 /WEB-INF/views/403.jsp 페이지로 보내지 않는다.
컨트롤러에서 매핑하지 않으면, 결국 http://localhost:8080/403을 요청하게 되고, web.xml에서 설정한 404 에러 페이지를 보게 된다.
WEB-INF/views/403.jsp 파일을 만들고 HomeController에 다음 메소드를 추가한다.
스프링 시큐리티 태그를 사용할 수 있으므로 header.jsp 파일을 인클루드하고 있다.
/403.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%> <%@ page import="net.java_school.user.User" %> <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8" /> <title>403</title> <link rel="stylesheet" href="/css/screen.css" type="text/css" /> <script type="text/javascript" src="/js/jquery-3.2.1.min.js"></script> </head> <body> <div id="wrap"> <div id="header"> <%@ include file="inc/header.jsp" %> </div> <div id="main-menu"> <%@ include file="inc/main-menu.jsp" %> </div> <div id="container"> <div id="content" style="min-height: 800px;"> <div id="content-categories">Error</div> <h1>403</h1> Access is Denied. </div> </div> <div id="sidebar"> <h1>Error</h1> </div> <div id="extra"> <%@ include file="inc/extra.jsp" %> </div> <div id="footer"> <%@ include file="inc/footer.jsp" %> </div> </div> </body> </html>
HomeController.java
@RequestMapping(value="/403", method={RequestMethod.GET,RequestMethod.POST}) public String error403() { return "403"; }
참고로, 다음은 web.xml의 에러 페이지 설정이다.
web.xml
<error-page> <error-code>404</error-code> <location>/WEB-INF/views/404.jsp</location> </error-page> <error-page> <error-code>500</error-code> <location>/WEB-INF/views/500.jsp</location> </error-page>
mvn clean compile war:inplace로 컴파일하고,
톰캣을 재실행한 후 http://localhost:8080/admin을 요청한다.
로그인한 사용자가 ROLE_USER 권한만 가진 사용자라면 /WEB-INF/views/403.jsp가 보일 것이다.
AccessDeniedHandler 구현
접근 권한이 없어 에러 페이지로 이동하는 상황에서 수행해야 할 비즈니스 로직이 있다면
org.springframework.security.web.access.AccessDeniedHandler를 구현해야 한다.
security.xml 파일을 다음과 같이 수정한다.
security.xml
<access-denied-handler ref="my403" />
security.xml에 다음을 추가한다.
security.xml
<beans:bean id="my403" class="net.java_school.spring.MyAccessDeniedHandler"> <beans:property name="errorPage" value="403" /> </beans:bean>
security.xml에 추가한 설정대로 AccessDeniedHandler를 구현하는 MyAccessDeniedHandler를 생성한다.
MyAccessDeniedHandler.java
package net.java_school.spring; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; public class MyAccessDeniedHandler implements AccessDeniedHandler { private String errorPage; public void setErrorPage(String errorPage) { this.errorPage = errorPage; } @Override public void handle(HttpServletRequest req, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException { //TODO 수행할 비즈니스 로직 req.getRequestDispatcher(errorPage).forward(req, resp); } }
web.xml 설정으로 보이는 에러 페이지는 스프링 시큐리티 태그가 작동하지 않는다. 이유는 스프링 필터 체인에서 뷰 레벨 보안이 적용하기 위한 필터가 작동하기 전에 에러 페이지로 이동하기 때문이다.
메소드 보안
스프링 MVC에서 익셉션과 에러 페이지를 매핑하는 방법 중 SimpleMappingExceptionResolver를 사용하는 것이 가장 간단하다. 아래는 org.springframework.security.access.AccessDeniedException 익셉션이 발생할 때 error-403으로 매핑한다. 그 외 다른 익셉션이 발생하면 error로 매핑한다. 매핑은 우리가 설정한 뷰 리졸버에 의해 각각 /WEB-INF/views/error-403.jsp와 /WEB-INF/views/error.jsp로 해석된다. 컨트롤러가 이들을 매핑할 필요는 없다.
spring-bbs-servlet.xml
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <property name="defaultErrorView" value="error" /> <property name="exceptionMappings"> <props> <prop key="AccessDeniedException"> error-403 </prop> </props> </property> </bean>
im@gmail.org/1111로 로그인하고 탈퇴메뉴에서 hong@gmail.org와 1111를 입력하여 탈퇴를 시도하면
UserService.java
@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER') and #user.email == principal.username") public void bye(User user);
위의 강조된 부분이 작동하여 org.springframework.security.access.AccessDeniedException 익셉션이 발생하여 결국 /WEB-INF/views/error-403.jsp을 보게 된다. 다시 탈퇴메뉴에서 이번에는 im@gmail.org와 비밀번호를 틀리게 입력하면
UserServiceImpl.java
@Override public void bye(User user) { String encodedPassword = this.getUser(user.getEmail()).getPasswd(); boolean check = this.bcryptPasswordEncoder.matches(user.getPasswd(), encodedPassword); if (check == false) { throw new AccessDeniedException("비밀번호가 틀립니다."); } userMapper.deleteAuthority(user.getEmail()); userMapper.delete(user); }
위의 강조된 부분이 작동하여 org.springframework.security.access.AccessDeniedException 익셉션이 발생하여 결국 /WEB-INF/views/error-403.jsp을 보게 된다.