© Marten Deinum, Daniel Rubio, and Josh Long 2017

Marten Deinum, Daniel Rubio and Josh Long, Spring 5 Recipes, https://doi.org/10.1007/978-1-4842-2790-9_7

7. Spring Security

Marten Deinum, Daniel Rubio2 and Josh Long3

(1)Meppel, Drenthe, The Netherlands

(2)F. Bahia, Ensenada, Baja California, Mexico

(3)Apartment 205, Canyon Country, California, USA

In this chapter, you will learn how to secure applications using the Spring Security framework, a subproject of the Spring Framework. Spring Security was initially known as Acegi Security, but its name was changed after joining with the Spring portfolio projects. Spring Security can be used to secure any Java application, but it’s mostly used for web-based applications. Web applications, especially those that can be accessed through the Internet, are vulnerable to hacker attacks if they are not secured properly.

If you’ve never handled security in an application, there are several terms and concepts that you must understand first. Authentication is the process of verifying a principal’s identity against what it claims to be. A principal can be a user, a device, or a system, but most typically, it’s a user. A principal has to provide evidence of identity to be authenticated. This evidence is called a credential, which is usually a password when the target principal is a user.

Authorization is the process of granting authority to an authenticated user so that this user is allowed to access particular resources of the target application. The authorization process must be performed after the authentication process. Typically, authorities are granted in terms of roles.

Access control means controlling access to an application’s resources. It entails making a decision on whether a user is allowed to access a resource. This decision is called an access control decision, and it’s made by comparing the resource’s access attributes with the user’s granted authorities or other characteristics.

After finishing this chapter, you will understand basic security concepts and know how to secure your web applications at the URL access level, the method invocation level, the view-rendering level, and the domain object level.

Note

Before starting this chapter, take a look at the application for recipe_7_1_i. This is the initial unsecured application you will use in this chapter. It is a basic to-do app in which you can list, create, and mark to-dos completed. When you deploy the application, you will be greeted with the content, as shown in Figure 7-1.

A314861_4_En_7_Fig1_HTML.jpg
Figure 7-1. Initial to-do application

7-1. Secure URL Access

Problem

Many web applications have some particular URLs that are critically important and private. You must secure these URLs by preventing unauthorized access to them.

Solution

Spring Security enables you to secure a web application’s URL access in a declarative way through simple configuration. It handles security by applying servlet filters to HTTP requests. To register a filter and detect the configuration, Spring Security provides a convenience base class to extend: AbstractSecurityWebApplicationInitializer.

Spring Security allows you to configure web application security through the various configure methods on the WebSecurityConfigurerAdapter configuration adapter. If your web application’s security requirements are straightforward and typical, you can leave the configuration as is and use the default enabled security settings, including the following:

  • Form-based login service: This provides a default page that contains a login form for users to log into this application.

  • HTTP Basic authentication: This can process the HTTP Basic authentication credentials presented in HTTP request headers. It can also be used for authenticating requests made with remoting protocols and web services.

  • Logout service: This provides a handler mapped with a URL for users to log out of this application.

  • Anonymous login: This assigns a principal and grants authorities to an anonymous user so that you can handle an anonymous user like a normal user.

  • Servlet API integration: This allows you to access security information in your web application via standard Servlet APIs, such as HttpServletRequest.isUserInRole() and HttpServletRequest.getUserPrincipal().

  • CSFR: This implements cross-site forgery request protection by creating a token and putting it in the HttpSession.

  • Security headers: Like disabling caching for secured packages, this offers XSS protection, transport security, and X-Frame security.

With these security services registered, you can specify the URL patterns that require particular authorities to access. Spring Security will perform security checks according to your configurations. A user must log into an application before accessing the secure URLs, unless these URLs are opened for anonymous access. Spring Security provides a set of authentication providers for you to choose from. An authentication provider authenticates a user and returns the authorities granted to this user.

How It Works

First you need to register the filters used by Spring Security. The easiest way to do this is by extending the aforementioned AbstractSecurityWebApplicationInitializer.

package com.apress.springrecipes.board.security;

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

public class TodoSecurityInitializer extends AbstractSecurityWebApplicationInitializer {

    public TodoSecurityInitializer() {
        super(TodoSecurityConfig.class);
    }
}

The AbstractSecurityWebApplicationInitializer class has a constructor that takes one or more configuration classes. These configuration classes are used to bootstrap the security.

Note

If you have a class that extends AbstractAnnotationConfigDispatcherServletInitializer, add the security configuration to that or you will get an exception during startup.

Although you can configure Spring Security in the same configuration class as the web and service layers, it’s better to separate the security configurations in an isolated class (e.g., TodoSecurityConfig). Inside WebApplicationInitializer (e.g., TodoWebInitializer), you need to add that configuration class to the list of classes for the configuration.

First you need the security configuration . For this, you will create the TodoSecurityConfig class, as shown here:

package com.apress.springrecipes.board.security;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;


@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {}

When building and deploying the application and trying to access http://localhost:8080/todos/todos, you will now be greeted by the default Spring Security login page (see Figure 7-2).

A314861_4_En_7_Fig2_HTML.jpg
Figure 7-2. Default Spring Security login page

Secure URL Access

If you look at the configure method of the org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter class, you will see that it includes the anyRequest().authenticated() call. This tells Spring Security that for every request that comes in, you have to be authenticated with the system. You will also see that by default HTTP Basic authentication and form-based login are enabled. Form-based login also includes a default login page creator that will be used if you don’t explicitly specify a login page.

protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .anyRequest().authenticated()
            .and()
        .formLogin().and()
        .httpBasic();
}

Let’s write a couple of security rules. Instead of only needing to be logged in, you can write some powerful access rules for the URLs.

package com.apress.springrecipes.board.security;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("[email protected]").password("user").authorities("USER")
            .and()
            .withUser("[email protected]").password("admin").authorities("USER", "ADMIN");
    }


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


        http.authorizeRequests()
            .antMatchers("/todos*").hasAuthority("USER")
            .antMatchers(HttpMethod.DELETE, "/todos*").hasAuthority("ADMIN")
            .and()
                .formLogin()
            .and()
                .csrf().disable();
    }
}

You can configure authorization rules and more by overriding the configure(HttpSecurity http) method (there are other configure methods as well).

With authorizeRequests(), you start securing your URLs. You can then use one of the matchers; in the previous code, you use antMatchers to define the matching rules and which authorities a user needs to have. Remember that you must always include a wildcard at the end of a URL pattern. Failing to do so will make the URL pattern unable to match a URL that has request parameters. As a result, hackers could easily skip the security check by appending an arbitrary request parameter. You have secured all access to /todos to users who have the authority USER. To be able to call /todos with a DELETE request, you need to be a user with the role ADMIN.

Note

Because you are now overriding the default access rules and login configuration, you need to enable formLogin again. There is also a call disabling CSFR protection for now, because CSFR protection would make the forms not work; this recipe will explain later how to enable it.

You can configure authentication services in the overridden configure(AuthenticationManagerBuilder auth) method. Spring Security supports several ways of authenticating users, including authenticating against a database or an LDAP repository. It also supports defining user details directly for simple security requirements. You can specify a username, a password, and a set of authorities for each user.

Now, you can redeploy this application to test its security configurations. You must log into this application with the correct username and password to see the to-dos. Finally, to delete a to-do, you must log in as an administrator.

Work with CSFR Protection

It is generally a good idea to leave the default for CSFR enabled because this will reduce the risk you have with a CSFR attack. It is enabled by default in Spring Security, and the line csfr().disable() can be removed from the configuration. When CSFR protection is enabled, Spring Security adds CsfrFilter to the list of filters it uses for protection. This filter in turn uses an implementation of CsrfTokenRepository to generate and store tokens; by default this is the HttpSessionCsrfTokenRepository class that, as the name implies, stores the generated token in the HttpSession interface. There is also a CookieCsrfTokenRepository class that stores the token information in a cookie. If you want to switch the CsfrTokenRepository class, you can use the csrfTokenRepository() configuration method to change it. You could also use this to configure an explicitly configured HttpSessionCsrfTokenRepository.

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


    HttpSessionCsrfTokenRepository repo = new HttpSessionCsrfTokenRepository();
    repo.setSessionAttributeName("csfr_token");
    repo.setParameterName("csfr_token")


    http.csrf().csrfTokenRepository(repo);
}

When CSFR is enabled, trying to complete or delete a to-do item after you log in will fail because of the absence of a CSFR token. To fix this, you need to pass the CSFR token back to the server on requests that modify content. You can easily do this with a hidden input in your form. The HttpSessionCsrfTokenRepository exposes the token in the session under the attribute _csfr (by default, unless you configured it explicitly). For a form you can use the parameterName and token properties to create the appropriate input tag.

Add the following to the two forms that complete and delete a to-do item:

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>              

Now when submitting the form, the token will be part of the request, and you will again be able to complete or delete a to-do item.

There is also a form in the todo-create.jsp page; however, because this is using the Spring MVC form tags, you don’t need to modify this. When using the Spring MVC form tags, the CSFR token is added to the form automatically. To make this possible, Spring Security registers a CsrfRequestDataValueProcessor class, which takes care of adding the token to the form.

7-2. Log In to Web Applications

Problem

A secure application requires its users to log in before they can access certain secure functions. This is especially important for web applications running on the open Internet because hackers can easily reach them. Most web applications have to provide a way for users to input their credentials to log in.

