Last Modified 2021.11.29

Spring Security - Handling access denial

Add the following to redirect access denied user requests to the /WEB-INF/views/403.jsp page.

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')"/>
	
  <!-- omit -->
	  

You need to declare a handler for "/403" in the controller with the above configuration. Otherwise, you will get a 404 error.

Create a 403.jsp file and add the following method to your HomeController.

/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="en">
<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";
}

Exclude 403 error from error page configuration in 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>

Run mvn clean compile war:inplace, rerun Tomcat, and visit http://localhost:8080/admin. If the user has only the ROLE_USER privilege, /WEB-INF/views/403.jsp will be displayed.

Implementing AccessDeniedHandler

If you have business logic to perform before showing the user an error page, you should configure your access denied handler by implementing the org.springframework.security.web.access.AccessDeniedHandler.

Create a MyAccessDeniedHandler that implements AccessDeniedHandler.

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: business logic
    req.getRequestDispatcher(errorPage).forward(req, resp);
  }
}

Add the following to security.xml.

security.xml
<beans:bean id="my403" class="net.java_school.spring.MyAccessDeniedHandler">
    <beans:property name="errorPage" value="403" />
</beans:bean>

Modify the security.xml file as follows:

security.xml
<access-denied-handler ref="my403" />

The Spring Security tag does not work for error pages set in web.xml because the request is forwarded to these error pages before the view-level security filter works.

Method Security

The simplest way to map an exception to an error page in Spring MVC is to use the SimpleMappingExceptionResolver. The following configuration maps to error-403 when the org.springframework.security.access.AccessDeniedException occurs, and to error when any other exception occurs. The view resolver interprets error-403 and error as /WEB-INF/views/error-403.jsp and /WEB-INF/views/error.jsp respectively.

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>

After signing up and logging in with janedoe@gmail.org/1111, try to withdraw membership with johndoe@gmail.org/1111 from the Bye menu.

UserService.java
@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER') and #user.email == principal.username")
public void bye(User user);

The highlighted part above will work and occur an org.springframework.security.access.AccessDeniedException, and you will see /WEB-INF/views/error-403.jsp.

On the Bye page, try to withdraw membership with janedoe@gmail.org and the wrong password.

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("Wrong password!");
  }
	
  userMapper.deleteAuthority(user.getEmail());
  userMapper.delete(user);
}

The highlighted part above will occur an org.springframework.security.access.AccessDeniedException, and you will see /WEB-INF/views/error-403.jsp.