Here’s the English translation of the article body:
When you research how to implement authentication in Spring Boot, you’ll find plenty of options — session authentication, JWT, OAuth2, and API Keys — and it’s easy to get stuck deciding which one to choose.
The truth is, choosing an authentication method isn’t about which technology is superior — it’s almost entirely determined by the type of client and your revocation requirements. In this article, we’ll narrow down your choice to a single method using a flowchart and a comparison table, review the minimal configuration for each method, and then point you to detailed implementation articles for the next step.
Conclusion: A Flowchart for Choosing an Authentication Method
Let’s start with the conclusion. Try walking through the flowchart from the top with your own app in mind.
Q1. Googleなど外部IdPでログインさせたい?
├─ Yes → OAuth2ログイン(oauth2Login)
└─ No ↓
Q2. ユーザーが関与しないマシン間通信?(バッチ・Webhook等)
├─ Yes → API Key(本格運用ならOAuth2クライアントクレデンシャル)
└─ No ↓
Q3. クライアントはThymeleafなどのSSR画面?
├─ Yes → セッション認証
└─ No(SPA・モバイル + API)↓
Q4. 強制ログアウトなど即時失効が最重要?
├─ Yes → セッション認証 + Redis共有も検討
└─ No → JWT認証
If you’re unsure, starting with session authentication is recommended. It rides on Spring Security’s defaults with minimal implementation cost, and it’s never too late to migrate to JWT once you genuinely need statelessness.
Comparison Table of the Four Methods
Let’s back up the flowchart’s decision points with a table.
| Criteria | Session | JWT | OAuth2 | API Key |
|---|---|---|---|---|
| State management | Stateful | Stateless | IdP-dependent | Stateless |
| Immediate revocation | ◎ Easy | △ Weak | ○ Controlled by IdP | ○ Key deactivation |
| Scale-out | Requires session sharing | ◎ Easy | ◎ Easy | ◎ Easy |
| CSRF protection | Required | Often unnecessary | Required on login side | Not required |
| Implementation cost | Low | Medium | Medium–High | Low |
| Best fit | SSR pages | SPA / mobile APIs | External IdP integration | Machine-to-machine |
Keep two points in mind — JWT’s weakness at immediate revocation and session authentication’s mandatory CSRF protection — as they lead directly to the pitfalls discussed later.
Session Authentication: The First Choice for SSR Web Apps
For server-side rendered web apps using Thymeleaf or similar, session authentication is the first choice. It’s the classic approach: after login, a JSESSIONID is stored in a cookie and the session is managed on the server side.
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/css/**").permitAll()
.anyRequest().authenticated())
.formLogin(form -> form.loginPage("/login").permitAll());
return http.build();
}
That’s all it takes to get it working. Immediate revocation (forced logout) is as simple as deleting the server-side session, making it well suited to business systems where account deletion or permission removal must take effect instantly.
You may hear that “sessions don’t scale, so they’re outdated,” but that’s a misconception. Even in a multi-instance setup, you can solve this by sharing sessions with Spring Session + Redis.
For loading user information from a database, see the article on database authentication with UserDetailsService. If you want to try the authentication mechanism itself in a minimal setup, check out the Basic authentication article.
JWT Authentication: The Standard for SPA and Mobile APIs
If your setup involves an SPA (such as React) or a mobile app calling REST APIs, JWT is the standard choice. Since no state is kept on the server, it keeps working no matter how many instances you add.
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated())
// JWT検証フィルタを差し込む(実装は詳細記事へ)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
The internals of the filter (token generation and validation) are fully covered in the JWT authentication implementation article, so we’ll stick to the skeleton here.
The weakness to watch out for is immediate revocation. An issued JWT stays valid until it expires, so a setup where access tokens are short-lived (5–15 minutes) and supplemented with refresh token rotation is practically mandatory.
One more thing: adding JWT to an SSR app that lives entirely on a single domain just “because it’s modern” is an anti-pattern. You throw away the immediate revocation that sessions give you and only add risk — don’t do it.
OAuth2: Distinguish the Two Roles in External IdP Integration
OAuth2 is less “one method” and more two distinct roles, and confusing them leads to the wrong choice.
The first is the side that lets users log in (the client) — for example, when you want users to sign in with their Google account. The biggest benefit is delegating password management to an external party, and the setup requires nothing more than properties and oauth2Login().
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2Login(Customizer.withDefaults());
return http.build();
}
The full walkthrough is in the Google social login article.
The second is the side that validates tokens (the resource server) — the role where your API receives and validates JWTs issued by an IdP such as Auth0 or Keycloak.
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
Since you don’t issue JWTs yourself, the implementation stays quite thin. The decision rule is simple: if you already have an IdP, be a resource server; if you don’t, issue your own JWTs. For details, see the article on JWT validation with a resource server.
API Key Authentication: A Pragmatic Choice for Machine-to-Machine Communication
For communication with “no concept of a user” — internal batch jobs, incoming webhooks, server-to-server integration — settling for an API Key is perfectly reasonable. All it takes is plugging in a custom filter that validates a header.
public class ApiKeyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
if (!expectedKey.equals(req.getHeader("X-API-KEY"))) {
res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
chain.doFilter(req, res);
}
}
That said, the limitations are clear. Key rotation, per-consumer permission separation, and auditing are weak, so once the number of integration partners grows, consider migrating to the OAuth2 client credentials flow.
Also, do not use API Keys for end-user authentication. You get no per-user revocation management, no authentication audit trail, and no way to contain the blast radius when a key leaks.
Three Pitfalls That Trip People Up During Selection
1. CSRF configuration in stateless setups. CSRF protection is mandatory with session authentication, but in a setup where the JWT is sent in the Authorization header, no cookies are involved, so csrf.disable() is the standard move. However, the story changes if you put the JWT in a cookie. The decision criteria are laid out in the CSRF protection article.
2. Learning about JWT’s immediate revocation problem too late. If you choose JWT despite having account-deletion or forced-logout requirements, you’ll end up needing server-side state like a blacklist anyway, erasing the benefits of statelessness. Check your revocation requirements at the very start of the selection process.
3. Confusing authentication with authorization. What we’re choosing in this article is authentication — verifying “who someone is.” Role-based control over “what they can do” can be implemented uniformly with @PreAuthorize and similar mechanisms regardless of the method, so see the method security article.
Combined Patterns: When You Don’t Have to Pick Just One
In practice, combinations like “sessions for the admin panel, JWT for the public API” are completely normal. You can achieve this by defining multiple SecurityFilterChain beans with @Order and splitting paths with securityMatcher.
@Bean
@Order(1)
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
http.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
@Order(2)
SecurityFilterChain webChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}
The classic SPA setup — “authenticate via OAuth2 social login, then issue your own JWT for API access” — is an extension of this same pattern. When combining chains, designing the evaluation order and match conditions is the key part, so just remember that chains with a smaller @Order are evaluated first.
Summary: An Article Map to Read Once You’ve Decided
Choose your authentication method based on client type and revocation requirements — that’s the conclusion of this article. Once you’ve decided on a method, move on to implementation with the following article map.
- Session authentication: Basic authentication, database authentication, and Redis session sharing
- JWT authentication: JWT implementation and refresh tokens
- OAuth2: social login and resource server
- Common topics: CSRF protection and method security
If you’re still undecided, the practical path is to start with minimal session authentication and grow it incrementally. Even if you switch methods after your requirements solidify, the configuration skeleton is shared across Spring Security, so nothing goes to waste.
One note: per your rules I kept all code blocks unchanged, which includes the flowchart in the first text code block and the Japanese comment (// JWT検証フィルタを差し込む…) in the JWT example — both remain in Japanese. If you’d like those translated as well, let me know.