Solution

Spring Security supports multiple ways for users to log into a web application . It supports form-based login by providing a default web page that contains a login form. You can also provide a custom web page as the login page. In addition, Spring Security supports HTTP Basic authentication by processing the Basic authentication credentials presented in HTTP request headers. HTTP Basic authentication can also be used for authenticating requests made with remoting protocols and web services.

Some parts of your application may allow for anonymous access (e.g., access to the welcome page). Spring Security provides an anonymous login service that can assign a principal and grant authorities to an anonymous user so that you can handle an anonymous user like a normal user when defining security policies.

Spring Security also supports “remember-me” login , which is able to remember a user’s identity across multiple browser sessions so that a user doesn’t need to log in again after logging in for the first time.

How It Works

To help you better understand the various login mechanisms in isolation, let’s first disable the default security configuration .

Caution

You generally want to stick with the defaults and just disable what you don’t want, such as httpBasic().disable(), instead of disabling all the security defaults!

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


    public TodoSecurityConfig() {
        super(true);
    }
}

Note that the login services introduced next will be registered automatically if you enable HTTP autoconfig. However, if you disable the default configuration or you want to customize these services, you have to configure the corresponding features explicitly.

Before enabling the authentication features, you will have to enable the basic Spring Security requirements you need to configure at least exception handling and security context integration.

protected void configure(HttpSecurity http) {

    http.securityContext()
        .and()
        .exceptionHandling();
}

Without these basics, Spring Security wouldn’t store the user after logging in, and it wouldn’t do proper exception translation for security-related exceptions (they would simply bubble up, which might expose some of your internals to the outside world). You also might want to enable the Servlet API integration so that you can use the methods on HttpServletRequest to do checks in your view.

protected void configure(HttpSecurity http) {
    http.servletApi();
}

Use HTTP Basic Authentication

The HTTP Basic authentication support can be configured via the httpBasic() method. When HTTP Basic authentication is required, a browser will typically display a login dialog or a specific login page for users to log in.

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            ...
            .httpBasic();
    }
}
Note

When HTTP Basic authentication and form-based login are enabled at the same time, the latter will be used. So, if you want your web application users to log in with this authentication type, you should not enable form-based login.

Use Form-Based Login

The form-based login service will render a web page that contains a login form for users to input their login details and process the login form submission. It’s configured via the formLogin method.

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            ...
            .formLogin();
    }
}

By default, Spring Security automatically creates a login page and maps it to the URL /login. So, you can add a link to your application (e.g., in todos.jsp) referring to this URL for login.

<a href="<c:url value="/login" />">Login</a>

If you don’t prefer the default login page , you can provide a custom login page of your own. For example, you can create the following login.jsp file in the root directory of the web application. Note that you shouldn’t put this file inside WEB-INF, which would prevent users from accessing it directly.

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<html>
<head>
    <title>Login</title>
    <link type="text/css" rel="stylesheet"
        href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.10/semantic.min.css">
    <style type="text/css">
        body {
            background-color: #DADADA;
        }
        body > .grid {
            height: 100%;
        }
        .column {
            max-width: 450px;
        }
    </style>
</head>


<body>
<div class="ui middle aligned center aligned grid">
    <div class="column">
        <h2 class="ui header">Log-in to your account</h2>
        <form method="POST" action="<c:url value="/login" />" class="ui large form">
            <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
            <div class="ui stacked segment">
                <div class="field">
                    <div class="ui left icon input">
                        <i class="user icon"></i>
                        <input type="text" name="username" placeholder="E-mail address">
                    </div>
                </div>
                <div class="field">
                    <div class="ui left icon input">
                        <i class="lock icon"></i>
                        <input type="password" name="password" placeholder="Password">
                    </div>
                </div>
                <button class="ui fluid large submit green button">Login</button>
            </div>
        </form>
    </div>
</div>
</body>
</html>

For Spring Security to display your custom login page when a login is requested, you have to specify its URL in the loginPage configuration method.

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            ...
            .formLogin().loginPage("/login.jsp");
    }
}

If the login page is displayed by Spring Security when a user requests a secure URL, the user will be redirected to the target URL once the login succeeds. However, if the user requests the login page directly via its URL, by default the user will be redirected to the context path’s root (i.e., http://localhost:8080/todos/) after a successful login. If you have not defined a welcome page in your web deployment descriptor, you may want to redirect the user to a default target URL when the login succeeds.

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            ...
            .formLogin().loginPage("/login.jsp").defaultSuccessUrl("/todos");
    }
}

If you use the default login page created by Spring Security, then when a login fails, Spring Security will render the login page again with the error message. However, if you specify a custom login page, you will have to configure the authentication-failure-url value to specify which URL to redirect to on login error. For example, you can redirect to the custom login page again with the error request parameter, as shown here:

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            ...
            .formLogin()
                .loginPage("/login.jsp")
                .defaultSuccessUrl("/messageList")
                .failureUrl("login.jsp?error=true");
    }
}

Then your login page should test whether the error request parameter is present. If an error has occurred, you will have to display the error message by accessing the session scope attribute SPRING_SECURITY_LAST_EXCEPTION, which stores the last exception for the current user.

<form>                  
    ...
    <c:if test="${not empty param.error}">
        <div class="ui error message" style="display: block;">
            Authentication Failed<br/>
            Reason : ${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message}
            </font>
        </div>
    </c:if>
</form>

Use the Logout Service

The logout service provides a handler to handle logout requests. It can be configured via the logout() configuration method.

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            ...
            .and()
                .logout();
    }
}

By default, it’s mapped to the URL /logout and will react to POST requests only. You can add a small HTML form to your page to log out.

<form action="<c:url value="/logout"/>" method="post"><button>Logout</button><form>
Note

When using CSRF protection, don’t forget to add the CSRF token to the form (see recipe 7-1) or the logout will fail.

By default, a user will be redirected to the context path’s root when the logout succeeds, but sometimes you may want to direct the user to another URL, which you can do by using the logoutSuccessUrl configuration method.

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            ...
            .and()
                .logout().logoutSuccessUrl("/logout-success.jsp");
    }
}

After logout, you might notice that when using the browser’s back button you will still be able to see the previous pages, even if your logout was successful. This has to do with the fact that the browser caches the pages. By enabling the security headers, with the headers() configuration method , the browser will be instructed to not cache the page.

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            ...
            .and()
                .headers();
    }
}

Next to the no-cache headers, this will also disable content sniffing and enable X-Frame protection (see recipe 7-1 for more information). With this enabled and using the browser’s back button, you will be redirected to the login page.

Implement Anonymous Login

The anonymous login service can be configured via the anonymous() method in a Java config, where you can customize the username and authorities of an anonymous user, whose default values are anonymousUser and ROLE_ANONYMOUS.

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            ...
            .and()
                .anonymous().principal("guest").authorities("ROLE_GUEST");
    }
}

Implement Remember-Me Support

Remember-me support can be configured via the rememberMe() method in a Java config. By default, it encodes the username, the password, the remember-me expiration time, and a private key as a token and stores the token as a cookie in the user’s browser. The next time the user accesses the same web application, this token will be detected so that the user can log in automatically.

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            ...
            .and()
                .rememberMe();
    }
}

However, static remember-me tokens can cause security issues because they may be captured by hackers. Spring Security supports rolling tokens for more advanced security needs, but this requires a database to persist the tokens. For details about rolling remember-me token deployment, please refer to the Spring Security reference documentation.

7-3. Authenticate Users

Problem

When a user attempts to log into your application to access its secure resources, you have to authenticate the user’s principal and grant authorities to this user.

Solution

In Spring Security, authentication is performed by one or more AuthenticationProviders, connected as a chain. If any of these providers authenticates a user successfully, that user will be able to log into the application. If any provider reports that the user is disabled or locked or that the credential is incorrect or if no provider can authenticate the user, then the user will be unable to log into this application.

Spring Security supports multiple ways of authenticating users and includes built-in provider implementations for them. You can easily configure these providers with the built-in XML elements. Most common authentication providers authenticate users against a user repository storing user details (e.g., in an application’s memory, a relational database, or an LDAP repository).

When storing user details in a repository, you should avoid storing user passwords in clear text because that makes them vulnerable to hackers. Instead, you should always store encrypted passwords in your repository. A typical way of encrypting passwords is to use a one-way hash function to encode the passwords. When a user enters a password to log in, you apply the same hash function to this password and compare the result with the one stored in the repository. Spring Security supports several algorithms for encoding passwords (including MD5 and SHA) and provides built-in password encoders for these algorithms.

If you retrieve a user’s details from a user repository every time a user attempts to log in, your application may incur a performance impact. This is because a user repository is usually stored remotely, and it has to perform some kinds of queries in response to a request. For this reason, Spring Security supports caching user details in local memory and storage to save you the overhead of performing remote queries.

How It Works

Here you will explore different authentication mechanism, first you will look at the in-memory implemention, followed by the database driven one and finally you will take a look at LDAP. The final section will cover how to enable caching for the different authentication mechanisms.

Authenticate Users with In-Memory Definitions

If you have only a few users in your application and you seldom modify their details, you can consider defining the user details in Spring Security’s configuration file so that they will be loaded into your application’s memory.

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


...
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("[email protected]").password("secret").authorities("ADMIN","USER").and()
            .withUser("marten@@ya2do.io").password("user").authorities("USER").and()
            .withUser("[email protected]").password("unknown").disabled(true).authorities("USER");
    }
}

You can define user details with the inMemoryAuthentication() method. Using the withUser method, you can define the users . For each user, you can specify a username, a password, a disabled status, and a set of granted authorities. A disabled user cannot log into an application.

Authenticate Users Against a Database

More typically, user details should be stored in a database for easy maintenance. Spring Security has built-in support for querying the user details from a database. By default, it queries user details, including authorities, with the following SQL statements:

SELECT username, password, enabled
FROM   users
WHERE  username = ?


SELECT username, authority
FROM   authorities
WHERE  username = ?

For Spring Security to query user details with these SQL statements , you have to create the corresponding tables in your database. For example, you can create them in the todo schema with the following SQL statements:

CREATE TABLE USERS (
    USERNAME    VARCHAR(50)    NOT NULL,
    PASSWORD    VARCHAR(50)    NOT NULL,
    ENABLED     SMALLINT       NOT NULL,
    PRIMARY KEY (USERNAME)
);


CREATE TABLE AUTHORITIES (
    USERNAME    VARCHAR(50)    NOT NULL,
    AUTHORITY   VARCHAR(50)    NOT NULL,
    FOREIGN KEY (USERNAME) REFERENCES USERS
);

Next, you can input some user details into these tables for testing purposes. Tables 7-1 and 7-2 show the data for these two tables.

Table 7-1. Testing User Data for the USERS Table

USERNAME

PASSWORD

ENABLED

[email protected]

secret

1

[email protected]

user

1

[email protected]

unknown

0

Table 7-2. Testing User Data for the AUTHORITIES Table

USERNAME

AUTHORITY

[email protected]

ADMIN

[email protected]

USER

[email protected]

USER

[email protected]

USER

For Spring Security to access these tables, you have to declare a data source to be able to create connections to this database.

For a Java config, use the jdbcAuthentication() configuration method and pass it a DataSource.

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .setName("board")
            .addScript("classpath:/schema.sql")
            .addScript("classpath:/data.sql")
            .build();
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication().dataSource(dataSource());
    }
}
Note

The @Bean method used for the DataSource has been moved from the TodoWebConfig to the TodoSecurityConfig.

However, in some cases, you may already have your own user repository defined in a legacy database. For example, suppose that the tables are created with the following SQL statements and that all users in the MEMBER table have the enabled status:

CREATE TABLE MEMBER (
    ID          BIGINT         NOT NULL,
    USERNAME    VARCHAR(50)    NOT NULL,
    PASSWORD    VARCHAR(32)    NOT NULL,
    PRIMARY KEY (ID)
);


CREATE TABLE MEMBER_ROLE (
    MEMBER_ID    BIGINT         NOT NULL,
    ROLE         VARCHAR(10)    NOT NULL,
    FOREIGN KEY  (MEMBER_ID)    REFERENCES MEMBER
);

Suppose you have legacy user data stored in these tables, as shown in Tables 7-3 and 7-4.

Table 7-3. Legacy User Data in the MEMBER Table

ID

USERNAME

PASSWORD

1

[email protected]

secret

2

[email protected]

user

Table 7-4. Legacy User Data in the MEMBER_ROLE Table

MEMBER_ID

ROLE

1

ROLE_ADMIN

1

ROLE_USER

2

ROLE_USER

Fortunately, Spring Security supports using custom SQL statements to query a legacy database for user details. You can specify the statements for querying a user’s information and authorities using the usersByUsernameQuery() and authoritiesByUsernameQuery() configuration methods.

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


...

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
            .dataSource(dataSource)
            .usersByUsernameQuery(
                "SELECT username, password, 'true' as enabled FROM member WHERE username = ?")
            .authoritiesByUsernameQuery(
                "SELECT member.username, member_role.role as authorities " +
                "FROM member, member_role " +
                "WHERE  member.username = ? AND member.id = member_role.member_id");
    }
}

Encrypt Passwords

Until now, you have been storing user details with clear-text passwords. But this approach is vulnerable to hacker attacks, so you should encrypt the passwords before storing them. Spring Security supports several algorithms for encrypting passwords. For example, you can choose BCrypt, a one-way hash algorithm, to encrypt your passwords.

Note

You may need a helper to calculate BCrypt hashes for your passwords. You can do this online through, for example, https://www.dailycred.com/article/bcrypt-calculator , or you can simply create a class with a main method that uses Spring Security’s BCryptPasswordEncoder.

Now, you can store the encrypted passwords in your user repository. For example, if you are using in-memory user definitions, you can specify the encrypted passwords in the password attributes. Then, you can specify the password encoder using the passwordEncoder() method on AuthenticationManagerBuilder.

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


...

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .jdbcAuthentication()
                .passwordEncoder(passwordEncoder())
                .dataSource(dataSource());
    }
}

Of course, you have to store the encrypted passwords in the database tables, instead of the clear-text passwords, as shown in Table 7-5. To store BCrypt hashes in the password field, the length of the field has to be at least 60 characters long (which is the length of the BCrypt hash).

Table 7-5. Testing User Data with Encrypted Passwords for the USERS Table

USERNAME

PASSWORD

ENABLED

[email protected]

$2a$10$E3mPTZb50e7sSW15fDx8Ne7hDZpfDjrmMPTTUp8wVjLTu.G5oPYCO

1

marten@ya2do. io

$2a$10$5VWqjwoMYnFRTTmbWCRZT.iY3WW8ny27kQuUL9yPK1/WJcPcBLFWO

1

[email protected]

$2a$10$cFKh0.XCUOA9L.in5smIiO2QIOT8.6ufQSwIIC.AVz26WctxhSWC6

0

Authenticate Users Against an LDAP Repository

Spring Security also supports accessing an LDAP repository for authenticating users. First, you have to prepare some user data for populating the LDAP repository. Let’s prepare the user data in the LDAP Data Interchange Format (LDIF), a standard plain-text data format for importing and exporting LDAP directory data. For example, create the users.ldif file containing the following contents:

dn: dc=springrecipes,dc=com
objectClass: top
objectClass: domain
dc: springrecipes


dn: ou=groups,dc=springrecipes,dc=com
objectclass: top
objectclass: organizationalUnit
ou: groups


dn: ou=people,dc=springrecipes,dc=com
objectclass: top
objectclass: organizationalUnit
ou: people


dn: uid=admin,ou=people,dc=springrecipes,dc=com
objectclass: top
objectclass: uidObject
objectclass: person
uid: admin
cn: admin
sn: admin
userPassword: secret


dn: uid=user1,ou=people,dc=springrecipes,dc=com
objectclass: top
objectclass: uidObject
objectclass: person
uid: user1
cn: user1
sn: user1
userPassword: 1111


dn: cn=admin,ou=groups,dc=springrecipes,dc=com
objectclass: top
objectclass: groupOfNames
cn: admin
member: uid=admin,ou=people,dc=springrecipes,dc=com


dn: cn=user,ou=groups,dc=springrecipes,dc=com
objectclass: top
objectclass: groupOfNames
cn: user
member: uid=admin,ou=people,dc=springrecipes,dc=com
member: uid=user1,ou=people,dc=springrecipes,dc=com

Don’t worry if you don’t understand this LDIF file very well. You probably won’t need to use this file format to define LDAP data often because most LDAP servers support GUI-based configuration. This users.ldif file includes the following contents:

  • The default LDAP domain, dc=springrecipes,dc=com

  • The groups and people organization units for storing groups and users

  • The admin and user1 users with the passwords secret and 1111

  • The admin group (including the admin user) and the user group (including the admin and user1 users)

For testing purposes, you can install an LDAP server on your local machine to host this user repository. For the sake of easy installation and configuration, we recommend installing OpenDS ( www.opends.org/ ), a Java-based open source directory service engine that supports LDAP.

Tip

In the bin directory, there is an ldap.sh script that will start a Dockerized version of OpenDS and that will import the earlier mentioned users.ldif. Note that the root user and password for this LDAP server are cn=Directory Manager and ldap, respectively. Later, you will have to use this user to connect to this server.

After the LDAP server has started up, you can configure Spring Security to authenticate users against its repository.

You have to configure the LDAP repository using the ldapAuthentication() configuration method. You can specify the search filters and search bases for searching users and groups via several callback methods, whose values must be consistent with the repository’s directory structure. With the preceding attribute values, Spring Security will search a user from the people organization unit with a particular user ID and search a user’s groups from the groups organization unit. Spring Security will automatically insert the ROLE_ prefix to each group as an authority.

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {
...
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth
             .ldapAuthentication()
                 .contextSource()
                 .url("ldap://localhost:1389/dc=springrecipes,dc=com")
                 .managerDn("cn=Directory Manager").managerPassword("ldap")
             .and()
                 .userSearchFilter("uid={0}").userSearchBase("ou=people")
                 .groupSearchFilter("member={0}").groupSearchBase("ou=groups")


              .passwordEncoder(new LdapShaPasswordEncoder())
              .passwordCompare().passwordAttribute("userPassword");
      }
}

As OpenDS uses Salted Secure Hash Algorithm (SSHA) to encode user passwords by default, you have to specify the LdapShaPasswordEncoder as the password encoder. Note that this value is different from sha because it’s specific to LDAP password encoding. You also need to specify the passwordAttribute value because the password encoder needs to know which field in LDAP is the password.

