Spring Security: How to get started
Learn to implement Spring Security for robust web app protection, from basic setup to advanced features, in this detailed guide.
Feb 9, 2024 • 12 Minute Read
In this article, you’ll learn about Spring Security, a Java security framework for authentication, authorization, and web application defense. We’ll start with a basic Spring Boot application and build in security capabilities step by step. Along the way, you’ll learn what complexities this takes off your plate so you can proceed knowing that Spring Security has your back.
Table of contents
Setting up Spring Security: First steps
To get started with any Spring module, I recommend using Spring Initializr at start.spring.io. Going there, you can add the modules you need to get going. To begin, I’m going to add only Spring Web so that I can first show you a bit of what life is like without Spring Security.
Here are the settings I chose to make my application:
And then, I added the following @RestController:
@SpringBootApplication
public class SpringSecurityStartApplication {
@RestController
public static class OkController {
@GetMapping
public String ok() {
return "ok";
}
}
public static void main(String[] args) {
SpringApplication.run(SpringSecurityStartApplication.class, args);
}
}
And that’s it! I can start the application like so:
./gradlew bootRun
.... and start poking around.
How Spring Security secures your defaults
One of the main important features that Spring Security gives you is secure defaults. That is, Spring Security picks by default the most secure thing it knows, considering your use case. The use case I’m going to use today is a REST API, as you can see by my use of the @RestController annotation from Spring Web up above.
If I try requesting the application’s root like so:
http :8080
Then, I’ll get a response similar to:
HTTP/1.1 200
Connection: keep-alive
Content-Length: 2
Content-Type: text/plain;charset=UTF-8
Date: Mon, 27 Nov 2023 21:22:51 GMT
Keep-Alive: timeout=60
ok
At the risk of stating the obvious, notice that the endpoint doesn’t require any proof of my identity (authentication) or proof of my authority (authorization). Because of that, this endpoint can’t easily adapt its behavior to or secure its information from different types of users.
Less obviously, calling this from a browser or another REST API is dubious at best. Without further defense, this application’s endpoints may be vulnerable to cross-site request forgery (CSRF), cross-site scripting (XSS), man-in-the-middle attacks (MITM), sensitive data exposure, and on and on.
Adding Spring Security to your application
So now I’ll add the Spring Security module to the application by changing the dependency file like so:
implementation ‘org.springframework.boot:spring-boot-starter-security’ // alphabetical order
implementation ‘org.springframework.boot:spring-boot-starter-web’
If I restart the application, this will add the Spring Security module and all of its secure defaults.
Now, if I make the same request:
http :8080
I get a different response:
HTTP/1.1 401
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: keep-alive
Content-Length: 0
Date: Mon, 27 Nov 2023 23:04:05 GMT
Expires: 0
Keep-Alive: timeout=60
Pragma: no-cache
Set-Cookie: JSESSIONID=4E5A935F1B30EBD82AE96FADD26AD23E; Path=/; HttpOnly
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
WWW-Authenticate: Basic realm="Realm"
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
That’s a lot to take in! What I hope you take away from it is a realization that security is more than just having a way to log the user in. It’s about ensuring that your application cannot be misused.
More concretely, there are three main differences:
The first is that the same endpoint now denies the request and returns a 401.
The second is that several more headers are supplied, each fine-tuned according to security best practice.
The third is that one particular header WWW-Authenticate tells us that the application is now configured to authenticate users using the HTTP Basic authentication scheme.
Notice that even if I request a non-existent endpoint like so:
http :8080/made-up-endpoint
... then Spring Security will also protect that endpoint with a 401 and the same set of headers.
I invite you to take a moment to realize the enormous leverage that Spring Security just gave our application.
With only the addition of the Spring Security module, it accepts a standards-based authentication scheme, it authorizes every request -- even ones you didn’t consider -- and it defends against the most common web application vulnerabilities.
There’s even more going on behind the scenes that isn’t apparent from looking at the response. Spring Security deploys a web application firewall, protects against timing attacks during the authentication process, securely encodes passwords and other secret information, and is compatible with the rest of the Spring ecosystem.
How the architecture works
One of the best ways to understand the architecture is to turn on TRACE logging in the application. I can do this by editing the application.properties file like so:
logging.level.org.springframework.security=TRACE
Then, if I make the same request as before:
http :8080
I’ll see much more information in the logs:
2023-11-27T17:34:33.169-07:00 DEBUG 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Securing GET /
2023-11-27T17:34:33.170-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/16)
2023-11-27T17:34:33.171-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/16)
2023-11-27T17:34:33.173-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/16)
2023-11-27T17:34:33.175-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/16)
2023-11-27T17:34:33.177-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking CorsFilter (5/16)
2023-11-27T17:34:33.194-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (6/16)
2023-11-27T17:34:33.196-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.csrf.CsrfFilter : Did not protect against CSRF since request did not match CsrfNotRequired [TRACE, HEAD, GET, OPTIONS]
2023-11-27T17:34:33.196-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking LogoutFilter (7/16)
2023-11-27T17:34:33.196-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.s.w.a.logout.LogoutFilter : Did not match request to Ant [pattern='/logout', POST]
2023-11-27T17:34:33.197-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking UsernamePasswordAuthenticationFilter (8/16)
2023-11-27T17:34:33.197-07:00 TRACE 293546 --- [nio-8080-exec-1] w.a.UsernamePasswordAuthenticationFilter : Did not match request to Ant [pattern='/login', POST]
2023-11-27T17:34:33.197-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking DefaultLoginPageGeneratingFilter (9/16)
2023-11-27T17:34:33.197-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking DefaultLogoutPageGeneratingFilter (10/16)
2023-11-27T17:34:33.197-07:00 TRACE 293546 --- [nio-8080-exec-1] .w.a.u.DefaultLogoutPageGeneratingFilter : Did not render default logout page since request did not match [Ant [pattern='/logout', GET]]
2023-11-27T17:34:33.198-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking BasicAuthenticationFilter (11/16)
2023-11-27T17:34:33.198-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.s.w.a.www.BasicAuthenticationFilter : Did not process authentication request since failed to find username and password in Basic Authorization header
2023-11-27T17:34:33.198-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking RequestCacheAwareFilter (12/16)
2023-11-27T17:34:33.198-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.s.w.s.HttpSessionRequestCache : matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided
2023-11-27T17:34:33.199-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderAwareRequestFilter (13/16)
2023-11-27T17:34:33.200-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking AnonymousAuthenticationFilter (14/16)
2023-11-27T17:34:33.201-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking ExceptionTranslationFilter (15/16)
2023-11-27T17:34:33.201-07:00 TRACE 293546 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking AuthorizationFilter (16/16)
What you can see from the logs is that Spring Security is fundamentally a set of filters that intercept each request. Each filter either performs authentication, authorization, or defense or performs some infrastructural role.
For example, you can see in the list of filters an example of each type:
BasicAuthenticationFilter which authenticates the HTTP Basic scheme,
AuthorizationFilter which authorizes the request,
HeaderWriterFilter which writes secure headers like the cache control headers you saw earlier, and
ExceptionTranslationFilter which captures Spring Security exceptions and translates them into appropriate HTTP responses
Spring Security has a set of web filters that intercept each HTTP request by default. It also has other filters, like for intercepting method invocations, websocket messages, and RSocket requests that require your configuration.
Whenever Spring Security is doing anything, it came originally from one of these Spring Security filters. The result of an authentication filter that succeeds is an instance of Authentication, which usually has the user’s identifying characteristics as well as the permissions Spring Security granted to that user.
How to configure authentication for your app
In spite of all these helpful secure defaults, the main goal for most applications that use Spring Security is to get users logged in.
As I already mentioned, Spring Security switches on HTTP Basic authentication by default. The default user is user and there is no default password. That’s right, the password is generated on startup to ensure that the app can’t accidentally be deployed with a default password; another Spring Security secure default.
You can change the password by setting it in the application.properties file like so:
spring.security.user.password=password
And then you can hit the endpoint:
http -a user:password :8080
And get the 200 response from before:
HTTP/1.1 200
Connection: keep-alive
Content-Length: 2
Content-Type: text/plain;charset=UTF-8
Date: Mon, 27 Nov 2023 21:51:51 GMT
Keep-Alive: timeout=60
ok
So, that shows that authentication works, but it doesn’t really show you how you’d use it in practice. Now, let’s connect it to something a little bit closer to reality.
Spring Security uses dependency injection like the rest of the Spring Framework as its primary configuration guiding principle. As such, if you publish a UserDetailsService bean, Spring Security will pick up that bean and inject it into the places it needs in order to have a different set of users from the default one.
For example, if you change your UserDetailsService out for a custom implementation like this one:
@Component
public class MyUsers implements UserDetailsService {
private final Map<String, User> users = new HashMap<>();
public MyUsers() {
this.users.put(“candice”, User.withUsername(“candice”).password(“{noop}password!”).authorities(“user”).build());
this.users.put(“tobias”, User.withUsername(“tobias”).password(“{noop}password!”).authorities(“user”).build());
this.users.put(“zee”, User.withUsername(“zee”).password(“{noop}password!”).authorities(“admin”, “user”).build());
}
@Override
public UserDetails loadUserByUsername(String username) {
if (this.users.containsKey(username)) {
return this.users.get(username);
}
throw new UserNotFoundException(“user not found”);
}
}
... then Spring Security will use your bean instead with your user store. You can see here that this class knows about three users, each with a different username, password, and set of authorities (permissions).
If you wanted, you could instead connect this with Spring Data and have it pull from your user database.
The key is, notice that this isn’t doing any of the password checking for you. All you are doing is providing the set of users and letting Spring Security do all the password encoding, protection against timing attacks, and the rest.
In case you don’t want to use HTTP Basic, know that Spring Security also supports X.509, JWT, Form, CAS, OAuth/OIDC, SAML, and other authentication mechanisms that can be similarly configured.
Authentication proves who the user is. Authorization proves that they have permission to execute the request. And, as already mentioned earlier, Spring Security authorizes every request by default. The default authorization rule is that the user be authenticated. In other words, so long as the user is logged in, they can view any endpoint.
This is a good secure default, but probably not what you want. Certainly there are pages that anyone can see, even if they aren’t logged in, and there are pages that only certain people can see, like admins.
Let’s say, for example, that you have stylesheets and javascript in `/css` and `/js`, respectively. In all likelihood, those need to be available even when the user isn’t logged in. And let’s say that everything under the `/admin` directory is for administrators.
You can describe all of this in Spring Security by publishing authorization rules for each web request like so:
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(“/css/**”, “/js/**”).permitAll()
.requestMatchers(“/admin/**”).hasAuthority(“admin”)
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
Here you can see that it all gets put together. When you define your authorization rules, you do so by publishing Spring Security’s set of HTTP filters. Several default filters are still configured for you, but you need to configure the AuthorizationFilter (authorizeHttpRequests) and any authentication filter you want to use (httpBasic). This declaration overrides any default authentication or authorization that Spring Security defined by default.
Now, if you try to hit an /admin endpoint with candice you get a 403:
http -a candice:password! :8080/admin
HTTP/1.1 403
But, if you use zee who has admin privileges (see the UserDetailsService listing for why that is), then it works:
http -a zee:password! :8080/admin
HTTP/1.1 404
Note that authorization can get very tricky as each company tends to roll their own domain modeling for it. As such, Spring Security gives you a flexible API to map your model onto. Sometimes this can be enough rope to tie yourself into a knot, so proceed carefully and be willing to adjust your model as time goes on.
How to configure web application defense
Finally, web application defense. Spring Security provides an application firewall, CORS, secure response headers, session fixation, and CSRF support, each with its own security filter.
Most of these are configured automatically and require no further work on your part. You can disable them if you don’t want them but they usually don’t get in the way.
There are two exceptions since they both are specs that require some agreement between the client application and the REST API: CORS and CSRF.
CORS is how you can specify what kinds of headers, methods, and origin values are acceptable in XHR requests. This is important, for example, when your front-end is deployed to a different host than your backend.
For example, you can say that your REST API will accept XHR requests from your Angular front-end, deployed to http://localhost:4200 like so:
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(List.of("*"));
corsConfiguration.setAllowedMethods(List.of("GET”, “POST"));
corsConfiguration.setAllowedOrigins(List.of("http://localhost:4200"));
return (request) -> corsConfiguration;
}
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(“/css/**”, “/js/**”).permitAll()
.requestMatchers(“/admin/**”).hasAuthority(“admin”)
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.cors(Customizer.withDefaults());
return http.build();
}
CSRF is a complicated web exploit that Spring Security deploys simple defenses to mitigate. Basically, Spring Security randomly generates a CSRF token once a user is authenticated and stores it by default in the session.
It’s up to the front end to read the CSRF token from Spring Security and return it on each request. Such is out of scope of this article, but know that Spring Security supports reading it from the session, from a cookie, or from a header.
Conclusion
Spring Security is a powerful security framework that supports several forms of authentication, provides a flexible API for authorization, and automatically defends against numerous web exploits. You can get started by using Spring Initializr and including the Spring Security module. Instantly, you’ll get reasonable secure defaults that protect your application from being misused. Additionally, you’ll get curated and battle-tested APIs that allow you to customize any part of Spring Security to suit your needs.
Other learning resources
Want to learn more about using the Spring Framework, Spring Security, and more? Pluralsight offers a wide range of courses that can help you become a master of all things Spring: