Have you ever noticed that most people in television sitcoms don’t lock their doors? In the days of Leave It to Beaver, it wasn’t so unusual for people to leave their doors unlocked. But it seems crazy that at a time when we’re concerned with privacy and security, we see television characters enabling unhindered access to their apartments and homes.
Information is probably the most valuable item we now have; crooks are looking for ways to steal our data and identities by sneaking into unsecured applications. As software developers, we must take steps to protect the information that resides in our applications. Whether it’s an email account protected with a username-password pair or a brokerage account protected with a trading PIN, security is a crucial aspect of most applications.
The very first step in securing your Spring application is to add the Spring Boot security starter dependency to your build. In the project’s pom.xml file, add the following <dependency>
entry:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
If you’re using Spring Tool Suite, this is even easier. Right-click on the pom.xml file and select Add Starters from the Spring context menu. In the starter dependencies dialog box, select the Spring Security entry under the Security category, as shown in figure 5.1.
Believe it or not, that dependency is the only thing that’s required to secure an application. When the application starts, autoconfiguration will detect that Spring Security is in the classpath and will set up some basic security configuration.
If you want to try it out, fire up the application and try to visit the home page (or any page, for that matter). You’ll be prompted for authentication with a rather plain login page that looks something like figure 5.2.
Tip Going incognito: You may find it useful to set your browser to private or incognito mode when manually testing security. This will ensure that you have a fresh session each time you open a private/incognito window. You’ll have to sign in to the application each time, but you can be assured that any changes you’ve made in security are applied and that there aren’t any remnants of an older session preventing you from seeing your changes.
To get past the login page, you’ll need to provide a username and password. The username is user. As for the password, it’s randomly generated and written to the application log file. The log entry will look something like this:
Assuming you enter the username and password correctly, you’ll be granted access to the application.
It seems that securing Spring applications is pretty easy work. With the Taco Cloud application secured, I suppose I could end this chapter now and move on to the next topic. But before we get ahead of ourselves, let’s consider what kind of security autoconfiguration has provided.
By doing nothing more than adding the security starter to the project build, you get the following security features:
This is a good start, but I think that the security needs of most applications (Taco Cloud included) will be quite different from these rudimentary security features.
You have more work to do if you’re going to properly secure the Taco Cloud application. You’ll need to at least configure Spring Security to do the following:
Provide for multiple users, and enable a registration page so new Taco Cloud customers can sign up.
Apply different security rules for different request paths. The home page and registration pages, for example, shouldn’t require authentication at all.
To meet your security needs for Taco Cloud, you’ll have to write some explicit configuration, overriding what autoconfiguration has given you. You’ll start by configuring a proper user store so that you can have more than one user.
Over the years, several ways of configuring Spring Security have existed, including lengthy XML configuration. Fortunately, several recent versions of Spring Security have supported Java configuration, which is much easier to read and write.
Before this chapter is finished, you’ll have configured all of your Taco Cloud security needs in a Java configuration for Spring Security. But to get started, you’ll ease into it by writing the configuration class shown in the following listing.
package tacos.security; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
What does this barebones security configuration do for you? Not much, actually. The main thing it does is declare a PasswordEncoder
bean, which we’ll use both when creating new users and when authenticating users at login. In this case, we’re using BCryptPasswordEncoder
, one of a handful of password encoders provided by Spring Security, including the following:
No matter which password encoder you use, it’s important to understand that the password in the database is never decoded. Instead, the password that the user enters at login is encoded using the same algorithm, and it’s then compared with the encoded password in the database. That comparison is performed in the PasswordEncoder
’s matches()
method.
In addition to the password encoder, we’ll fill in this configuration class with more beans to define the specifics of security for our application. We’ll start by configuring a user store that can handle more than one user.
To configure a user store for authentication purposes, you’ll need to declare a UserDetailsService
bean. The UserDetailsService
interface is relatively simple, including only one method that must be implemented. Here’s what UserDetailsService
looks like:
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
The loadUserByUsername()
method accepts a username and uses it to look up a UserDetails
object. If no user can be found for the given username, then it will throw a UsernameNotFoundException
.
As it turns out, Spring Security offers several out-of-the-box implementations of UserDetailsService
, including the following:
Or, you can also create your own implementation to suit your application’s specific security needs.
To get started, let’s try out the in-memory implementation of UserDetailsService
.
One place where user information can be kept is in memory. Suppose you have only a handful of users, none of which are likely to change. In that case, it may be simple enough to define those users as part of the security configuration.
The following bean method shows how to create an InMemoryUserDetailsManager
(which implements UserDetailsService) with two users, “buzz” and “woody,” for that purpose.
@Bean public UserDetailsService userDetailsService(PasswordEncoder encoder) { List<UserDetails> usersList = new ArrayList<>(); usersList.add(new User( "buzz", encoder.encode("password"), Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")))); usersList.add(new User( "woody", encoder.encode("password"), Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")))); return new InMemoryUserDetailsManager(usersList); }
Here, a list of Spring Security User
objects are created, each with a username, password, and a list of one or more authorities. Then an InMemoryUserDetailsManager
is created using that list.
If you try out the application now, you should be able to log in as either “woody” or “buzz,” using password as the password.
The in-memory user details service is convenient for testing purposes or for very simple applications, but it doesn’t allow for easy editing of users. If you need to add, remove, or change a user, you’ll have to make the necessary changes and then rebuild and redeploy the application.
For the Taco Cloud application, you want customers to be able to register with the application and manage their own user accounts. That doesn’t fit with the limitations of the in-memory user details service. So let’s take a look at how to create our own implementation of UserDetailsService
that allows for a user store database.
In the previous chapter, you settled on using Spring Data JPA as your persistence option for all taco, ingredient, and order data. It would thus make sense to persist user data in the same way. If you do so, the data will ultimately reside in a relational database, so you could use JDBC authentication. But it’d be even better to leverage the Spring Data JPA repository used to store users.
First things first, though. Let’s create the domain object and repository interface that represents and persists user information.
Defining the user domain and persistence
When Taco Cloud customers register with the application, they’ll need to provide more than just a username and password. They’ll also give you their full name, address, and phone number. This information can be used for a variety of purposes, including prepopulating the order form (not to mention potential marketing opportunities).
To capture all of that information, you’ll create a User
class, as follows.
package tacos; import java.util.Arrays; import java.util.Collection; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority. SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import lombok.AccessLevel; import lombok.Data; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; @Entity @Data @NoArgsConstructor(access=AccessLevel.PRIVATE, force=true) @RequiredArgsConstructor public class User implements UserDetails { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy=GenerationType.AUTO) private Long id; private final String username; private final String password; private final String fullname; private final String street; private final String city; private final String state; private final String zip; private final String phoneNumber; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
The first thing to notice about this User
type is that it’s not the same as the User
class we used when creating the in-memory user details service. This one has more details about the user that we’ll need to fulfill taco orders, including the user’s address and contact information.
You’ve also probably noticed that the User
class is a bit more involved than any of the other entities defined in chapter 3. In addition to defining a handful of properties, User
also implements the UserDetails
interface from Spring Security.
Implementations of UserDetails
will provide some essential user information to the framework, such as what authorities are granted to the user and whether the user’s account is enabled.
The getAuthorities()
method should return a collection of authorities granted to the user. The various is*
methods return a boolean
to indicate whether the user’s account is enabled, locked, or expired.
For your User
entity, the getAuthorities()
method simply returns a collection indicating that all users will have been granted ROLE_USER
authority. And, at least for now, Taco Cloud has no need to disable users, so all of the is*
methods return true
to indicate that the users are active.
With the User
entity defined, you can now define the repository interface as follows:
package tacos.data; import org.springframework.data.repository.CrudRepository; import tacos.User; public interface UserRepository extends CrudRepository<User, Long> { User findByUsername(String username); }
In addition to the CRUD operations provided by extending CrudRepository
, UserRepository
defines a findByUsername()
method that you’ll use in the user details service to look up a User
by their username.
As you learned in chapter 3, Spring Data JPA automatically generates the implementation of this interface at run time. Therefore, you’re now ready to write a custom user details service that uses this repository.
Creating a user details service
As you’ll recall, the UserDetailsService
interface defines only a single loadUserByUsername()
method. That means it is a functional interface and can be implemented as a lambda instead of as a full-blown implementation class. Because all we really need is for our custom UserDetailsService
to delegate to the UserRepository
, it can be simply declared as a bean using the following configuration method.
@Bean public UserDetailsService userDetailsService(UserRepository userRepo) { return username -> { User user = userRepo.findByUsername(username); if (user != null) return user; throw new UsernameNotFoundException("User '" + username + "' not found"); }; }
The userDetailsService()
method is given a UserRepository
as a parameter. To create the bean, it returns a lambda that takes a username
parameter and uses it to call findByUsername()
on the given UserRepository
.
The loadByUsername()
method has one simple rule: it must never return null
. Therefore, if the call to findByUsername()
returns null
, the lambda will throw a UsernameNotFoundException
(which is defined by Spring Security). Otherwise, the User
that was found will be returned.
Now that you have a custom user details service that reads user information via a JPA repository, you just need a way to get users into the database in the first place. You need to create a registration page for Taco Cloud patrons to register with the application.
Although Spring Security handles many aspects of security, it really isn’t directly involved in the process of user registration, so you’re going to rely on a little bit of Spring MVC to handle that task. The RegistrationController
class in the following listing presents and processes registration forms.
package tacos.security; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import tacos.data.UserRepository; @Controller @RequestMapping("/register") public class RegistrationController { private UserRepository userRepo; private PasswordEncoder passwordEncoder; public RegistrationController( UserRepository userRepo, PasswordEncoder passwordEncoder) { this.userRepo = userRepo; this.passwordEncoder = passwordEncoder; } @GetMapping public String registerForm() { return "registration"; } @PostMapping public String processRegistration(RegistrationForm form) { userRepo.save(form.toUser(passwordEncoder)); return "redirect:/login"; } }
Like any typical Spring MVC controller, RegistrationController
is annotated with @Controller
to designate it as a controller and to mark it for component scanning. It’s also annotated with @RequestMapping
such that it will handle requests whose path is /register.
More specifically, a GET
request for /register will be handled by the registerForm()
method, which simply returns a logical view name of registration
. The following listing shows a Thymeleaf template that defines the registration
view.
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <title>Taco Cloud</title> </head> <body> <h1>Register</h1> <img th:src="@{/images/TacoCloud.png}"/> <form method="POST" th:action="@{/register}" id="registerForm"> <label for="username">Username: </label> <input type="text" name="username"/><br/> <label for="password">Password: </label> <input type="password" name="password"/><br/> <label for="confirm">Confirm password: </label> <input type="password" name="confirm"/><br/> <label for="fullname">Full name: </label> <input type="text" name="fullname"/><br/> <label for="street">Street: </label> <input type="text" name="street"/><br/> <label for="city">City: </label> <input type="text" name="city"/><br/> <label for="state">State: </label> <input type="text" name="state"/><br/> <label for="zip">Zip: </label> <input type="text" name="zip"/><br/> <label for="phone">Phone: </label> <input type="text" name="phone"/><br/> <input type="submit" value="Register"/> </form> </body> </html>
When the form is submitted, the processRegistration()
method handles the HTTPS POST
request. The form fields will be bound to a RegistrationForm
object by Spring MVC and passed into the processRegistration()
method for processing. RegistrationForm
is defined in the following class:
package tacos.security; import org.springframework.security.crypto.password.PasswordEncoder; import lombok.Data; import tacos.User; @Data public class RegistrationForm { private String username; private String password; private String fullname; private String street; private String city; private String state; private String zip; private String phone; public User toUser(PasswordEncoder passwordEncoder) { return new User( username, passwordEncoder.encode(password), fullname, street, city, state, zip, phone); } }
For the most part, RegistrationForm
is just a basic Lombok class with a handful of properties. But the toUser()
method uses those properties to create a new User
object, which is what processRegistration()
will save, using the injected UserRepository
.
You’ve no doubt noticed that RegistrationController
is injected with a PasswordEncoder
. This is the exact same PasswordEncoder
bean you declared earlier. When processing a form submission, RegistrationController
passes it to the toUser()
method, which uses it to encode the password before saving it to the database. In this way, the submitted password is written in an encoded form, and the user details service will be able to authenticate against that encoded password.
Now the Taco Cloud application has complete user registration and authentication support. But if you start it up at this point, you’ll notice that you can’t even get to the registration page without being prompted to log in. That’s because, by default, all requests require authentication. Let’s look at how web requests are intercepted and secured so you can fix this strange chicken-and-egg situation.
The security requirements for Taco Cloud should require that a user be authenticated before designing tacos or placing orders. But the home page, login page, and registration page should be available to unauthenticated users.
To configure these security rules, we’ll need to declare a SecurityFilterChain
bean. The following @Bean
method shows a minimal (but not useful) SecurityFilterChain
bean declaration:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.build(); }
The filterChain()
method accepts an HttpSecurity
object, which acts as a builder that can be used to configure how security is handled at the web level. Once security configuration is set up via the HttpSecurity
object, a call to build()
will create a SecurityFilterChain
that is returned from the bean method.
The following are among the many things you can configure with HttpSecurity
:
Intercepting requests to ensure that the user has proper authority is one of the most common things you’ll configure HttpSecurity
to do. Let’s ensure that your Taco Cloud customers meet those requirements.
You need to ensure that requests for /design and /orders are available only to authenticated users; all other requests should be permitted for all users. The following configuration does exactly that:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeRequests() .antMatchers("/design", "/orders").hasRole("USER") .antMatchers("/", "/**").permitAll() .and() .build(); }
The call to authorizeRequests()
returns an object (ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry
) on which you can specify URL paths and patterns and the security requirements for those paths. In this case, you specify the following two security rules:
Requests for /design and /orders should be for users with a granted authority of ROLE_USER
. Don’t include the ROLE_
prefix on roles passed to hasRole()
; it will be assumed by hasRole()
.
The order of these rules is important. Security rules declared first take precedence over those declared lower down. If you were to swap the order of those two security rules, all requests would have permitAll()
applied to them; the rule for /design and /orders requests would have no effect.
The hasRole()
and permitAll()
methods are just a couple of the methods for declaring security requirements for request paths. Table 5.1 describes all the available methods.
Most of the methods in table 5.1 provide essential security rules for request handling, but they’re self-limiting, enabling security rules only as defined by those methods. Alternatively, you can use the access()
method to provide a SpEL expression to declare richer security rules. Spring Security extends SpEL to include several security-specific values and functions, as listed in table 5.2.
As you can see, most of the security expression extensions in table 5.2 correspond to similar methods in table 5.1. In fact, using the access()
method along with the hasRole()
and permitAll
expressions, you can rewrite the SecurityFilterChain
configuration as follows.
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeRequests() .antMatchers("/design", "/orders").access("hasRole('USER')") .antMatchers("/", "/**").access("permitAll()") .and() .build(); }
This may not seem like a big deal at first. After all, these expressions only mirror what you already did with method calls. But expressions can be much more flexible. For instance, suppose that (for some crazy reason) you wanted to allow only users with ROLE_USER
authority to create new tacos on Tuesdays (for example, on Taco Tuesday); you could rewrite the expression as shown in this modified version of the SecurityFilterChain
bean method:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeRequests() .antMatchers("/design", "/orders") .access("hasRole('USER') && " + "T(java.util.Calendar).getInstance().get("+ "T(java.util.Calendar).DAY_OF_WEEK) == " + "T(java.util.Calendar).TUESDAY") .antMatchers("/", "/**").access("permitAll()") .and() .build(); }
With SpEL security constraints, the possibilities are virtually endless. I’ll bet that you’re already dreaming up interesting security constraints based on SpEL.
The authorization needs for the Taco Cloud application are met by the simple use of access()
and the SpEL expressions. Now let’s see about customizing the login page to fit the look of the Taco Cloud application.
The default login page is much better than the clunky HTTP basic dialog box you started with, but it’s still rather plain and doesn’t quite fit with the look of the rest of the Taco Cloud application.
To replace the built-in login page, you first need to tell Spring Security what path your custom login page will be at. That can be done by calling formLogin()
on the HttpSecurity
object, as shown next:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeRequests() .antMatchers("/design", "/orders").access("hasRole('USER')") .antMatchers("/", "/**").access("permitAll()") .and() .formLogin() .loginPage("/login") .and() .build(); }
Notice that before you call formLogin()
, you bridge this section of configuration and the previous section with a call to and()
. The and()
method signifies that you’re finished with the authorization configuration and are ready to apply some additional HTTP configuration. You’ll use and()
several times as you begin new sections of configuration.
After the bridge, you call formLogin()
to start configuring your custom login form. The call to loginPage()
after that designates the path where your custom login page will be provided. When Spring Security determines that the user is unauthenticated and needs to log in, it will redirect them to this path.
Now you need to provide a controller that handles requests at that path. Because your login page will be fairly simple—nothing but a view—it’s easy enough to declare it as a view controller in WebConfig
. The following addViewControllers()
method sets up the login page view controller alongside the view controller that maps “/” to the home controller:
@Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("home"); registry.addViewController("/login"); }
Finally, you need to define the login page view itself. Because you’re using Thymeleaf as your template engine, the following Thymeleaf template should do fine:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <title>Taco Cloud</title> </head> <body> <h1>Login</h1> <img th:src="@{/images/TacoCloud.png}"/> <div th:if="${error}"> Unable to login. Check your username and password. </div> <p>New here? Click <a th:href="@{/register}">here</a> to register.</p> <form method="POST" th:action="@{/login}" id="loginForm"> <label for="username">Username: </label> <input type="text" name="username" id="username" /><br/> <label for="password">Password: </label> <input type="password" name="password" id="password" /><br/> <input type="submit" value="Login"/> </form> </body> </html>
The key things to note about this login page are the path it posts to and the names of the username and password fields. By default, Spring Security listens for login requests at /login and expects that the username and password fields be named username
and password
. This is configurable, however. For example, the following configuration customizes the path and field names:
.and() .formLogin() .loginPage("/login") .loginProcessingUrl("/authenticate") .usernameParameter("user") .passwordParameter("pwd")
Here, you specify that Spring Security should listen for requests to /authenticate to handle login submissions. Also, the username and password fields should now be named user
and pwd
.
By default, a successful login will take the user directly to the page that they were navigating to when Spring Security determined that they needed to log in. If the user were to directly navigate to the login page, a successful login would take them to the root path (for example, the home page). But you can change that by specifying a default success page, as shown next:
As configured here, if the user were to successfully log in after directly going to the login page, they would be directed to the /design page.
Optionally, you can force the user to the design page after login, even if they were navigating elsewhere prior to logging in, by passing true
as a second parameter to defaultSuccessUrl
as follows:
Signing in with a username and password is the most common way to authenticate in a web application. But let’s have a look at another way to authenticate a user that uses someone else’s login page.
You may have seen links or buttons on your favorite website that say “Sign in with Facebook,” “Log in with Twitter,” or something similar. Rather than asking a user to enter their credentials on a login page specific to the website, they offer a way to sign in via another website like Facebook that they may already be logged into.
This type of authentication is based on OAuth2 or OpenID Connect (OIDC). Although OAuth2 is an authorization specification, and we’ll talk more about how to use it to secure REST APIs in chapter 8, it can be also used to perform authentication via a third-party website. OpenID Connect is another security specification that is based on OAuth2 to formalize the interaction that takes place during a third-party authentication.
To employ this type of authentication in your Spring application, you’ll need to add the OAuth2 client starter to the build as follows:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency>
Then, at the very least, you’ll need to configure details about one or more OAuth2 or OpenID Connect servers that you want to be able to authenticate against. Spring Security supports sign-in with Facebook, Google, GitHub, and Okta out of the box, but you can configure other clients by specifying a few extra properties.
The general set of properties you’ll need to set for your application to act as an OAuth2/OpenID Connect client follows:
spring: security: oauth2: client: registration: <oauth2 or openid provider name>: clientId: <client id> clientSecret: <client secret> scope: <comma-separated list of requested scopes>
For example, suppose that for Taco Cloud, we want users to be able to sign in using Facebook. The following configuration in application.yml will set up the OAuth2 client:
spring: security: oauth2: client: registration: facebook: clientId: <facebook client id> clientSecret: <facebook client secret> scope: email, public_profile
The client ID and secret are the credentials that identify your application to Facebook. You can obtain a client ID and secret by creating a new application entry at https://developers.facebook.com/. The scope
property specifies the access that the application will be granted. In this case, the application will have access to the user’s email address and the essential information from their public Facebook profile.
In a very simple application, this is all you will need. When the user attempts to access a page that requires authentication, their browser will redirect to Facebook. If they’re not already logged in to Facebook, they’ll be greeted with the Facebook sign-in page. After signing in to Facebook, they’ll be asked to authorize your application and grant the requested scope. Finally, they’ll be redirected back to your application, where they will have been authenticated.
If, however, you’ve customized security by declaring a SecurityFilterChain
bean, then you’ll need to enable OAuth2 login along with the rest of the security configuration as follows:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeRequests() .mvcMatchers("/design", "/orders").hasRole("USER") .anyRequest().permitAll() .and() .formLogin() .loginPage("/login") .and() .oauth2Login() ... .and() .build(); }
You may also want to offer both a traditional username-password login and third-party login. In that case, you can specify the login page in the configuration like this:
This will cause the application to always take the user to the application-provided login page where they may choose to log in with their username and password as usual. But you can also provide a link on that same login page that offers them the opportunity to log in with Facebook. Such a link could look like this in the login page’s HTML template:
Now that you’ve dealt with logging in, let’s flip to the other side of the authentication coin and see how you can enable a user to log out. Just as important as logging in to an application is logging out. To enable logout, you simply need to call logout
on the HttpSecurity
object as follows:
This sets up a security filter that intercepts POST
requests to /logout. Therefore, to provide logout capability, you just need to add a logout form and button to the views in your application, as shown next:
When the user clicks the button, their session will be cleared, and they will be logged out of the application. By default, they’ll be redirected to the login page where they can log in again. But if you’d rather they be sent to a different page, you can call logoutSuccessUrl()
to specify a different post-logout landing page, as shown here:
In this case, users will be sent to the home page following logout.
Cross-site request forgery (CSRF) is a common security attack. It involves subjecting a user to code on a maliciously designed web page that automatically (and usually secretly) submits a form to another application on behalf of a user who is often the victim of the attack. For example, a user may be presented with a form on an attacker’s website that automatically posts to a URL on the user’s banking website (which is presumably poorly designed and vulnerable to such an attack) to transfer money. The user may not even know that the attack happened until they notice money missing from their account.
To protect against such attacks, applications can generate a CSRF token upon displaying a form, place that token in a hidden field, and then stow it for later use on the server. When the form is submitted, the token is sent back to the server along with the rest of the form data. The request is then intercepted by the server and compared with the token that was originally generated. If the token matches, the request is allowed to proceed. Otherwise, the form must have been rendered by an evil website without knowledge of the token generated by the server.
Fortunately, Spring Security has built-in CSRF protection. Even more fortunate is that it’s enabled by default and you don’t need to explicitly configure it. You only need to make sure that any forms your application submits include a field named _csrf
that contains the CSRF token.
Spring Security even makes that easy by placing the CSRF token in a request attribute with the name _csrf
. Therefore, you could render the CSRF token in a hidden field with the following in a Thymeleaf template:
If you’re using Spring MVC’s JSP tag library or Thymeleaf with the Spring Security dialect, you needn’t even bother explicitly including a hidden field. The hidden field will be rendered automatically for you.
In Thymeleaf, you just need to make sure that one of the attributes of the <form>
element is prefixed as a Thymeleaf attribute. That’s usually not a concern, because it’s quite common to let Thymeleaf render the path as context relative. For example, the th:action
attribute shown next is all you need for Thymeleaf to render the hidden field for you:
It’s possible to disable CSRF support, but I’m hesitant to show you how. CSRF protection is important and easily handled in forms, so there’s little reason to disable it. But if you insist on disabling it, you can do so by calling disable()
like this:
Again, I caution you not to disable CSRF protection, especially for production applications.
All of your web layer security is now configured for Taco Cloud. Among other things, you now have a custom login page and the ability to authenticate users against a JPA user repository. Now let’s see how you can obtain information about the logged-in user.
Although it’s easy to think about security at the web-request level, that’s not always where security constraints are best applied. Sometimes it’s better to verify that the user is authenticated and has been granted adequate authority at the point where the secured action will be performed.
For example, let’s say that for administrative purposes, we have a service class that includes a method for clearing out all orders from the database. Using an injected OrderRepository
, that method might look something like this:
Now, suppose we have a controller that calls the deleteAllOrders()
method as the result of a POST
request, as shown here:
@Controller @RequestMapping("/admin") public class AdminController { private OrderAdminService adminService; public AdminController(OrderAdminService adminService) { this.adminService = adminService; } @PostMapping("/deleteOrders") public String deleteAllOrders() { adminService.deleteAllOrders(); return "redirect:/admin"; } }
It’d be easy enough to tweak SecurityConfig
as follows to ensure that only authorized users are allowed to perform that POST
request:
.authorizeRequests() ... .antMatchers(HttpMethod.POST, "/admin/**") .access("hasRole('ADMIN')") ....
That’s great and would prevent any unauthorized user from making a POST
request to /admin/deleteOrders that would result in all orders disappearing from the database.
But suppose that some other controller method also calls deleteAllOrders()
. You’d need to add more matchers to secure the requests for the other controllers that will need to be secured.
Instead, we can apply security directly on the deleteAllOrders()
method like this:
The @PreAuthorize
annotation takes a SpEL expression, and, if the expression evaluates to false
, the method will not be invoked. On the other hand, if the expression evaluates to true
, then the method will be allowed. In this case, @PreAuthorize
is checking that the user has the ROLE_ADMIN
privilege. If so, then the method will be called and all orders will be deleted. Otherwise, it will be stopped in its tracks.
In the event that @PreAuthorize
blocks the call, then Spring Security’s AccessDeniedException
will be thrown. This is an unchecked exception, so you don’t need to catch it, unless you want to apply some custom behavior around the exception handling. If left uncaught, it will bubble up and eventually be caught by Spring Security’s filters and handled accordingly, either with an HTTP 403 page or perhaps by redirecting to the login page if the user is unauthenticated.
For @PreAuthorize
to work, you’ll need to enable global method security. For that, you’ll need to annotate the security configuration class with @EnableGlobalMethodSecurity
as follows:
@Configuration @EnableGlobalMethodSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { ... }
You’ll find @PreAuthorize
to be a useful annotation for most method-level security needs. But know that it has a slightly less useful after-invocation counterpart in @PostAuthorize
. The @PostAuthorize
annotation works almost the same as the @PreAuthorize
annotation, except that its expression won’t be evaluated until after the target method is invoked and returns. This allows the expression to consider the return value of the method in deciding whether to permit the method invocation.
For example, suppose we have a method that fetches an order by its ID. If you want to restrict it from being used except by admins or by the user who the order belongs to, you can use @PostAuthorize
like this:
@PostAuthorize("hasRole('ADMIN') || " + "returnObject.user.username == authentication.name") public TacoOrder getOrder(long id) { ... }
In this case, the returnObject
is the TacoOrder
returned from the method. If its user
property has a username
that is equal to the authentication’s name
property, then it will be allowed. To know that, though, the method will need to be executed so that it can return the TacoOrder
object for consideration.
But wait! How can you secure a method from being invoked if the condition for applying security relies on the return value from the method invocation? That chicken-and-egg riddle is solved by allowing the method to be invoked, then throwing an AccessDeniedException
if the expression returns false
.
Often, it’s not enough to simply know that the user has logged in and what permissions they have been granted. It’s usually important to also know who they are, so that you can tailor their experience.
For example, in OrderController
, when you initially create the TacoOrder
object that’s bound to the order form, it’d be nice if you could prepopulate the TacoOrder
with the user’s name and address, so they don’t have to reenter it for each order. Perhaps even more important, when you save their order, you should associate the TacoOrder
entity with the User
that created the order.
To achieve the desired connection between an TacoOrder
entity and a User
entity, you need to add the following new property to the TacoOrder
class:
@Data @Entity @Table(name="Taco_Order") public class TacoOrder implements Serializable { ... @ManyToOne private User user; ... }
The @ManyToOne
annotation on this property indicates that an order belongs to a single user and, conversely, that a user may have many orders. (Because you’re using Lombok, you won’t need to explicitly define accessor methods for the property.)
In OrderController
, the processOrder()
method is responsible for saving an order. It will need to be modified to determine who the authenticated user is and to call setUser()
on the TacoOrder
object to connect the order with the user.
We have several ways to determine who the user is. A few of the most common ways follow:
Inject a java.security.Principal
object into the controller method.
Inject an org.springframework.security.core.Authentication
object into the controller method.
Use org.springframework.security.core.context.SecurityContextHolder
to get at the security context.
Inject an @AuthenticationPrincipal
annotated method parameter. (@AuthenticationPrincipal
is from Spring Security’s org.springframework
.security.core.annotation
package.)
For example, you could modify processOrder()
to accept a java.security.Principal
as a parameter. You could then use the principal name to look up the user from a UserRepository
as follows:
@PostMapping public String processOrder(@Valid TacoOrder order, Errors errors, SessionStatus sessionStatus, Principal principal) { ... User user = userRepository.findByUsername( principal.getName()); order.setUser(user); ... }
This works fine, but it litters code that’s otherwise unrelated to security with security code. You can trim down some of the security-specific code by modifying processOrder()
to accept an Authentication
object as a parameter instead of a Principal
, as shown next:
@PostMapping public String processOrder(@Valid TacoOrder order, Errors errors, SessionStatus sessionStatus, Authentication authentication) { ... User user = (User) authentication.getPrincipal(); order.setUser(user); ... }
With the Authentication
in hand, you can call getPrincipal()
to get the principal object which, in this case, is a User
. Note that getPrincipal()
returns a java.util
.Object
, so you need to cast it to User
.
Perhaps the cleanest solution of all, however, is to simply accept a User
object in processOrder()
but annotate it with @AuthenticationPrincipal
so that it will be the authentication’s principal, as follows:
@PostMapping public String processOrder(@Valid TacoOrder order, Errors errors, SessionStatus sessionStatus, @AuthenticationPrincipal User user) { if (errors.hasErrors()) { return "orderForm"; } order.setUser(user); orderRepo.save(order); sessionStatus.setComplete(); return "redirect:/"; }
What’s nice about @AuthenticationPrincipal
is that it doesn’t require a cast (as with Authentication
), and it limits the security-specific code to the annotation itself. By the time you get the User
object in processOrder()
, it’s ready to be used and assigned to the TacoOrder
.
There’s one other way of identifying who the authenticated user is, although it’s a bit messy in the sense that it’s very heavy with security-specific code. You can obtain an Authentication
object from the security context and then request its principal like this:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); User user = (User) authentication.getPrincipal();
Although this snippet is thick with security-specific code, it has one advantage over the other approaches described: it can be used anywhere in the application, not just in a controller’s handler methods. This makes it suitable for use in lower levels of the code.
Spring Security autoconfiguration is a great way to get started with security, but most applications will need to explicitly configure security to meet their unique security requirements.
User details can be managed in user stores backed by relational databases, LDAP, or completely custom implementations.
Spring Security automatically protects against CSRF attacks.
Information about the authenticated user can be obtained via the SecurityContext
object (returned from SecurityContextHolder.getContext()
) or injected into controllers using @AuthenticationPrincipal
.