Wednesday, April 16, 2014

Spring Security 3.2 Authorization - Part 2

In this post we will add a custom access denied (403) page.  The code in this post will build on the last post in this series, Spring Security 3.2 Authorization - Part 1.

1. We will start by creating our custom error page in WEB-INF/views/error in Eclipse. We will name our html file 403.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <!-- Core CSS - Include with every page -->
    <link type="text/css" rel="stylesheet" href="../../../resources/css/bootstrap-3.1.1.min.css" 
        data-th-href="@{/resources/css/bootstrap-3.1.1.min.css}" />
        
    <link type="text/css" rel="stylesheet" href="../../../resources/font-awesome/css/font-awesome.css" 
        data-th-href="@{/resources/font-awesome/css/font-awesome.css}" />

    <!-- SB Admin CSS - Include with every page -->
    <link type="text/css" rel="stylesheet" href="../../../resources/css/sb-admin.css" 
        data-th-href="@{/resources/css/sb-admin.css}" />
    
    <style>
        .no-border-on-me>thead>tr>th,
        .no-border-on-me>tbody>tr>th,
        .no-border-on-me>tfoot>tr>th,
        .no-border-on-me>thead>tr>td,
        .no-border-on-me>tbody>tr>td,
        .no-border-on-me>tfoot>tr>td
        {
            border-top-style: none;
            border-bottom-style: none;
        }
    </style>

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
          <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
          <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
        <![endif]-->

    <title>403 - Not Authorized</title>
</head>

<body>

    <div class="container">
        <div class="row">
            <div class="col-md-6 col-md-offset-3">
                <div align="center"><h1>403 - Not Authorized</h1></div>
                <div align="center"><p>You are not authorized to access this page</p></div>
            </div>
        </div>
    </div>

</body>
</html>

2. The next step is to create a class to handle the access denied exception. This class is shown below
package com.dtr.oas.controller;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

public class AccessDeniedExceptionHandler implements AccessDeniedHandler {
    static Logger logger = LoggerFactory.getLogger(AccessDeniedExceptionHandler.class);
    
    private String errorPage;
    
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException arg2) throws IOException, ServletException {

        logger.debug(String.format("### AccessDeniedHandler: URL [%s] ", request.getServletPath()));
        logger.debug("### Error page: [" + errorPage + "]");
        request.getRequestDispatcher(errorPage).forward(request, response);
    }

    public String getErrorPage() {
        return errorPage;
    }

    public void setErrorPage(String errorPage) {
        this.errorPage = errorPage;
    }
}
This class implements AccessDeniedHandler, and is required to have an implementation of the handle() method. The getter() and setter() methods will be used to get and set the target page associated with the configuration file.


3. We will now add this class as a bean to our root configuration file called DatabaseConfig. In addition, we will update this configuration file to scan all of the packages in "com.dtr.oas". This is needed because the AccessDeniedExceptionHandler class is in the controller package that had been excluded from scanning in the DatabaseConfig file. The updated file is shown below, with changes highlighted.
package com.dtr.oas.config;
import java.util.Properties;
import javax.annotation.Resource;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.hibernate4.HibernateTransactionManager;
import org.springframework.orm.hibernate4.LocalSessionFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import com.dtr.oas.controller.AccessDeniedExceptionHandler;

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {"com.dtr.oas"})
@PropertySource("classpath:database.properties")
public class DatabaseConfig {

    private static final String PROPERTY_NAME_DATABASE_DRIVER   = "db.driver";
    private static final String PROPERTY_NAME_DATABASE_PASSWORD = "db.password";
    private static final String PROPERTY_NAME_DATABASE_URL      = "db.url";
    private static final String PROPERTY_NAME_DATABASE_USERNAME = "db.username";

    private static final String PROPERTY_NAME_HIBERNATE_DIALECT              = "hibernate.dialect";
    private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL             = "hibernate.show_sql";
    private static final String PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN = "entitymanager.packages.to.scan";

    @Resource
    private Environment env;
    
    @Bean 
    AccessDeniedExceptionHandler accessDeniedExceptionHandler() {
        AccessDeniedExceptionHandler accessDeniedExceptionHandler = new AccessDeniedExceptionHandler();
        accessDeniedExceptionHandler.setErrorPage("/error/accessDeniedPage");
        return accessDeniedExceptionHandler;
    }
    
    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();

        dataSource.setDriverClassName(env.getRequiredProperty(PROPERTY_NAME_DATABASE_DRIVER));
        dataSource.setUrl(env.getRequiredProperty(PROPERTY_NAME_DATABASE_URL));
        dataSource.setUsername(env.getRequiredProperty(PROPERTY_NAME_DATABASE_USERNAME));
        dataSource.setPassword(env.getRequiredProperty(PROPERTY_NAME_DATABASE_PASSWORD));

        return dataSource;
    }

    private Properties hibProperties() {
        Properties properties = new Properties();
        properties.put(PROPERTY_NAME_HIBERNATE_DIALECT, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT));
        properties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL));
        return properties;
    }

    @Bean
    public HibernateTransactionManager transactionManager() {
        HibernateTransactionManager transactionManager = new HibernateTransactionManager();
        transactionManager.setSessionFactory(sessionFactory().getObject());
        return transactionManager;
    }

    @Bean
    public LocalSessionFactoryBean sessionFactory() {
        LocalSessionFactoryBean sessionFactoryBean = new LocalSessionFactoryBean();
        sessionFactoryBean.setDataSource(dataSource());
        sessionFactoryBean.setPackagesToScan(env.getRequiredProperty(PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN));
        sessionFactoryBean.setHibernateProperties(hibProperties());
        return sessionFactoryBean;
    }
}
As you can see above, we've set the error page to "/error/accessDeniedPage". This path will be handled by a new controller that we will create in a later step.


4. We will now update the security configuration file to use our custom access denied handler. We've autowired our access denied handler bean (defined in step 3), and configured the HttpSecurity to use this bean as our accessDeniedHandler. We've also allowed open access to links that start with "/error/**". The new code is highligthed below.
package com.dtr.oas.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import com.dtr.oas.controller.AccessDeniedExceptionHandler;

@Configuration
@EnableWebMvcSecurity
@ComponentScan(basePackageClasses=com.dtr.oas.service.UserServiceImpl.class)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    public void configureGlobal(UserDetailsService userDetailsService, AuthenticationManagerBuilder auth) throws Exception {
        auth
            .userDetailsService(userDetailsService);
    }
   
    @Autowired
    AccessDeniedExceptionHandler accessDeniedExceptionHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/resources/**").permitAll()
                .antMatchers("/error/**").permitAll()
                .antMatchers("/strategy/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .permitAll()
                .and()
            .logout()
                .permitAll()
                .and()
            .exceptionHandling()
                .accessDeniedHandler(accessDeniedExceptionHandler);
    }
}

5. Now we will add a controller to handle URL requests for "/error/accessDeniedPage". This controller will forward requests for this URL to our 403.html page.
package com.dtr.oas.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class ErrorController {
    static Logger logger = LoggerFactory.getLogger(ErrorController.class);
    
    @RequestMapping(value = {"/error/accessDeniedPage"}, method = RequestMethod.GET)
    public String sendTo403(Model model) {
        logger.debug("IN: /error/403-GET");
        return "error/403";
    }              
}

6. We are finished with the code for this example. The directory structure will now look like the following.


7. If we log in as the user "user" and attempt to access the strategy pages, we should now see our custom error page shown below.


In the next post, we will add role based authorization to the strategy controller.

Code at GitHub: https://github.com/dtr-trading/spring-ex12-auth

No comments:

Post a Comment