Finally, you have to refer to an LDAP server definition, which defines how to create connections to an LDAP server. You can specify the root user’s username and password to connect to the LDAP server running on localhost by configuring the server using the contextSource method.

Cache User Details

Both <jdbc-user-service> and <ldap-user-service> support caching user details, but first you have to choose a cache implementation that provides a caching service. As Spring and Spring Security have built-in support for Ehcache ( http://ehcache.sourceforge.net/ ), you can choose it as your cache implementation and create a configuration file for it (e.g., ehcache.xml in the classpath root) with the following contents:

<ehcache>                    
    <diskStore path="java.io.tmpdir"/>


    <defaultCache
        maxElementsInMemory="1000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="true"
        />


    <cache name="userCache"
        maxElementsInMemory="100"
        eternal="false"
        timeToIdleSeconds="600"
        timeToLiveSeconds="3600"
        overflowToDisk="true"
        />
</ehcache>

This Ehcache configuration file defines two types of cache configurations. One is for the default, and the other is for caching user details. If the user cache configuration is used, a cache instance will cache the details of at most 100 users in memory. The cached users will overflow to disk when this limit is exceeded. A cached user will expire if it has been idle for 10 minutes or live for 1 hour after its creation.

Spring Security comes with two UserCache implementations: EhCacheBasedUserCache, which has to refer to an Ehcache instance, and SpringCacheBasedUserCache, which uses Spring’s caching abstraction.

In a Java-based configuration, at the moment of this writing, only the jdbcAuthentication() method allows for easy configuration of a user cache. For a Spring caching-based cache solution (which still delegates to Ehcache), you need to configure a CacheManager instance and make this aware of Ehcache.

@Configuration
public class MessageBoardConfiguration {
...
    @Bean
    public EhCacheCacheManager cacheManager() {
        EhCacheCacheManager cacheManager = new EhCacheCacheManager();
        cacheManager.setCacheManager(ehCacheManager().getObject());
        return cacheManager;
    }


    @Bean
    public EhCacheManagerFactoryBean ehCacheManager() {
        return new EhCacheManagerFactoryBean();
    }
}

This is best added to the configuration of the services because the caching can also be used for other means (see the recipes regarding Spring caching). Now that you have the CacheManager instance set up, you need to configure a SpringCacheBasedUserCache class.

@Configuration
@EnableWebMvcSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    private CacheManager cacheManager;


    @Bean
    public SpringCacheBasedUserCache userCache() throws Exception {
        Cache cache = cacheManager.getCache("userCache");
        return new SpringCacheBasedUserCache(cache);
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
            .userCache(userCache())
    ...
    }
}

Notice the autowiring of CacheManager into the configuration class. You need access to it because you need to retrieve a Cache instance that you pass into the constructor of SpringCacheBasedUseCache. You are going to use the cache named userCache (which you configured in the ehcache.xml file). Finally, you pass the configured UserCache into the jdbcAuthentications.userCache() method.

7-4. Make Access Control Decisions

Problem

In the authentication process , an application will grant a successfully authenticated user a set of authorities. When this user attempts to access a resource in the application, the application has to decide whether the resource is accessible with the granted authorities or other characteristics.

Solution

The decision of whether a user is allowed to access a resource in an application is called an access control decision. It is made based on the user’s authentication status and the resource’s nature and access attributes. In Spring Security, access control decisions are made by access decision managers, which have to implement the AccessDecisionManager interface. You are free to create your own access decision managers by implementing this interface, but Spring Security comes with three convenient access decision managers based on the voting approach. They are shown in Table 7-6.

Table 7-6. Access Decision Managers That Come with Spring Security

Access Decision Manager

Specifies when to grant access.

AffirmativeBased

At least one voter votes to grant access.

ConsensusBased

A consensus of voters votes to grant access.

UnanimousBased

All voters vote to abstain or grant access (no voter votes to deny access).

All these access decision managers require a group of voters to be configured for voting on access control decisions. Each voter has to implement the AccessDecisionVoter interface. A voter can vote to grant, abstain, or deny access to a resource. The voting results are represented by the ACCESS_GRANTED, ACCESS_DENIED, and ACCESS_ABSTAIN constant fields defined in the AccessDecisionVoter interface.

By default, if no access decision manager is specified explicitly, Spring Security will automatically configure an AffirmativeBased access decision manager with the following two voters configured:

  • RoleVoter votes for an access control decision based on a user’s role. It will only process access attributes that start with the ROLE_ prefix, but this prefix can be customized. It votes to grant access if the user has the same role as required to access the resource or to deny access if the user lacks any role required to access the resource. If the resource does not have an access attribute starting with ROLE_, it will abstain from voting.

  • AuthenticatedVoter votes for an access control decision based on a user’s authentication level. It will only process the access attributes IS_AUTHENTICATED_FULLY, IS_AUTHENTICATED_REMEMBERED, and IS_AUTHENTICATED_ANONYMOUSLY. It votes to grant access if the user’s authentication level is higher than the required attribute. From highest to lowest, authentication levels are fully authenticated, authentication remembered, and anonymously authenticated.

How It Works

By default, Spring Security will automatically configure an access decision manager if none is specified. This default access decision manager is equivalent to the one defined with the following configuration:

@Bean
public AffirmativeBased accessDecisionManager() {
    List<AccessDecisionVoter> decisionVoters = Arrays.asList(new RoleVoter(), new AuthenticatedVoter());
    return new AffirmativeBased(decisionVoters);
}


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


    http.authorizeRequests()
        .accessDecisionManager(accessDecisionManager())
    ...
}

This default access decision manager and its decision voters should satisfy most typical authorization requirements. However, if they don’t satisfy yours, you can create your own. In most cases, you’ll only need to create a custom voter. For example, you can create a voter to vote for a decision based on a user’s IP address.

package com.apress.springrecipes.board.security;

import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.WebAuthenticationDetails;


import java.util.Collection;
import java.util.Objects;



public class IpAddressVoter implements AccessDecisionVoter<Object> {


    private static final String IP_PREFIX = "IP_";
    private static final String IP_LOCAL_HOST = "IP_LOCAL_HOST";


    public boolean supports(ConfigAttribute attribute) {
        return (attribute.getAttribute() != null) && attribute.getAttribute().startsWith(IP_PREFIX);
    }


    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }


    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> configList) {
        if (!(authentication.getDetails() instanceof WebAuthenticationDetails)) {
            return ACCESS_DENIED;
        }


        WebAuthenticationDetails details = (WebAuthenticationDetails) authentication.getDetails();
        String address = details.getRemoteAddress();


        int result = ACCESS_ABSTAIN;

        for (ConfigAttribute config : configList) {
            result = ACCESS_DENIED;


            if (Objects.equals(IP_LOCAL_HOST, config.getAttribute())) {
                if (address.equals("127.0.0.1") || address.equals("0:0:0:0:0:0:0:1")) {
                    return ACCESS_GRANTED;
                }
            }
        }


        return result;
    }
}

Note that this voter will only process the access attributes that start with the IP_ prefix. At the moment, it only supports the IP_LOCAL_HOST access attribute. If the user is a web client whose IP address is equal to 127.0.0.1 or 0:0:0:0:0:0:0:1—the last value being returned by networkless Linux workstations—this voter will vote to grant access. Otherwise, it will vote to deny access. If the resource does not have an access attribute starting with IP_, it will abstain from voting.

Next, you have to define a custom access decision manager that includes this voter.

@Bean
public AffirmativeBased accessDecisionManager() {
    List<AccessDecisionVoter> decisionVoters = Arrays.asList(new RoleVoter(), new AuthenticatedVoter(), new IpAddressVoter());
    return new AffirmativeBased(decisionVoters);
}

Now, suppose you would like to allow users of the machine running the web container (i.e., the server administrators) to delete to-dos without logging in. You have to refer to this access decision manager from the configuration and add the access attribute IP_LOCAL_HOST to the delete URL mapping.

http.authorizeRequests()
    .accessDecisionManager()
    .antMatchers(HttpMethod.DELETE, "/todos*").access("ADMIN,IP_LOCAL_HOST");

When calling the URL directly, the to-do will be removed. To access it through the web interface, you still need to be logged in.

Use an Expression to Make Access Control Decisions

Although AccessDecisionVoters allow for a certain degree of flexibility , sometimes you want more complex access control rules to be more flexible. With Spring Security, it is possible to use Springs Expression Language (SpEL) to create powerful access control rules. Spring Security supports a couple of expressions out of the box (see Table 7-7 for a list). By using constructs such as and, or, and not, you can create very powerful and flexible expressions. Spring Security will automatically configure an access decision manager with WebExpressionVoter. This access decision manager is equivalent to the one defined with the following bean configuration:

Table 7-7. Spring Security Built-in Expressions

Expression

Description

hasRole(role) or hasAuthority(authority)

Returns true if the current user has the given role

hasAnyRole(role1,role2) / hasAnyAuthority(auth1,auth2)

Returns true if the current user has at least one of the given roles

hasIpAddress(ip-address)

Returns true if the current user has the given IP address

principal

The current user

Authentication

Access to the Spring Security authentication object

permitAll

Always evaluates to true

denyAll

Always evaluates to false

isAnonymous()

Returns true if the current user is anonymous

isRememberMe()

Returns true if the current user logged in by means of the remember-me functionality

isAuthenticated()

Returns true if this is not an anonymous user

