Tuesday, April 15, 2014

Spring Security 3.2 Authorization - Part 1

In this next series of posts, we will add role based authorization to the CRUD application we've been building.  The code in this post will build on the latest version of the application that we completed in the last post, Spring MVC 4 & Spring Security 3.2 - Part 3.

1. We will start by updating our security configuration to authorize access to the links containing the pattern "/strategy/**".  We will restrict access to these pages to users with the role "ROLE_ADMIN".  In the configuration, we can drop the prefix "ROLE".  Here is the updated security configuration file.
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;

@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);
    }
   
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/resources/**").permitAll()
                .antMatchers("/strategy/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }
}
Access to our strategy pages is now limited to uses in the ROLE_ADMIN.


2. To make our example look a little nicer, we will create home pages based on the users role. At this time, we will make them all look about the same, but with slightly different navigation bars. The nav bars are controlled by the Thymeleaf fragments. In total, we will have one home page html file for each role, and one fragment html file for each role. The directory structure is shown below.


3. The home page for users with the "ROLE_USER" will look like:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org">
    
<head data-th-fragment="header">
    <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 data-th-text="#{user.page.title}">Title</title>
</head>

<body>

<div id="wrapper">                <!-- /#wrapper -->

    <div data-th-replace="fragments/sb-user :: top-nav"></div>
    
    <div data-th-replace="fragments/sb-user :: vert-nav"></div>

    <div id="page-wrapper">
        <div class="row">
            <div class="col-lg-12">

                <h1 data-th-text="#{user.page.title}">Title</h1>
                <p>
                    <a href="strategy/">Strategy list</a><br />
                </p>

            </div>  <!-- /.col-lg-12 -->                
        </div>      <!-- /.row -->              
    </div>          <!-- page wrapper -->
</div>              <!-- /#wrapper -->

        
    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script type="text/javascript" src="../../resources/js/jquery-1.11.0.min.js" 
        data-th-src="@{/resources/js/jquery-1.11.0.min.js}"></script>
        
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script type="text/javascript" src="../../resources/js/bootstrap-3.1.1.min.js" 
        data-th-src="@{/resources/js/bootstrap-3.1.1.min.js}"></script>

    <!-- Core Scripts - Include with every page -->
    <script type="text/javascript" src="../../resources/js/plugins/metisMenu/jquery.metisMenu.js" 
        data-th-src="@{/resources/js/plugins/metisMenu/jquery.metisMenu.js}"></script>
        
    <!-- SB Admin Scripts - Include with every page -->
    <script type="text/javascript" src="../../resources/js/sb-admin.js" 
        data-th-src="@{/resources/js/sb-admin.js}"></script>

</body>
</html>
Notice the highlighted rows above:
  • The head section is no different than the admin users home page, other than the title
  • The included top-nav is now coming from the sb-user fragment (sb-user.html), rather than the sb-admin fragment.
  • The included vert-nav is now coming from the sb-user fragement (sb-user.html), rather than the sb-admin fragment.
  • The link to the strategy list is still on the page at this time so that we can test our authorization update.


4. The updated fragment is shown below.
<!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 >SB-User</title>   
</head>

<body>
    <div id="wrapper">

        <nav data-th-fragment="top-nav" class="navbar navbar-default navbar-static-top" style="margin-bottom: 0">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".sidebar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">Option Algo System</a>
            </div>
            <!-- /.navbar-header -->

            <ul class="nav navbar-top-links navbar-right" data-th-with="currentUser=${#httpServletRequest.userPrincipal?.name}">
                    <li><a href="#" data-th-href="@{/}"            >Home</a></li>
                    <li class="dropdown" data-th-if="${currentUser != null}">
                            <a class="dropdown-toggle" data-toggle="dropdown" href="#">
                                <i class="fa fa-user fa-fw"></i>
                                <font color="#049cbd" th:text="'&nbsp;' + ${currentUser} + '&nbsp;&nbsp;'">&nbsp;Dave&nbsp;&nbsp;</font>
                                <i class="fa fa-caret-down"></i>
                            </a>
                            <ul class="dropdown-menu dropdown-user">
                                <li><a href="#"><i class="fa fa-user fa-fw"></i>User Profile</a></li>
                                <li><a href="#"><i class="fa fa-gear fa-fw"></i>Settings</a></li>
                                <li class="divider"></li>
                                <li>
                                    <form class="navbar-form" data-th-action="@{/logout}" method="post">
                                       <label for="mySubmit" class="btn"><i class="fa fa-sign-out fa-fw"></i>Log Out</label>
                                       <input id="mySubmit" type="submit" value="Go" class="hidden" />
                                    </form>
                                </li>
                            </ul>
                            <!-- /.dropdown-user -->
                    </li>
            </ul>  <!-- /.navbar-top-links -->
        </nav>     <!-- /.navbar-static-top -->

        <nav data-th-fragment="vert-nav" class="navbar-default navbar-static-side" >
            <div class="sidebar-collapse">
                <ul class="nav" id="side-menu">
                    <li>
                        <a href="/accounts"><i class="fa fa-tasks fa-fw"></i> Accounts</a>
                    </li>
                </ul> <!-- /#side-menu -->
            </div>    <!-- /.sidebar-collapse -->
        </nav>        <!-- /.navbar-static-side -->

        <div id="page-wrapper">
            <div class="row">
                <div class="col-lg-12">
                    <h1 class="page-header">Blank</h1>
                </div>
                <!-- /.col-lg-12 -->
            </div>
            <!-- /.row -->
        </div>
        <!-- /#page-wrapper -->

    </div>
    <!-- /#wrapper -->

    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="../../../resources/js/jquery-1.11.0.min.js" 
        data-th-src="@{/resources/js/jquery-1.11.0.min.js}"></script>
        
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script src="../../../resources/js/bootstrap-3.1.1.min.js" 
        data-th-src="@{/resources/js/bootstrap-3.1.1.min.js}"></script>

    <!-- Core Scripts - Include with every page -->
    <script src="../../../resources/js/plugins/metisMenu/jquery.metisMenu.js" 
        data-th-src="@{/resources/js/plugins/metisMenu/jquery.metisMenu.js}"></script>
        
    <!-- SB Admin Scripts - Include with every page -->
    <script src="../../../resources/js/sb-admin.js" 
        data-th-src="@{/resources/js/sb-admin.js}"></script>

</body>
</html>
The hightlighted rows above are the sections that drive the role based menus.


5. The last step is to update the controller to direct a user to a specific home page based on their role. The controller is shown below.
package com.dtr.oas.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import com.dtr.oas.model.User;

@Controller
public class LinkController {
    static Logger logger = LoggerFactory.getLogger(LinkController.class);

    @RequestMapping(value = "/")
    public String mainPage() {
        String rolename = getRole();
        logger.debug("Directing to home page for: [" + rolename + "]");

        if (rolename.equals("ROLE_ADMIN")) {
            return "home-admin";
        } else if (rolename.equals("ROLE_TRADER")) {
            return "home-trader";
        } else {
            return "home-user";
        }
    }

    @RequestMapping(value = "/index")
    public String indexPage() {
        return "redirect:/";
    }
    
    private String getRole() {
        String rolename = "";
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (principal instanceof User) {
            rolename = ((User) principal).getRole().getRolename();
        } else {
            logger.error("Principal is not an instance of com.dtr.oas.model.User");
        }
        return rolename;
    }

}
A user cannot get to their home page (or any page other than the login page) without being authenticated. The link controller will handle requests for "/" and "/index" and will determine a users role at this request. Based on the user's role, the user will be directed to the home page for their role.


6. Screen shots of each home page are shown below.




7. If the user with the role "ROLE_USER" clicks on the strategy list link, they will receive an access denied (403) message as shown below. The user with the role "ROLE_ADMIN" will be allowed to access the strategy pages.


In the next post, we will modify the access denied (403) page.

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

2 comments:

Unknown said...

Thank you very much for all these tutorial...

btw i found a mistake (i think) in source codes that drove me crazy:
in some pages, jquery-1.11.0.min.js, bootstrap-3.1.1.min.js, jquery.metisMenu.js, sb-admin.js
couldn't be found.

I think you should use data-th-src instead data-th-href

Result:

src="../../resources/js/jquery-1.11.0.min.js" data-th-src="@{/resources/js/jquery-1.11.0.min.js}"

src="../../resources/js/bootstrap-3.1.1.min.js" data-th-src="@{/resources/js/bootstrap-3.1.1.min.js}"

src="../../resources/js/plugins/metisMenu/jquery.metisMenu.js" data-th-src="@{/resources/js/plugins/metisMenu/jquery.metisMenu.js}"

src="../../resources/js/sb-admin.js" data-th-src="@{/resources/js/sb-admin.js}"

Dave R. said...

Nice catch.

You are correct that the src tag should be used rather than the href tag. You will notice that nearly all of my Thymeleaf files use the correct src tag.

These typos are isolated to the three fragment files, the login file and the home file. The three fragment files are never served by the web server directly (since they are fragments), but you may have seen issues with the home file.

I'll update the source files when shortly.

Thanks!

Dave

Post a Comment