How to Separate External and Internal Authentication in Spring Cloud Microservices
This article explains why authentication loops occur when external user tokens and internal service tokens are handled together in Spring Security, and provides a step‑by‑step solution using distinct headers, custom filters, and Feign interceptors to break the cycle.
Introduction
In a microservice architecture, external requests and internal service calls usually follow different authentication logic.
External user access must validate a user_token, while service‑to‑service calls only need a client_token to indicate an internal request.
Problem: When a user sends an access_token to Service A, Spring Security’s filter triggers a Feign call to Service B with the same token, which is intercepted again, causing an authentication loop.
Requirement Analysis
1. External request
Must carry user_token When passing through Service A, Spring Security must run user authentication logic
2. Service‑to‑service call
Feign call must include client_token Service B only validates client_token, no user authentication needed
3. Problem point
When Service A’s security filter uses Feign to verify access_token, the request is intercepted again, leading to infinite recursion.
Common Pitfalls
Not distinguishing token types
User token and client token are mixed, causing all requests to follow the same interception logic.
Feign applies global interceptor
Spring Security’s OncePerRequestFilter applies to all requests, including Feign internal calls.
Risk of dead loop
Service A calls B for token verification, B intercepts as a user request, calls A again, forming a loop.
Solution Idea
1. Distinguish two request types
User request: header Authorization: Bearer user_token Internal call: header
X-Client-Token: client_token2. Spring Security double authentication chain
User requests go through JWT validation
Internal calls only validate client_token, bypassing user authentication
3. Add client_token to Feign requests
Use RequestInterceptor to automatically add
X-Client-Token4. Custom authentication filters
Two filters are needed: one for user token, one for client token.
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
@Bean
public RequestInterceptor clientTokenInterceptor() {
return (RequestTemplate template) -> {
// Simulate obtaining client_token from config or vault
template.header("X-Client-Token", "my-client-token-123");
};
}
} import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class ClientTokenFilter extends OncePerRequestFilter {
private final String CLIENT_TOKEN = "my-client-token-123";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String clientToken = request.getHeader("X-Client-Token");
if (clientToken != null && clientToken.equals(CLIENT_TOKEN)) {
// Authenticate as internal service
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken("internal-service", null, null);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
} import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable());
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
);
// client token filter has higher priority than user token filter
http.addFilterBefore(new ClientTokenFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(new UserTokenFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
} import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class UserTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String userToken = authHeader.substring(7);
// TODO: call Service B to verify user_token, then set user context
// JWT verification logic omitted for brevity
}
filterChain.doFilter(request, response);
}
}The logic is simple: if the request contains a valid X-Client-Token, it is authenticated as an internal service; otherwise the request proceeds to the usual user authentication flow.
Practical Scenario
User "Xiao Ming" logs in via the app and sends a request to /api/orders with user_token.
Service A receives the request and needs to call Service B to verify the token.
The Feign call automatically adds X-Client-Token, so Service B treats it as an internal call and skips user authentication.
After Service B validates successfully, Service A continues its business logic.
Conclusion
Add client_token to all Feign calls via a request interceptor.
Insert a ClientTokenFilter before the user token filter in the Spring Security chain.
Keep user requests flowing through JWT or access_token validation.
Proper filter ordering prevents Feign calls from triggering user authentication, fully solving the dead‑loop issue.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
