Overview of This Lesson

Refresh this page because I am probably still making changes to it.

In the previous lesson we did an overview of web application security concepts and terminology, including the different kinds of authentication available in Java web application. We wrote a very simple application that used Basic Authentication that included a SecurityWebApplicationInitializer class and a SecurityConfig class. Recall the following method we added to the SecurityConfig, which changed a few of the default security settings:

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().passwordEncoder(NoOpPasswordEncoder.getInstance())
            .withUser("Foo").password("4444").roles("USER");
}

This method set the in-memory authentication's password encoding to NO password encoding at all (remember, this is something you would NEVER do, we only did it for demonstration purposes; we should always use a password encoder) and then we added one user to the in-memory security realm: that user has the user name "Foo", a password of 4444 and was added to the USER role (which Spring stores as ROLE_USER).

In this lesson, we'll learn about form authentication by using our own custom login form. We'll also set aside some parts of our application to be accessed only by authenticated users and we'll create a "permission denied" error page. We're going to see some new methods and some cool new things that Spring does automatically when handling authentication and authorization!

Pre-Requisites

Before doing this tutorial, make sure you've gone through the Overview of Spring Security lesson, first.

Resources for This Lesson

Adding a Custom Form

So let's get right into it. First, we'll start a new project and add a custom form to it.

  1. Start up a new project and add Spring Web, Dev Tools, Thymeleaf, and Spring Security.
  2. Add an index page in the root of your templates directory. Give it a title and heading (e.g. Main Index) and add a link to "/secure" using href and th:href. For example:
    <h1>Main Index</h1>
    
    <p><a href="/secure" th:href="@{/secure}">Access secured pages</a>.</p>
                        
  3. Add a login.html form to your /templates. Set the title and give it an appropriate heading.
  4. Add a login form to the page. The form must have the following characteristics/elements:
    • It must perform a POST to the URL /login
    • The field for user name must be given the name="" attribute value "username". Emails are commonly used as user names these days, so we'll use an email field.
    • The field for password must be given the name="" attribute value "password"
    • It must include a submit button.
    <form method="post" th:action="@{/login}">
        <p>
          <label for="email">Email: 
            <input type="email" id="email" name="username">
          </label>
          <br>
          <label for="pass">Password: 
            <input type="password" id="pass" name="password">
          </label>
        </p>
        <p><input type="submit" name="login" value="Log In"></p>
    </form>
  5. In addition to the form on your login page, add a link (inside a block element, of course) back to a main index page at the root (/).
  6. Add 2 block elements above the form. These are going to display certain messages in specific circumstances:
    <div th:if="${param.error}" class="error">Invalid user name or password.</div>
    <div th:if="${param.logout}">You have been logged out.</div>

There is a lot of cool functionality built into this form:

Now let's add some content to our project that only certain authenticated users can access:

  1. Create a directory called /secure in your templates root.
  2. Add an index.html page to the /secure directory:
    • Give it a meaningful title and heading.
    • Add a link back to the index page (fallback and thymeleaf) and a log out link (fallback and thymeleaf, goes to /logout).

Since we're restricting access to some pages, we should create an "Access Denied" page:

  1. Add an /error directory in the templates root.
  2. Add a page inside /error called "permissionDenied.html" (or something similar)
    • Add a meaningful title and heading.
    • Add links back to the main index page and a log out link, as on the secure page.

Now we need controller methods to load each of our pages:

Here's how our program is going to work:

We already know how to do some of these things, but not others. For example, how do we log a "permission denied" request and send the user to the /error page we created? In the next sections, we'll look at how to perform each of these tasks.

Handling Access Denied

First, let's look at how to deal with a user who is denied access to the application's secure pages.

There is an interface called the AccessDeniedHandler interface which handles an AccessDeniedException (this exception is thrown when authorization fails on a requested resource). When our GUEST user logs in upon requesting /secure resources, they will not be authorized, so the AccessDeniedException will occur. The AccessDeniedHandler has a handle() method where we can put code that should execute when the AccessDeniedException occurs.

We will create a class of our own that implements this AccessDeniedHandler. This class should go in our .security package and it needs to include the @Component interface because we're going to @Autowired it into our SecurityConfig class later:

// NOTE: in .security package
@Component
public class LogAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException, ServletException {
        
        // log access-denied exceptions and redirect to permission denied page
    }
}

As you saw from reading the documentation for the handle() method, there are parameters for the request that was being made and the response that is going to be sent back. We'll use both of these in our method's code.

So, we need to ensure that there is in fact an authenticated user, or we won't be able to log their details. We can retrieve this information from the current SecurityContext. A Security Context contains security information about the current thread of execution (in this case, the current request being processed). It's getAuthentication() method gets the currently authenticated principal (recall from the previous lesson that the principal = currently logged-in user).