isFullyAuthenticated()

Returns true if the user is not an anonymous or remember-me user

@Bean
public AffirmativeBased accessDecisionManager() {
    List<AccessDecisionVoter> decisionVoters = Arrays.asList(new WebExpressionVoter());
    return new AffirmativeBased(decisionVoters);
}
Caution

Although the role and authority are almost the same, there is a slight, but important, difference in how they are processed. When using hasRole, the passed-in value for the role will be checked if it starts with ROLE_ (the default role prefix). If not, this will be added before checking the authority. So, hasRole('ADMIN') will actually check whether the current user has the authority ROLE_ADMIN. When using hasAuthority, it will check the value as is.

The previous expression would give access to delete a post if someone had the ADMIN role or was logged in on the local machine. In the previous section, you needed to create your own custom AccessDecisionVoter. Now you only have to write an expression. Writing expressions can be done through the access method instead of one of the has* methods when defining a matcher.

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/messageList*").hasAnyRole("USER", "GUEST")
                .antMatchers("/messagePost*").hasRole("USER")
                .antMatchers("/messageDelete*")
             .access("hasRole('ROLE_ADMIN') or hasIpAddress('127.0.0.1') or hasIpAddress('0:0:0:0:0:0:0:1')")
            ...
    }
...
}

Although Spring Security has already several built-in functions that can be used when creating expressions, it is possible to extend the functions with your own. For this, you need to create a class that implements the SecurityExpressionOperations interface and register it with Spring Security. Although it would be possible to create a class that implements all the methods on this interface, it is in general easier to extend the default when you want to add expressions .

package com.apress.springrecipes.board.security;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.WebSecurityExpressionRoot;


public class ExtendedWebSecurityExpressionRoot extends WebSecurityExpressionRoot {

    public ExtendedWebSecurityExpressionRoot(Authentication a, FilterInvocation fi) {
        super(a, fi);
    }


    public boolean localAccess() {
        return hasIpAddress("127.0.0.1") || hasIpAddress("0:0:0:0:0:0:0:1");


    }
}

Here you extended WebSecurityExpressionRoot, which provides the default implementation, and you added the method localAccess(). This method checks whether you are logging in from the local machine. To make this class available for Spring Security, you need to create the SecurityExpressionHandler interface.

package com.apress.springrecipes.board.security;

import org.springframework.security.access.expression.SecurityExpressionOperations;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.access.expression.WebSecurityExpressionRoot;


public class ExtendedWebSecurityExpressionHandler extends DefaultWebSecurityExpressionHandler {

    private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();

    @Override
    protected SecurityExpressionOperations
        createSecurityExpressionRoot(Authentication authentication, FilterInvocation fi) {


        ExtendedWebSecurityExpressionRoot root =
            new ExtendedWebSecurityExpressionRoot(authentication, fi);
        root.setPermissionEvaluator(getPermissionEvaluator());
        root.setTrustResolver(trustResolver);
        root.setRoleHierarchy(getRoleHierarchy());
        return root;
    }


    @Override
    public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
        this.trustResolver=trustResolver;
        super.setTrustResolver(trustResolver);
    }
}

You are extending DefaultWebSecurityExpressionHandler, which provides the default implementation. You override the createSecurityExpressionRoot method and let that create an instance of the ExtendedWebSecurityExpressionRoot class. As you need to add a couple of collaborators, you call the get methods of the superclass. As there isn’t a getTrustResolver method, you need to create a new instance of that yourself and implement the setter method.

@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .expressionHandler(new ExtendedWebSecurityExpressionHandler())
                .antMatchers("/todos*").hasAuthority("USER")
                .antMatchers(DELETE, "/todos*").access("hasRole('ROLE_ADMIN) or localAccess()")
    }
}

You set the custom expression handler with the expressionHandler method. Now you can rewrite your expression using your localAccess() expression.

Use an Expression to Make Access Control Decisions Using Spring Beans

Although you can extend Spring Security using these methods, it isn’t the recommended approach. Instead, it is advises you to write a custom class and use that in the expression. Using the @ syntax in the expression, you can call any bean in the application context. So, you could write an expression like @accessChecker.hasLocalAccess(authentication) and provide a bean named accessChecker, which has a hasLocalAccess method that takes an Authentication object.

package com.apress.springrecipes.board.security;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.WebAuthenticationDetails;


public class AccessChecker {

    public boolean hasLocalAccess(Authentication authentication) {
        boolean access = false;
        if (authentication.getDetails() instanceof WebAuthenticationDetails) {
            WebAuthenticationDetails details = (WebAuthenticationDetails) authentication.getDetails();
            String address = details.getRemoteAddress();
            access = address.equals("127.0.0.1") || address.equals("0:0:0:0:0:0:0:1");
        }
        return access;
    }
}

The AccessChecker still does the same checks as the earlier IpAddressVoter or custom expression handler but doesn’t extend the Spring Security classes.

@Bean
public AccessChecker accessChecker() {
    return new AccessChecker();
}


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


    http.authorizeRequests()
        .antMatchers("/todos*").hasAuthority("USER")
        .antMatchers(HttpMethod.DELETE, "/todos*").access("hasAuthority('ADMIN') or @accessChecker.hasLocalAccess(authentication)")
   ...
}

7-5. Secure Method Invocations

Problem

As an alternative or a complement to securing URL access in the web layer, sometimes you may need to secure method invocations in the service layer. For example, in the case that a single controller has to invoke multiple methods in the service layer, you may want to enforce fine-grained security controls on these methods.

Solution

Spring Security enables you to secure method invocations in a declarative way. You annotate methods declared in a bean interface or an implementation class with the @Secured, @PreAuthorize/@PostAuthorize, or @PreFilter/@PostFilter annotations and then enable security for them using the @EnableGlobalMethodSecurity annotation.

How It Works

First you will explore how to secure method invocations using annotations and how to write security expression. Finally you will also see how you can use annotations and expression to filter input arguments and output of a method.

Secure Methods with Annotations

The approach to securing methods is by annotating them with @Secured. For example, you can annotate the methods in MessageBoardServiceImpl with the @Secured annotation and specify the access attributes as its value, whose type is String[] and which takes one or more authorities that will have access to the method.

package com.apress.springrecipes.board.service;
...
import org.springframework.security.access.annotation.Secured;


public class MessageBoardServiceImpl implements MessageBoardService {
    ...
    @Secured({"ROLE_USER", "ROLE_GUEST"})
    public List<Message> listMessages() {
        ...
    }


    @Secured("ROLE_USER")
    public synchronized void postMessage(Message message) {
        ...
    }


    @Secured({"ROLE_ADMIN", "IP_LOCAL_HOST"})
    public synchronized void deleteMessage(Message message) {
        ...
    }


    @Secured({"ROLE_USER", "ROLE_GUEST"})
    public Message findMessageById(Long messageId) {
        return messages.get(messageId);
    }
}

Finally, you need to enable the method security. To do so, you have to add the @EnableGlobalMethodSecurity annotation to your configuration class. As you want to use @Secured, you have to set the securedEnabled attribute to true.

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class TodoWebConfiguration { ... }
Note

It is important that you add the @EnableGlobalMethodSecurity annotation to the application context configuration that contains the beans you want to secure!

Secure Methods with Annotations and Expressions

If you need more elaborate security rules, you can, just like with URL protection, use security expressions based on SpEL to secure your application. For this, you can use the @PreAuthorize and @PostAuthorize annotations. With them you can write security-based expressions just like with URL-based security. To enable the processing of those annotations, you have to set the prePostEnabled attribute on the @EnableGlobalMethodSecurity annotation to true.

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TodoWebConfiguration { ... }

Now you can use the @PreAuthorize and @PostAuthorize annotations to secure your application.

package com.apress.springrecipes.board;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;


import javax.transaction.Transactional;
import java.util.List;


@Service
@Transactional
class TodoServiceImpl implements TodoService {


    private final TodoRepository todoRepository;

    TodoServiceImpl(TodoRepository todoRepository) {
        this.todoRepository = todoRepository;
    }


    @Override
    @PreAuthorize("hasAuthority('USER')")
    public List<Todo> listTodos() {
        return todoRepository.findAll();
    }


    @Override
    @PreAuthorize("hasAuthority('USER')")
    public void save(Todo todo) {
        this.todoRepository.save(todo);
    }


    @Override
    @PreAuthorize("hasAuthority('USER')")
    public void complete(long id) {
        Todo todo = findById(id);
        todo.setCompleted(true);
        todoRepository.save(todo);
    }


    @Override
    @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')")
    public void remove(long id) {
        todoRepository.remove(id);
    }


    @Override
    @PreAuthorize("hasAuthority('USER')")
    @PostAuthorize("returnObject.owner == authentication.name")
    public Todo findById(long id) {
        return todoRepository.findOne(id);
    }
}

The @PreAuthorize annotation will be triggered before the actual method call, and the @PostAuthorize annotation will be triggered after the method call. You can also write a security expression and use the result of the method invocation using the returnObject expression. See the expression on the findById method; now if someone else as the owner tried to access the Todo object, a security exception would be thrown.

Filter with Annotations and Expressions

In addition to the @PreAuthorize and @PostAuthorize annotations, there are also the @PreFilter and @PostFilter annotations. The main difference between the two groups of annotations is that the @*Authorize ones will throw an exception if the security rules don’t apply. The @*Filter annotations will simply filter the input and output variables of elements you don’t have access to.

