Spring Security Basics
Executive Summary
Spring Security basics start with two questions. Authentication answers who is making this request, while authorization answers what that identity may do. Conflating the two is the most common security design mistake. Sessions keep identity on the server behind an opaque cookie id. Tokens, typically JWTs, carry identity in the request itself, so they trade server memory for self-contained verification.
Never store passwords in plaintext or hash them with a fast general-purpose digest like SHA-256 alone. After all, only a slow, salted algorithm such as BCrypt or PBKDF2 resists offline cracking once a database leaks. Similarly, OWASP’s recurring risk categories map directly onto Java constructs you already know. These include injection, broken authentication, and broken access control. For example, PreparedStatement defeats injection, and a repository’s query filtering defeats access control bypass.
Finally, Spring Security automates all of this behind a SecurityFilterChain bean. That one filter pipeline authenticates a request before it reaches a controller. However, deep coverage of that pipeline, JWT, OAuth2, and method-level rules belongs in the Spring Boot course on this site.
Authentication vs Authorization: Two Different Questions
Authentication proves identity: a username and password, a token signature, a client certificate. Authorization decides what that proven identity may do: read this resource, delete that record, call this endpoint. Yet a system can authenticate a user correctly and still fail if it skips the authorization check.
In plain Java terms, these are two questions against two separate pieces of data. So the code should keep them separate:
public record Principal(String userId, Set<String> roles) {}
public Principal authenticate(String username, char[] password) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new AuthenticationException("invalid credentials"));
if (!passwordHasher.matches(password, user.passwordHash())) {
throw new AuthenticationException("invalid credentials"); // who you are, rejected
}
return new Principal(user.id(), user.roles());
}
public void authorize(Principal principal, String requiredRole) {
if (!principal.roles().contains(requiredRole)) {
throw new AuthorizationException("forbidden"); // what you can do, rejected
}
}
Notice that authenticate never mentions roles, and authorize never mentions passwords. For example, I once saw a single checkAccess method that verified a password and checked a role in one branch. Later, a refactor dropped the role check while “simplifying” the password logic. Splitting the two questions into two methods, two exceptions, and two failure messages keeps that refactor safe.
The trade-off: separating the concerns costs one more method and one more exception type. Still, it pays for itself the first time someone adds a second authentication mechanism, such as an API key, without touching a single authorization rule.
Sessions vs Tokens: Where Identity Lives Between Requests
HTTP is stateless, so every request must re-prove identity somehow. Sessions and tokens are the two dominant answers, and they place the state in different places:
SESSION MODEL TOKEN MODEL
client server client server
|-- login ------> | |-- login ------> |
|<-- Set-Cookie --| (session id) |<-- JWT ---------| (signed, self-contained)
| | [session store] | | (no server-side store)
|-- Cookie: sid -->| lookup by id |-- Authorization:-->| verify signature
| | Bearer <jwt> | | and expiry locally
A session stores a small opaque id on the client. Meanwhile, the real identity data lives in server memory or a shared store like Redis. As a result, revocation takes effect instantly: delete the server-side entry and the cookie becomes worthless. A token, typically a JSON Web Token, carries the claims (user id, roles, expiry) signed by the server. So any node can verify it without a shared store. However, revoking a single token before its expiry needs an extra mechanism, such as a blocklist or a short lifetime.
In pure Java terms, a token is simply a string your HttpClient code from the REST article would attach as an Authorization header. Likewise, a session is just a cookie the same client would carry automatically. Nothing about either model requires a framework. Spring Security simply automates the attaching, verifying, and rejecting.
The trade-off is statelessness against revocability. Sessions scale less easily across stateless server fleets without a shared store, but they revoke instantly. Tokens scale horizontally with zero shared state, but they linger until expiry unless you build a blocklist. That blocklist quietly reintroduces the state you were avoiding.
Password Hashing: Never Store Plaintext, Never Roll Your Own
A password hash must be slow and salted, and a general-purpose digest alone fails both properties. Here is the mistake, then the fix:
// WRONG: fast, unsalted, cracked offline in minutes with a GPU
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] hash = sha256.digest(password.getBytes(StandardCharsets.UTF_8));
// RIGHT: slow, salted, tunable work factor (conceptual PBKDF2, no third-party library)
byte[] salt = SecureRandom.getInstanceStrong().generateSeed(16);
PBEKeySpec spec = new PBEKeySpec(password, salt, 100_000, 256); // 100k iterations
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] hash = factory.generateSecret(spec).getEncoded(); // store salt + hash together
SHA-256 aims to be fast, which is exactly the wrong property for a password hash. In other words, an attacker with a leaked hash table can try billions of guesses a second on cheap hardware. MessageDigest is the right tool for file integrity checks, not for passwords. In contrast, PBKDF2, BCrypt, and Argon2 add a deliberate work factor and a per-password salt. So cracking one password does not crack all of them, and each guess takes measurably longer.
In my experience tuning this trade-off, the iteration count is a live knob, not a one-time constant. For example, we once doubled PBKDF2’s iteration count during a routine security review. Login latency went from 40ms to 75ms on the same hardware, an acceptable cost for a harder offline attack. Spring Security’s BCryptPasswordEncoder exposes the same trade-off as a constructor argument, the “strength” parameter. This is exactly the kind of mechanism this course defers to the Spring Boot course rather than re-deriving.
OWASP Basics: The Risks Every Java Service Must Design Against
The OWASP Top Ten changes its exact ranking release to release. Still, a handful of categories recur, and each maps to a Java habit you likely already have from earlier in this course:
| OWASP risk category | What it means | The Java-side defense |
|---|---|---|
| Injection | Untrusted input changes a query’s structure, not just its data | PreparedStatement with bound parameters, never string-concatenated SQL (the JDBC article’s rule) |
| Broken authentication | Weak, guessable, or improperly verified credentials | Salted, slow password hashing and rate-limited login attempts |
| Broken access control | An authenticated user reaches data or actions outside their role | Authorization checks at the repository or service boundary, not only in the UI |
| Security misconfiguration | Defaults left open: verbose errors, permissive CORS, exposed admin endpoints | Explicit configuration reviewed in code, not left to framework defaults |
| Sensitive data exposure | Secrets or personal data logged, cached, or transmitted unencrypted | TLS for transport, and deliberate exclusion of secrets from logs (the logging article’s discipline) |
Injection is the easiest to see because the JDBC and CRUD articles already taught the fix without naming the threat. A PreparedStatement’s bound parameters never turn into SQL syntax, no matter what a user types into a form. Broken access control is the subtler one, because the bug is an absent check, not a present flaw. Absent code rarely shows up in a diff review unless someone is looking for it.
The trade-off across all five categories is the same. Every defense costs a small, constant amount of friction, such as one bound parameter, one authorization check, or one TLS handshake. In return, it prevents a failure that is expensive, public, and sometimes irreversible once customer data leaks.
Spring Security Basics: The Filter Chain
Spring Security’s job is to automate everything above. It intercepts every request, authenticates it, and authorizes it before it reaches your controller. The mechanism is a chain of servlet filters, configured through one bean that the container injects as the dependency injection article described:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll() // no authentication required
.anyRequest().authenticated()) // everything else needs a principal
.httpBasic(Customizer.withDefaults()) // authentication: Basic auth for this example
.csrf(csrf -> csrf.disable()); // disabled here only because this is a stateless API
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // the slow, salted hash from the section above
}
}
Read this against everything above it. The authorizeHttpRequests call is the authorization half, deciding which paths need a principal at all. Meanwhile, the httpBasic call wires one authentication mechanism. Swapping it for a JWT filter later changes only that one line, not the authorization rules. BCryptPasswordEncoder is the concrete, tuned implementation of the hashing concept from two sections ago. Its defaults also pick a sensible work factor.
In short, that single bean is the entire Spring Security surface this course covers. Deep coverage of JWT filters, OAuth2 flows, and method-level security annotations belongs in the Spring Boot course on this site. There, the filter chain above becomes a starting point rather than a destination.
How Real Systems Do This
Production Java services almost never hand-roll authentication filters today. The failure modes, such as timing attacks, token replay, and session fixation, are well studied. Yet they are easy to get subtly wrong under deadline pressure. Spring Security, and its equivalents in other ecosystems, exist precisely to centralize that hard-won knowledge into one audited pipeline.
What does not change, framework or not, is the separation this article opened with. For example, teams that keep authentication and authorization as two testable concerns can swap a session-based login for a token-based one without touching a single role check. In contrast, teams that fuse the two into one conditional discover, usually during an incident, that one fix breaks the other.
Decision Framework
- Is the question “who is this” or “what can they do”? Keep the two checks in separate code paths, regardless of framework.
- Do you need instant revocation? Favor sessions with a shared store; tokens need a blocklist or a short lifetime to match that guarantee.
- Does your client fleet need to scale statelessly across many nodes? Favor tokens, and accept the revocation trade-off deliberately.
- Is a password hash fast by design? Replace it; BCrypt, PBKDF2, or Argon2 are the minimum bar, never a bare digest.
- Is user input ever concatenated into a query string, a shell command, or an HTML response? Treat it as the injection risk it is, and bind, escape, or encode it.
- Are you about to write a custom authentication filter from scratch? Reach for Spring Security’s SecurityFilterChain instead, and defer the deep configuration to the Spring Boot course.
When NOT to Use This
- Do not reach for Spring Security’s full filter chain for a single internal script with one trusted caller. Instead, a shared secret header checked in one place is proportionate.
- Do not design a custom token format when a well-reviewed standard like JWT already fits. A homemade scheme is new surface area for the exact bugs OWASP tracks.
- Do not treat this article’s filter chain example as production configuration. It omits CSRF handling, token refresh, and logout semantics that a real deployment needs.
Common Mistakes
- Checking authorization only in the UI layer: a direct API call bypasses the button entirely. So the server must enforce the rule again.
- Hashing passwords with a fast digest like plain SHA-256 or MD5: a leaked database becomes a cracked database within hours.
- Storing JWTs in a way that defeats the point, like a server-side session that duplicates the token’s claims.
- Logging full request bodies that include passwords or tokens, undoing the hashing and transport protections built everywhere else.
- Disabling CSRF protection by habit instead of by reasoned exception. That choice then travels from a stateless API into a cookie-based session app where it no longer applies.
- Assuming an authenticated request is automatically an authorized one, and skipping the second check entirely.
Key Takeaways
- Authentication proves identity; authorization decides permission; keep the two as separate methods, separate exceptions, and separate tests.
- Sessions keep identity on the server for instant revocation. Tokens carry identity in the request for stateless scaling, at the cost of harder revocation.
- Password hashing must be slow and salted; BCrypt, PBKDF2, or Argon2, never a bare fast digest like SHA-256 alone.
- OWASP’s recurring risk categories map to habits you already have, such as bound parameters against injection, explicit authorization checks against broken access control.
- Spring Security basics come down to one SecurityFilterChain pipeline for authentication and authorization. The concepts behind it are plain HTTP, not framework magic.
- Deep coverage of Spring Security, including JWT filters, OAuth2, and method security, belongs in the Spring Boot course on this site.
FAQ
What is the difference between authentication and authorization?
Authentication verifies who is making a request, typically with a password or token. Authorization then decides what that verified identity may do. A system needs both checks, and skipping either one creates a distinct class of bug.
Should I use sessions or tokens for a Java REST API?
Use sessions when instant revocation matters more than stateless scaling. Use tokens when many stateless nodes must verify identity without a shared store. Most REST APIs serving external clients lean toward tokens; internal server-rendered apps often lean toward sessions.
Why is SHA-256 not safe for password storage?
SHA-256 is fast by design. That lets an attacker with a leaked hash try billions of guesses per second on commodity hardware. Password hashing needs a deliberately slow, salted algorithm such as BCrypt, PBKDF2, or Argon2 instead.
What does Spring Security’s SecurityFilterChain actually do?
It defines a pipeline of servlet filters that runs before your controller code. The filters check which paths require authentication, verify credentials or tokens, and reject failing requests. One bean configures the whole pipeline for a Spring application.
Where can I learn Spring Security in depth?
This course deliberately stops at the filter chain, since Part 9 introduces Spring rather than the framework itself. Deep coverage of Spring Security, JWT, OAuth2, and method-level security belongs in the Spring Boot course on this site.
Conclusion
You now have the Spring Security basics every Java backend conversation relies on. They cover authentication against authorization, sessions against tokens, and hashing done right. They also cover the OWASP categories that turn abstract risk into concrete code habits. The one Spring Security example above is a preview, not a destination, and that is by design.
So carry the concepts forward even where the framework changes. If you cannot state who a request claims to be and what it may do, your security model is not finished yet.
Last updated on 4 September 2026.
