스프링 시큐리티 - 접근 거부 핸들링
웹 요청 보안
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.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 jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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을 보게 된다.