Currently, when calling listTodos, everything is returned from the database. You want to restrict the retrieval of all elements to the user with the authority ADMIN, and others can see only their own list of to-dos. This can be simply implemented with an @PostFilter annotation. Adding @PostFilter("hasAuthority('ADMIN') or filterObject.owner == authentication.name") will implement this rule.

@PreAuthorize("hasAuthority('USER')")
@PostFilter("hasAnyAuthority('ADMIN') or filterObject.owner == authentication.name")
public List<Todo> listTodos() {
    return todoRepository.findAll();
}

When you redeploy the application and log in as a user, you will now only see your own to-dos, and when using a user with the ADMIN authority, you will still see all the available to-dos. See also recipe 7-7 for a more elaborate use of the @*Filter annotations.

Caution

Although @PostFilter and @PreFilter are a simple way of filtering the input/output of a method, use them with caution. When using them with large results, you can severely impact the performance of your application.

7-6. Handle Security in Views

Problem

Sometimes you may want to display a user’s authentication information , such as the principal name and the granted authorities, in the views of your web application. In addition, you want to render the view contents conditionally according to the user’s authorities.

Solution

Although you can write JSP scriptlets in your JSP files to retrieve authentication and authorization information through the Spring Security API, it’s not an efficient solution. Spring Security provides a JSP tag library for you to handle security in JSP views. It includes tags that can display a user’s authentication information and render the view contents conditionally according to the user’s authorities.

How It Works

You will first learn how to use the Spring Security tags to display information of the currently authenticated user. Next you will learn how to conditionally hide parts of the page based on the authorities of the current authenticated user.

Display Authentication Information

Suppose you would like to display a user’s principal name and grant authorities in the header of the to-do’s listing page (i.e., todos.jsp). First, you have to import Spring Security’s tag library definition.

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

The <sec:authentication> tag exposes the current user’s Authentication object for you to render its properties. You can specify a property name or property path in its property attribute. For example, you can render a user’s principal name through the name property.

<h4>Todos for <sec:authentication property="name" /></h4>              

In addition to rendering an authentication property directly, this tag supports storing the property in a JSP variable, whose name is specified in the var attribute. For example, you can store the authorities property, which contains the authorities granted to the user, in the JSP variable authorities and render them one by one with a <c:forEach> tag. You can further specify the variable scope with the scope ascope attribute.

<sec:authentication property="authorities" var="authorities" />              
<ul>
    <c:forEach items="${authorities}" var="authority">
        <li>${authority.authority}</li>
    </c:forEach>
</ul>

Render View Contents Conditionally

If you want to render view contents conditionally according to a user’s authorities, you can use the <sec:authorize> tag. For example, you can decide whether to render the message authors according to the user’s authorities.

<td>                  
    <sec:authorize ifAllGranted="ROLE_ADMIN,ROLE_USER">${todo.owner}</sec:authorize>
</td>

If you want the enclosing content to be rendered only when the user has been granted certain authorities at the same time, you have to specify them in the ifAllGranted attribute. Otherwise, if the enclosing content can be rendered with any of the authorities, you have to specify them in the ifAnyGranted attribute.

<td>                  
    <sec:authorize ifAnyGranted="ROLE_ADMIN,ROLE_USER">${todo.owner}</sec:authorize>
</td>

You can also render the enclosing content when a user has not been granted any of the authorities specified in the ifNotGranted attribute.

<td>                  
    <sec:authorize ifNotGranted="ROLE_ADMIN,ROLE_USER">${todo.owner}</sec:authorize>
</td>

7-7. Handle Domain Object Security

Problem

Sometimes you may have complicated security requirements that require handling security at the domain object level . That means you have to allow each domain object to have different access attributes for different principals.

Solution

Spring Security provides a module named ACL that allows each domain object to have its own access control list (ACL). An ACL contains a domain object’s object identity to associate with the object and also holds multiple access control entries (ACEs) , each of which contains the following two core parts:

  • Permissions: An ACE’s permissions are represented by a particular bit mask, with each bit value for a particular type of permission. The BasePermission class predefines five basic permissions as constant values for you to use: READ (bit 0 or integer 1), WRITE (bit 1 or integer 2), CREATE (bit 2 or integer 4), DELETE (bit 3 or integer 8), and ADMINISTRATION (bit 4 or integer 16). You can also define your own using other unused bits.

  • Security identity (SID): Each ACE contains permissions for a particular SID. An SID can be a principal (PrincipalSid) or an authority (GrantedAuthoritySid) to associate with permissions. In addition to defining the ACL object model, Spring Security defines APIs for reading and maintaining the model, and it provides high-performance JDBC implementations for these APIs. To simplify ACL’s usages, Spring Security also provides facilities, such as access decision voters and JSP tags, for you to use ACL consistently with other security facilities in your application.

How It Works

First you will see how to setup an ACL service and how to maintain the ACL permissions for your entities. Finally you will learn how to use security expressions to secure access to your entities using the stored ACL permissions.

Set Up an ACL Service

Spring Security provides built-in support for storing ACL data in a relational database and accessing it with JDBC. First, you have to create the following tables in your database for storing ACL data:

CREATE TABLE ACL_SID(
    ID         BIGINT        NOT NULL GENERATED BY DEFAULT AS IDENTITY,
    SID        VARCHAR(100)  NOT NULL,
    PRINCIPAL  SMALLINT      NOT NULL,
    PRIMARY KEY (ID),
    UNIQUE (SID, PRINCIPAL)
);


CREATE TABLE ACL_CLASS(
    ID     BIGINT        NOT NULL GENERATED BY DEFAULT AS IDENTITY,
    CLASS  VARCHAR(100)  NOT NULL,
    PRIMARY KEY (ID),
    UNIQUE (CLASS)
);


CREATE TABLE ACL_OBJECT_IDENTITY(
    ID                  BIGINT    NOT NULL GENERATED BY DEFAULT AS IDENTITY,
    OBJECT_ID_CLASS     BIGINT    NOT NULL,
    OBJECT_ID_IDENTITY  BIGINT    NOT NULL,
    PARENT_OBJECT       BIGINT,
    OWNER_SID           BIGINT,
    ENTRIES_INHERITING  SMALLINT  NOT NULL,
    PRIMARY KEY (ID),
    UNIQUE (OBJECT_ID_CLASS, OBJECT_ID_IDENTITY),
    FOREIGN KEY (PARENT_OBJECT)   REFERENCES ACL_OBJECT_IDENTITY,
    FOREIGN KEY (OBJECT_ID_CLASS) REFERENCES ACL_CLASS,
    FOREIGN KEY (OWNER_SID)       REFERENCES ACL_SID
);


CREATE TABLE ACL_ENTRY(
    ID                  BIGINT    NOT NULL GENERATED BY DEFAULT AS IDENTITY,
    ACL_OBJECT_IDENTITY BIGINT    NOT NULL,
    ACE_ORDER           INT       NOT NULL,
    SID                 BIGINT    NOT NULL,
    MASK                INTEGER   NOT NULL,
    GRANTING            SMALLINT  NOT NULL,
    AUDIT_SUCCESS       SMALLINT  NOT NULL,
    AUDIT_FAILURE       SMALLINT  NOT NULL,
    PRIMARY KEY (ID),
    UNIQUE (ACL_OBJECT_IDENTITY, ACE_ORDER),
    FOREIGN KEY (ACL_OBJECT_IDENTITY) REFERENCES ACL_OBJECT_IDENTITY,
    FOREIGN KEY (SID)                 REFERENCES ACL_SID
);

Spring Security defines APIs and provides high-performance JDBC implementations for you to access ACL data stored in these tables, so you’ll seldom have a need to access ACL data from the database directly. As each domain object can have its own ACL, there may be a large number of ACLs in your application. Fortunately, Spring Security supports caching ACL objects. You can continue to use Ehcache as your cache implementation and create a new configuration for ACL caching in ehcache.xml (located in the classpath root).

<ehcache>                  
    ...
    <cache name="aclCache"
        maxElementsInMemory="1000"
        eternal="false"
        timeToIdleSeconds="600"
        timeToLiveSeconds="3600"
        overflowToDisk="true"
        />
</ehcache>

Next, you have to set up an ACL service for your application. However, as Spring Security doesn’t support configuring the ACL module with Java-based configuration yet, you have to configure this module with a group of normal Spring beans. For this reason, let’s create a separate bean configuration class named TodoAclConfig, which will store ACL-specific configurations, and add its location in the deployment descriptor.

package com.apress.springrecipes.board.security;

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

public class TodoSecurityInitializer extends AbstractSecurityWebApplicationInitializer {

    public TodoSecurityInitializer() {
        super(TodoSecurityConfig.class, TodoAclConfig.class);
    }
}

In an ACL configuration file, the core bean is an ACL service. In Spring Security, there are two interfaces that define operations of an ACL service: AclService and MutableAclService. AclService defines operations for you to read ACLs. MutableAclService is a subinterface of AclService that defines operations for you to create, update, and delete ACLs. If your application only needs to read ACLs, you can simply choose an AclService implementation, such as JdbcAclService. Otherwise, you should choose a MutableAclService implementation, such as JdbcMutableAclService.

package com.apress.springrecipes.board.security;

