Common Backend Vulnerabilities
Authentication & Authorization
Spring Security JWT authentication
From its name, Spring Security is the security framework for Spring applications. It is a separate library that hooks into your Spring Boot app and takes over the Authentication & Authorization job you'd otherwise have to build by hand on every endpoint, and provides a highly configurable, declarative approach built right into the Spring ecosystem.
Without a framework, you would write the same checks over and over: read a header, verify a token, look up the user, check their role, return 401 or 403β¦ in every single controller. That is repetitive and easy to get wrong, and security bugs are the most expensive kind.
Spring Security moves all of that to one central place. Requests are checked before they ever reach your controller, so your controller code can assume "if I'm running, the caller is already allowed to be here." It also ships with safe defaults for many of the attacks you read about earlier (it sets security headers, protects against CSRF for browser sessions, integrates BCrypt, and more).
<aside> π
Mental model: Spring Security is a security guard standing at the door of your application. Every request has to show ID and prove it's allowed in before it reaches the room (your controller) it's trying to enter.
</aside>
spring-boot-starter-security dependencyMaven (pom.xml):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Spring Security works as a chain of servlet filters that intercept every request before it reaches your controllers. This makes it easy to enforce security rules consistently across the entire application without scatteringΒ ifΒ checks throughout your business logic.
To understand Spring Security and how requests are intercepted, you need one new concept: the filter.
In a Java web application, before a request reaches your controller, it passes through a chain of filters. A filter is just a piece of code that can look at the request, do something with it, and then either pass it along to the next filter or stop it. Think of filters as a series of checkpoints the request walks through in order.
Spring Security works by inserting its own filters into that chain. This collection of security filters is called the security filter chain.

Each filter has a single responsibility. Some examples of what filters in the chain do:
If any checkpoint decides the request shouldn't continue, the chain stops the request right there and returns an error response (401 Unauthorized or 403 Forbidden) β your controller is never even called.
Two new terms you'll see:
Authenticationβ an object Spring Security creates to represent the current user: their username, their roles/authorities, and whether they're authenticated.SecurityContextHolderβ the place Spring Security stores thatAuthenticationobject for the duration of a request. Later, anywhere in your code, you can ask "who is the current user?" by reading from it.
You don't write these filters yourself β Spring Security provides them. Your job is to tell the chain what your rules are, which is exactly what the next sections cover.
By default, Spring Security protects every endpoint. Just adding the dependency changes your app's behaviour. Nothing is public until you say so. This is a deliberate, safe default: it's much better to accidentally lock something that should be open (you'll notice immediately) than to accidentally leave something open that should be locked (an attacker notices instead of you).
Try to call any endpoint, and you will get: β 401 Unauthorized
Spring Boot also does two more things by default to give you a way in:
user.Using generated security password: 1f8e2c5a-6b3d-44a7-9f0e-2d1c3b4a5e6f
So out of the box, you can log in as user with that printed password. This is only meant for getting started β it is not a real login system. In a real app you replace all of this with your own configuration, which is the rest of this chapter.
<aside> π‘
With Spring Security, security is opt-out, not opt-in. You start fully locked down and then open up exactly the doors you want.
</aside>
SecurityFilterChain beanTo replace the defaults with your own rules, you define a SecurityFilterChain bean inside a configuration class. This is the single most important piece of Spring Security to understand β it's where you describe which endpoints are public, which require login, and which require specific roles.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll() // login & register: public
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll() // anyone can browse
.anyRequest().authenticated() // everything else: must be logged in
);
return http.build();
}
}
Let's read this the way Spring Security reads it β top to bottom, first match wins:
requestMatchers("/auth/**").permitAll() β any URL starting with /auth/ is open to everyone (this is where login and registration live).requestMatchers(HttpMethod.GET, "/api/products/**").permitAll() β GET requests to products are public, but other methods (POST, DELETEβ¦) are not, so they fall through to the next rule..anyRequest().authenticated() β every request that didn't match a rule above requires an authenticated user.A few things worth knowing about this configuration:
HttpSecurity http is a builder. You configure it using the lambda style (auth -> auth...). This is the modern, required way to write Spring Security configuration β older tutorials using WebSecurityConfigurerAdapter or .and() chaining are for Spring Security 5 and earlier and no longer work in current versions.anyRequest() last.permitAll() opens an endpoint, authenticated() requires login, and (next section) hasRole(...) requires a specific role.Because we're building a stateless JWT API (not a browser app with sessions), we'll add two more settings to this chain in section 8: turning off CSRF and turning off server-side sessions. We'll explain why there, since it depends on JWT.
Authentication tells you who the user is. Authorization by role tells you what they may do. In Spring Security, a user carries a set of authorities, and a role is just an authority with a conventional ROLE_ prefix.
You restrict endpoints to roles right inside the same SecurityFilterChain:
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN") // only admins
.requestMatchers("/api/orders/**").hasAnyRole("USER", "ADMIN") // users or admins
.anyRequest().authenticated()
);
hasRole("ADMIN") β caller must have the ADMIN role.hasAnyRole("USER", "ADMIN") β caller must have at least one of these roles.hasAuthority("ORDER_DELETE") β when you want fine-grained permissions instead of broad roles.The one gotcha everybody hits: the
ROLE_prefix. When you writehasRole("ADMIN"), Spring Security automatically looks for the authorityROLE_ADMIN. So:
- With
hasRole("ADMIN")β do not write the prefix.- The user's stored authority must be
ROLE_ADMIN(with the prefix).hasAuthority("...")does no automatic prefixing β it matches the string exactly.If you build users with
.roles("ADMIN")(shown in the next section), Spring adds theROLE_prefix for you, so it lines up correctly withhasRole("ADMIN"). Mixing these up is the number-one reason "I'm logged in but I keep getting 403."
401 vs 403 β know the difference:
401 Unauthorized β You're not authenticated at all (no token, bad token). "I don't know who you are."403 Forbidden β You're authenticated, but you lack the required role. "I know who you are, and you're not allowed."@PreAuthorize and @SecuredURL rules in the filter chain are great for broad strokes, but sometimes the rule belongs right next to the code it protects β for example, on a service method that several controllers might call. For that, Spring Security offers method-level security.
First, switch it on with @EnableMethodSecurity (add this on a @Configuration class β your SecurityConfig is a good home):
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // enables @PreAuthorize / @PostAuthorize
// @EnableMethodSecurity(securedEnabled = true) // also enables @Secured
public class SecurityConfig { /* ... */ }
Now you can annotate methods.
@PreAuthorize runs before the method and accepts a SpEL expression (Spring Expression Language), which makes it very flexible:
@Service
public class ProductService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteProduct(Long id) {
// only admins ever get here
}
// A user may only read their OWN profile.
// `authentication.name` is the logged-in username; `#username` is the method argument.
@PreAuthorize("#username == authentication.name or hasRole('ADMIN')")
public Profile getProfile(String username) {
// ...
}
}
@Secured is the older, simpler annotation β it only checks roles and has no SpEL. Note you must write the full ROLE_ prefix here:
@Secured("ROLE_ADMIN")
public void promoteUser(Long id) {
// ...
}
Which to use? Prefer @PreAuthorize β it does everything @Secured does and a lot more (combine roles, compare against method arguments, call your own beans). @Secured mainly shows up in older codebases. When a method-level check fails, Spring throws an AccessDeniedException, which surfaces as a 403.
Filter-chain rules vs method annotations β when to use which? Use URL rules in the
SecurityFilterChainfor coarse, route-based protection (/api/admin/**is admin-only). Use method security when the rule depends on the data β "only the owner of this resource," "only an admin can delete." Many apps use both.