java-school logo
Last Modified 2016.10.2

스프링 시큐리티에서 접근 거부 상황 다루기

웹 요청 보안

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" %>
<%
String contextPath = request.getContextPath();
%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>403</title>
<link rel="stylesheet" href="<%=contextPath %>/css/screen.css" type="text/css" />
<script type="text/javascript" src="<%=contextPath %>/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="url-navi">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을 보게 된다.

관련 글