import org.springframework.cache.CacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.acls.AclEntryVoter;
import org.springframework.security.acls.domain.*;
import org.springframework.security.acls.jdbc.BasicLookupStrategy;
import org.springframework.security.acls.jdbc.JdbcMutableAclService;
import org.springframework.security.acls.jdbc.LookupStrategy;
import org.springframework.security.acls.model.AclCache;
import org.springframework.security.acls.model.AclService;
import org.springframework.security.acls.model.Permission;
import org.springframework.security.acls.model.PermissionGrantingStrategy;
import org.springframework.security.core.authority.SimpleGrantedAuthority;


import javax.sql.DataSource;

@Configuration
public class TodoAclConfig {


    private final DataSource dataSource;

    public TodoAclConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }


    @Bean
    public AclEntryVoter aclEntryVoter(AclService aclService) {
        return new AclEntryVoter(aclService, "ACL_MESSAGE_DELETE", new Permission[] {BasePermission.ADMINISTRATION, BasePermission.DELETE});
    }


    @Bean
    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
        return new EhCacheManagerFactoryBean();
    }


    @Bean
    public AuditLogger auditLogger() {
        return new ConsoleAuditLogger();
    }


    @Bean
    public PermissionGrantingStrategy permissionGrantingStrategy() {
        return new DefaultPermissionGrantingStrategy(auditLogger());
    }


    @Bean
    public AclAuthorizationStrategy aclAuthorizationStrategy() {
        return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ADMIN"));
    }


    @Bean
    public AclCache aclCache(CacheManager cacheManager) {
        return new SpringCacheBasedAclCache(cacheManager.getCache("aclCache"), permissionGrantingStrategy(), aclAuthorizationStrategy());
    }


    @Bean
    public LookupStrategy lookupStrategy(AclCache aclCache) {
        return new BasicLookupStrategy(this.dataSource, aclCache, aclAuthorizationStrategy(), permissionGrantingStrategy());
    }


    @Bean
    public AclService aclService(LookupStrategy lookupStrategy, AclCache aclCache) {
        return new JdbcMutableAclService(this.dataSource, lookupStrategy, aclCache);
    }
}

The core bean definition in this ACL configuration file is the ACL service, which is an instance of JdbcMutableAclService that allows you to maintain ACLs. This class requires three constructor arguments. The first is a data source for creating connections to a database that stores ACL data. You should have a data source defined beforehand so that you can simply refer to it here (assuming that you have created the ACL tables in the same database). The third constructor argument is a cache instance to use with an ACL, which you can configure using Ehcache as the back-end cache implementation.

The only implementation that comes with Spring Security is BasicLookupStrategy, which performs basic lookup using standard and compatible SQL statements. If you want to make use of advanced database features to increase lookup performance, you can create your own lookup strategy by implementing the LookupStrategy interface. A BasicLookupStrategy instance also requires a data source and a cache instance. Besides, it requires a constructor argument whose type is AclAuthorizationStrategy. This object determines whether a principal is authorized to change certain properties of an ACL, usually by specifying a required authority for each category of properties. For the preceding configurations, only a user who has the ADMIN authority can change an ACL’s ownership, an ACE’s auditing details, or other ACL and ACE details, respectively. Finally, it needs a constructor argument whose type is PermissionGrantingStrategy. This object’s responsibility is to check whether the ACL grants access to the given Sid with the Permissions value it has.

Finally, JdbcMutableAclService embeds standard SQL statements for maintaining ACL data in a relational database. However, those SQL statements may not be compatible with all database products. For example, you have to customize the identity query statement for Apache Derby.

Maintain ACLs for Domain Objects

In your back-end services and DAOs, you can maintain ACLs for domain objects with the previously defined ACL service via dependency injection. For your message board, you have to create an ACL for a to-do when it is posted and delete the ACL when this to-do is deleted.

package com.apress.springrecipes.board;

import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.acls.domain.*;
import org.springframework.security.acls.model.MutableAcl;
import org.springframework.security.acls.model.MutableAclService;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.stereotype.Service;


import javax.transaction.Transactional;
import java.util.List;


import static org.springframework.security.acls.domain.BasePermission.DELETE;
import static org.springframework.security.acls.domain.BasePermission.READ;
import static org.springframework.security.acls.domain.BasePermission.WRITE;


@Service
@Transactional
class TodoServiceImpl implements TodoService {


    private final TodoRepository todoRepository;
    private final MutableAclService mutableAclService;


    TodoServiceImpl(TodoRepository todoRepository, MutableAclService mutableAclService) {
        this.todoRepository = todoRepository;
        this.mutableAclService = mutableAclService;
    }


    @Override
    @PreAuthorize("hasAuthority('USER')")
    public void save(Todo todo) {


        this.todoRepository.save(todo);

        ObjectIdentity oid = new ObjectIdentityImpl(Todo.class, todo.getId());
        MutableAcl acl = mutableAclService.createAcl(oid);
        acl.insertAce(0, READ, new PrincipalSid(todo.getOwner()), true);
        acl.insertAce(1, WRITE, new PrincipalSid(todo.getOwner()), true);
        acl.insertAce(2, DELETE, new PrincipalSid(todo.getOwner()), true);

        acl.insertAce(3, READ, new GrantedAuthoritySid("ADMIN"), true);
        acl.insertAce(4, WRITE, new GrantedAuthoritySid("ADMIN"), true);
        acl.insertAce(5, DELETE, new GrantedAuthoritySid("ADMIN"), true);


    }

    @Override
    @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')")
    public void remove(long id) {
        todoRepository.remove(id);


        ObjectIdentity oid = new ObjectIdentityImpl(Todo.class, id);
        mutableAclService.deleteAcl(oid, false);
    }


    ...
}

When a user creates a to-do , you create a new ACL for this message at the same time, using the ID as the ACL’s object identity. When a user deletes a to-do, you delete the corresponding ACL as well. For a new to-do, you insert the following ACEs into its ACL:

  • The owner of the to-do can READ, WRITE, and DELETE the to-do.

  • A user who has the ADMIN authority can also READ, WRITE, and DELETE the to-dos.

JdbcMutableAclService requires that the calling methods have transactions enabled so that its SQL statements can run within transactions. So, you annotate the two methods involving ACL maintenance with the @Transactional annotation and then define a transaction manager and @EnableTransactionManagement on the TodoWebConfig. Also, don’t forget to inject the ACL service into TodoService for it to maintain ACL.

package com.apress.springrecipes.board.web;

import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
...


import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
...
public class TodoWebConfig implements WebMvcConfigurer {


    ...

    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

Make Access Control Decisions Using Expressions

With an ACL for each domain object, you can use an object’s ACL to make access control decisions on methods that involve this object. For example, when a user attempts to delete a to-do, you can consult this message’s ACL about whether the user is permitted to delete this to-do.

Configuring ACL can be a daunting task. Luckily, you can use annotations and expressions to make your life easier. You can use the @PreAuthorize and @PreFilter annotations to check whether someone is allowed to execute the method or use certain method arguments. The @PostAuthorize and @PostFilter annotations can be used to check whether a user is allowed to access the result or to filter results based on the ACL. To enable the processing of these annotations, you need to set the prePostEnabled attribute of the @EnableGlobalMethodSecurity annotation to true.

@EnableGlobalMethodSecurity(prePostEnabled=true)

In addition, you need to configure infrastructure components to be able to make decisions. You need to set up an AclPermissionEvaluator, which is needed to evaluate the permission for an object. This is done in TodoWebConfig and is needed here because that is the configuration class that enables the global method security, and as you want to use ACL to secure the methods using an expression, it needs the custom permission evaluator.

package com.apress.springrecipes.board.web.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.acls.AclPermissionEvaluator;
import org.springframework.security.acls.domain.AclAuthorizationStrategyImpl;
import org.springframework.security.acls.domain.ConsoleAuditLogger;
import org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy;
import org.springframework.security.acls.domain.SpringCacheBasedAclCache;
import org.springframework.security.acls.jdbc.BasicLookupStrategy;
import org.springframework.security.acls.jdbc.JdbcMutableAclService;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;


import javax.sql.DataSource;

@Configuration
public class TodoWebConfig {


    ...

    @Bean
    public AclPermissionEvaluator permissionEvaluator() {
        return new AclPermissionEvaluator(jdbcMutableAclService());
    }


}

The AclPermissionEvaluator requires an AclService to obtain the ACL for the objects it needs to check. When doing a Java-based configuration, this is enough because the PermissionEvaluator will be automatically detected and wired to the DefaultMethodSecurityExpressionHandler. Now everything is in place to use the annotations together with expressions to control access.

package com.apress.springrecipes.board;

...

@Service
@Transactional
class TodoServiceImpl implements TodoService {


    @Override
    @PreAuthorize("hasAuthority('USER')")
    @PostFilter("hasAnyAuthority('ADMIN') or hasPermission(filterObject, 'read')")
    public List<Todo> listTodos() { ... }


    @Override
    @PreAuthorize("hasAuthority('USER')")
    public void save(Todo todo) { ... }


    @Override
    @PreAuthorize("hasPermission(#id, 'com.apress.springrecipes.board.Todo', 'write')")
    public void complete(long id) { ... }


    @Override
    @PreAuthorize("hasPermission(#id, 'com.apress.springrecipes.board.Todo', 'delete')")
    public void remove(long id) { ... }


