Skip to content

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

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

Please set an alt value for this image...
  • Software Development
  • Security
  • java

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.

How to configure authorization for your app

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:

Josh Cummings

Josh C.

Like many software craftsmen, Josh eats, sleeps, and dreams in code. He codes for fun, and his kids code for fun! Right now, Josh works as a full-time committer on Spring Security and loves every minute. Application Security holds a special place in his heart, a place diametrically opposed to and cosmically distant from his unending hatred for checked exceptions.

More about this author