Authentication auth = SecurityContextHolder.getContext().getAuthentication();

We use the SecurityContextHolder class to retrieve the security context. We talked about the Authentication interface (the one in the package orspringframework.security.core) in the previous lesson.

If there is no authentication instance, then there is no authenticated user. We need to make sure there is an instance so we don't end up with null pointer exceptions when we try to invoke methods on the authentication object (such as .getName()). If there is an instance, log the user's name and the resource they were trying to access to the console:

if (auth != null) {
System.out.printf("%s was trying to access protected resource\n%s\n",
        auth.getName(), request.getRequestURI());
}

The request.getRequestURI() retrieves the URI/URL that was requested when the exception occurred.

In a more sophisticated program, you would probably log these details to a file or even a database table, but this is ok for now.

Lastly, we want to redirect the user to the permission denied page we added to the /error directory:

response.sendRedirect(request.getContextPath() + "/permissionDenied");

Here, we are using the response object's sendRedirect() method, which you can probably guess redirects the request to a different page or resource. In this case, we're retrieving the context path (in this case, that would be localhost:8080) and concatenating the string literal for our "permission denied" mapping. The literal should match the @GetMapping you used to load the permissionDenied.html page in the /error directory.

That's it for now. We'll use this class in our SecurityConfig when we want to configure what should happen when an exception occurs during authorization.

the code for the log access denied handler class
The LogAccessDeniedHandler class

Configuring Security

The next task in our development of this application is to add our SecurityConfig class. It will be a bit different this time because even though it still needs the @EnableWebSecurity annoation, it's also going to extend WebSecurityConfigurerAdapter. Creating our SecurityConfig as a child of WebSecurityConfigurerAdapter is going to allow us to add even more security configurations than we did before, and we'll be able to do it in a very convenient way (it may not seem convenient at first, but trust me that this is going to be much easier than it was before we had Spring!) We're also going to Autowire in an instance of the LogAccessDeniedHandler component we just created.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private LogAccessDeniedHandler accessDeniedHandler;

}

WebSecurityConfigureAdapter has a series of overloaded configure() methods that allow us to configure several different sets of security properties. Here's what we're going to set up:

For the first set of tasks, we're going to override one of the configure() methods that takes an HttpSecurity parameter. HttpSecurity allows to you configure security settings that are specific to HTTP requests. For example, we can use it to set up authorization for specific requests (like requests for anything in the /secure directory).

@Override
protected void configure(HttpSecurity http) throws Exception {

}

The HttpSecurity class has a method called .authorizeRequests() that allows you to set up constraints for specific resources. We can set up authorization for pages and files, or groups of pages and files, by using an AntMatcher. An AntMatcher pattern is just like a URL Pattern and they share the same syntax, for the most part:

We can set up ant matchers using the .antMatchers() method chained to the authorizeRequests() method (feel free to have a look at the authorizeRequests() method in the docs if you want to learn more about what this method returns). For example, we can restrict the /secure resources to only users in the USER role by writing code such as:

@Override
protected void configure(HttpSecurity http) throws Exception {
    
    http.authorizeRequests().antMatchers("/secure/**").hasRole("USER");
}

This code says that when a request is made to anything in the /secure directory, make sure that the authenticated user is a member of the USER role.

The antMatcher() method accepts a variable-length arument list of Strings representing the patterns that you want to apply a constraint to. These apply to either the GET or POST method. It ignores any query string in the URL when it matches the patterns, just like a GetMapping or PostMapping does.

Any other methods you chain to antMatchers() applies to requests that match the pattern. Therefore, in the example above, hasRole("USER") applies to the antMatcher("/secure/**").

You can add additional constraints by adding more antMatcher() methods. For example, we also want to permit access to all other parts of our application to everyone, even if they're not authenticated:

.antMatchers("/", "/js/**", "/css/**", "/images/**").permitAll()

This permits all users (even visitors that aren't logged in) access to the root (and therefore the index page), the /js directory and its contents, the /css directory and its contents, and the /images directory and its contents.

The permitAll() method is a convenient way to say "grant access to everyone".

In case we missed any part of our application, or in case we add more directories and files at a later date, we'll ensure that anything not mentioned above can only be accessed by authenticated users:

.anyRequest().authenticated()

The anyRequest() method is a more generic matcher than an ant matcher and it allows us to use the .authenticated() method.

You might not always want to use anyRequest().authenticated(). If you wanted the opposite (all other resources freely available to all, then you would leave this out and add "/**" to the second ant matcher that defines which resources are accessible to all visitors, even unauthenticated ones.

The next thing we want to do is to tell Spring that we want to use our own custom login page, where our custom login page is, and that everyone is allowed to access it:

.and().formLogin().loginPage("/login").defaultSuccessUrl("/secure", true).permitAll()

We can attach this to the existing authorization rules using the and() method just like we did in the previous lesson.

The .formLogin() method is invoked on the HttpSecurity object and it indicates that we want to use form-based authentication. If you don't use the loginPage() method, it will use the default login page.

We do have our own custom login page, so we chain the .loginPage() onto the .formLogin() method to specify where our login page is located and what the file name or mapping is. This is how Spring knows where to go or what handler to execute when a login is required (in our case, the handler mapped to /login).

The defaultSuccessUrl("/secure", true) states that when a user successfully logs in, go to the /secure index page. If you don't include this, your user will automatically go back to the page they requested *before* they requested whatever got them to the login page (so an authorized user will end up back on the main index page, for example). This isn't required, but when I was playing with this example I found it annoying, so I added it in!

To configure how we want the /logout to work, we attach another rule using the logout() method. This tells Spring that we want it to automatically log out a user when we map to the /logout URL pattern. This method returns a LogoutConfigurer, which contains methods we can use to configure what we want to happen when a user logs out.

.and().logout()....

Recall that we want to clear the authentication data for this user and invalide their current session:

.and().logout().invalidateHttpSession(true).clearAuthentication(true)

We also want to define the ant matcher pattern that triggers the logout process. We can do this by adding on the method .logoutRequestMatcher(new AntPathRequestMatcher("/logout")).

And lastly, we want to specify what URL should be requested when a successful logout occurs: .logoutSuccessUrl("/login?logout").permitAll(). Remember that the ?logout parameter added to the /login URL will load our login.html form and show that extra DIV with the "You are logged out" message. We also want to make sure that this specific pattern is allowed to be accessed by everyone.

So now we've added another rule:

.and().logout()
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout").permitAll()

The last thing we want to do is indicate that we want to log any access to our /secure resources that fail because of an access denied exception and redirect the user to the "permission denied" page. This was all done in our LogAccessDeniedHandler, which we've already autowired:

.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)

The exceptionHandling() method returns an instance of ExceptionHandlingConfigurer, which allows you to configure how you want things to work when exceptions occur

The accessDeniedHandler() method in the ExceptionHandlingConfigurer allows you to specify what object should handle any Access Denied exceptions. In our case, we're passing it a reference to our LogAccessDeniedHandler object that we autowired in this SecurityConfig class.

If you simply wanted the user to go to a specific page only, you could use the accessDeniedPage() method, instead.

That's the whole configure(HttpSecurity) method! Here it in in its entirety in case you got stuck:

the code for the configure(HttpSecurity) method in the security config class
One of the configure() methods in the SecurityConfig class

All that's left is to add another method that configures our in-memory security realm. We can override the configure() method that accepts an AuthenticationManagerBuilder. Set up in-memory authentication to use no password encoder, and add 2 users: one in the USER role and one in the GUEST role. Use email addresses as the user names. You've done this before, so this shouldn't be too hard.

the code for the configure(AuthenticationBuilderManager) method in the security config class
The configure() method that sets up the users

Your SecurityConfig class is done! Now add your SecurityWebApplicationInitializer class. See the previous lesson if you need help with creating it.

You should now be able to run your application!

Running and Testing The Application

Those are the basics. Feel free to explore the different classes and see what else you can do. For example, you can create a LoginSuccessHandler (much like your access denied handler) to have authorized users redirect to one location and unauthorized users to a different location. You could also find a way to redirect a user who fails to authenticate to a page other than the form login page.

Exercises

Add an /images directory to your demo project's static resources and put an image from your camera roll inside it (or a public domain picture you downloaded from the internet, if you prefer, but don't choose the same picture as someone else).

Add the image on your project's main index page. Don't forget to include fallback code.

Add a directory /exercises to your demonstration project and put an index.html file inside it. Add a header to your page that contains the content "Exercise Page". Use whatever structure you like - a HEADER containing/or a series of level-x heading elements, whatever. Style them to your liking as long as they're easy to read (no dark-on-dark or light-on-light!)

Then add a block element that contains your first and last name as the content.

Add the your same picture to this index page and make sure you also include the fallback code.

Add a link to the exercises index page to both the main index page and the secure index page.

Configure the security settings so that only members of the USER role can view the pages inside the /exercises directory.

Restart the application and make sure it works: you should see the image on the main index page even if you're not logged in. You should only be able to get to the exercises index page when you're logged in as the user with the USER role.

Make sure you've included external CSS in your application!