    @Override
    @PostFilter("hasPermission(filterObject, 'read')")
    public Todo findById(long id) { ... }
}

You probably noticed the different annotations and the expressions inside these annotations. The @PreAuthorize annotation can be used to check whether someone has the correct permissions to execute the method. The expression uses #message, which refers to the method argument with the name message. The hasPermission expression is a built-in expression from Spring Security (see Table 7-7).

The @PostFilter annotation allows you to filter the collection and remove the elements someone isn’t allowed to read. In the expression, the keyword filterObject refers to an element in the collection. To remain in the collection, the logged-in user needs to have read permission.

@PostAuthorize can be used to check whether a single return value can be used (i.e., if the user has the right permissions). To use the return value in an expression, use the keyword returnObject.

7-8. Add Security to a WebFlux Application

Problem

You have an application built with Spring WebFlux (see Chapter 5), and you want to add security.

Solution

Enable security by adding @EnableWebFluxSecurity to your configuration and create a SecurityWebFilterChain containing the security configuration.

How It Works

A Spring WebFlux application is very different in nature than a regular Spring MVC application. Nonetheless, Spring Security strives to make the configuration as easy as possible, and it tries to be as similar to regular web configuration as possible.

Secure URL Access

First let’s create a SecurityConfiguration class and put @EnableWebFluxSecurity on that class.

@Configuration
@EnableWebFluxSecurity
public class SecurityConfiguration { ... }

The @EnableWebFluxSecurity annotation registers a WebFluxConfigurer (see recipe 5-5) to add AuthenticationPrincipalArgumentResolver, which allows you to inject the Authentication object into a Spring WebFlux handler method. It also registers the WebFluxSecurityConfiguration class from Spring Security, which detects instances of SecurityWebFilterChain (containing the security configuration), which is wrapped as a WebFilter (comparable with a regular servlet filter), which in turn is used by WebFlux to add behavior to incoming requests (just like a normal servlet filter).

Your configuration now only enables security; let’s add some security rules.

@Bean
SecurityWebFilterChain springWebFilterChain(HttpSecurity http) throws Exception {
    return http
        .authorizeExchange()
            .pathMatchers("/welcome", "/welcome/**").permitAll()
            .pathMatchers("/reservation*").hasRole("USER")
            .anyExchange().authenticated()
        .and()
        .build();
}

org.springframework.security.config.annotation.web.reactive.HttpSecurity should look familiar (see recipe 7-1) and is used to add security rules and do further configuration (such as adding/removing headers and configuring the login method). With the authorizeExchange, it is possible to write rules. Here you secure URLs; the /welcome URL is permitted for everyone, and the /reservation URLs are available only for the role USER. For other requests, you have to be authenticated. Finally, you need to call build() to actually build the SecurityWebFilterChain.

In addition to the authorizeExchange, it is also possible to use the headers() configuration method to add security headers to requests (see also recipe 7-2) such as cross-site scripting protection, cache headers, and so on.

Log in to WebFlux Applications

Currently, there is only the httpBasic() authentication mechanism supported by Spring Security WebFlux, and it is enabled by default. You could override parts of the default configuration by explicitly configuring them, and you could override the authentication manager used and the repository used to store the security context. The authentication manager is detected automatically; you just need to register a bean of type ReactiveAuthenticationManager or of type UserDetailsRepository.

You can also configure the location where the SecurityContext value is stored by configuring SecurityContextRepository. The default implementation used is the WebSessionSecurityContextRepository, which stores the context in the WebSession. The other default implementation the ServerWebExchangeAttributeSecurityContextRepository stores the SecurityContext as an attribute for the current exchange (i.e., request).

@Bean
SecurityWebFilterChain springWebFilterChain(HttpSecurity http) throws Exception {
    return http.httpBasic().
        .authenticationManager(new CustomReactiveAuthenticationManager())
        .securityContextRepository(new ServerWebExchangeAttributeSecurityContextRepository()).and().build();
}

This will override the defaults with a CustomReactiveAuthenticationManager and the stateless ServerWebExchangeAttributeSecurityContextRepository. However, for this application, you are going to stick with the defaults.

Authenticate Users

Authenticating users in a Spring WebFlux-based application is done through a ReactiveAuthenticationManager. This is an interface with a single authenticate method. You can either provide your own implementation or use one of the two provided implementations. The first is the UserDetailsRepositoryAuthenticationManager, which wraps an instance of UserDetailsRepository.

Note

The UserDetailsRepository has only a single implementation, the MapUserDetailsRepository, which is an in-memory implementation. You could, of course, provide your own implementation based on a reactive data store (like MongoDB or Couchbase).

The other implementation, ReactiveAuthenticationManagerAdapter, is actually a wrapper for a regular AuthenticationManager (see recipe 7-3). It will wrap a regular instance, and because of that, you can use the blocking implementations in a reactive way. This doesn’t make them reactive; they still block, but they are reusable in this way. With this, you could use JDBC, LDAP, and so on, for your reactive application.

When configuring Spring Security in a Spring WebFlux application, you can add an instance of either a ReactiveAuthenticationManager to your Java configuration class or a UserDetailsRepository. When the latter is detected, it will automatically be wrapped in a UserDetailsRepositoryAuthenticationManager.

@Bean
public MapUserDetailsRepository userDetailsRepository() {
    UserDetails marten = User.withUsername("marten").password("secret").roles("USER").build();
    UserDetails admin = User.withUsername("admin").password("admin").roles("USER","ADMIN").build();
    return new MapUserDetailsRepository(marten, admin);
}

When you now deploy the application (or run the ReactorNettyBootstrap class), you are free to access the /welcome page, but when accessing a URL starting with /reservation, you are greeted by a basic authentication prompt from the browser (Figure 7-3).

A314861_4_En_7_Fig3_HTML.jpg
Figure 7-3. Basic authentication login screen

Make Access Control Decisions

Table 7-8 shows the Spring Security WebFlux built-in expressions .

Table 7-8. Spring Security WebFlux Built-in Expressions

Expression

Description

hasRole(role) or hasAuthority(authority)

Returns true if the current user has the given role

permitAll()

Always evaluates to true

denyAll()

Always evaluates to false

authenticated()

Returns true if the user is authenticated

access()

Use a function to determine whether access is granted

Caution

Although the role and authority are almost the same, there is a slight, but important, difference in how they are processed. When using hasRole, the passed-in value for the role will checked if it starts with ROLE_ (the default role prefix). If not, this will be added before checking the authority. So, hasRole('ADMIN') will actually check whether the current user has the authority ROLE_ADMIN. When using hasAuthority, it will check the value as is.

@Bean
SecurityWebFilterChain springWebFilterChain(HttpSecurity http) throws Exception {
    return http
        .authorizeExchange()
            .pathMatchers("/users/{user}/**").access(this::userEditAllowed)
            .anyExchange().authenticated()
        .and()
        .build();
}


private Mono<AuthorizationDecision> userEditAllowed(Mono<Authentication> authentication, AuthorizationContext context) {
    return authentication
        .map( a -> context.getVariables().get("user").equals(a.getName()) || a.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN")))
        .map( granted -> new AuthorizationDecision(granted));
}

The access() expression can be used to write powerful expressions. The previous snippet uses a path parameter in the URL {user}, and access is allowed if the current user is the actual user or if someone has the ROLE_ADMIN authority. AuthorizationContext contains the parsed variables that you could use to compare the name from the URI. Authentication contains the collection of GrantedAuthorities, which you can check for the ROLE_ADMIN authority. Of course, you can write as many complex expressions as you like; you could check for the IP address, request headers, and so on.

Summary

In this chapter, you learned how to secure applications using Spring Security. It can be used to secure any Java application, but it’s mostly used for web applications. The concepts of authentication, authorization, and access control are essential in the security area, so you should have a clear understanding of them.

You often have to secure critical URLs by preventing unauthorized access to them. Spring Security can help you to achieve this in a declarative way. It handles security by applying servlet filters, which can be configured with a simple Java-based configuration. Spring Security will automatically configure the basic security services for you and tries to be as secure as possible by default.

Spring Security supports multiple ways for users to log into a web application, such as form-based login and HTTP Basic authentication. It also provides an anonymous login service that allows you to handle an anonymous user just like a normal user. Remember-me support allows an application to remember a user’s identity across multiple browser sessions.

Spring Security supports multiple ways of authenticating users and has built-in provider implementations for them. For example, it supports authenticating users against in-memory definitions, a relational database, and an LDAP repository. You should always store encrypted passwords in your user repository because clear-text passwords are vulnerable to hacker attacks. Spring Security also supports caching user details locally to save you the overhead of performing remote queries.

Decisions on whether a user is allowed to access a given resource are made by access decision managers. Spring Security comes with three access decision managers that are based on the voting approach. All of them require a group of voters to be configured for voting on access control decisions.

Spring Security enables you to secure method invocations in a declarative way, either by embedding a security interceptor in a bean definition or by matching multiple methods with AspectJ pointcut expressions or annotations. Spring Security also allows you to display a user’s authentication information in JSP views and render view contents conditionally according to a user’s authorities.

Spring Security provides an ACL module that allows each domain object to have an ACL for controlling access. You can read and maintain an ACL for each domain object with Spring Security’s high-performance APIs, which are implemented with JDBC. Spring Security also provides facilities such as access decision voters and JSP tags for you to use ACLs consistently with other security facilities.

Spring Security also has support for securing Spring WebFlux-based applications. In the previous recipe, you explored how you can add security to such an application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset