Skip to main content

Spring Boot RestTemplate with Basic Auth: A Modern Guide

Basic Auth-secured microservice calls rebuilt on RestClient, Spring Boot 4’s recommended HTTP client: two patterns ported from RestTemplate, the {noop} password myth corrected, and the RestClient.exchange() trap that silently swallows 401s.

Microservices talk to each other constantly, and a large share of those calls are still secured with nothing more elaborate than HTTP Basic Authentication — simple, widely supported, and good enough for a huge number of service-to-service links. This post builds a Basic Auth-secured endpoint and a client that calls it, the same way the original version of this article did — except the client is now RestClient, Spring Boot 4’s recommended synchronous HTTP client, because RestTemplate is no longer the client new code should reach for. Every pattern from the original RestTemplate version has a direct RestClient equivalent, and this post builds both of them, plus the one difference between the two clients that actually matters: what happens on a 401.
Versions used in this post, verified with mvn dependency:tree against a throwaway spring-boot-starter-parent:4.1.1 project.

Spring Boot 4.1.1 · Spring Framework 7.0.9 · Spring Security (managed by Boot 4.1.1) · JDK 25
All the code below lives in a real, tested companion module: restclient-basic-auth in the spring-boot-demo repository. mvn test starts a real embedded Tomcat with a real Spring Security filter chain on a random port and makes real HTTP calls against it — every transcript quoted is a genuine request/response pair, not a mocked one.

The problem: one service needs to prove who it is to another

Imagine an internal reporting service that needs to pull data from an inventory service before it can build its report. The inventory service should not answer just anyone — it needs to know the caller is the reporting service, or at least something holding valid credentials for it. Basic Auth solves this with almost no ceremony: a username and password, Base64-encoded, sent in an Authorization header on every request. It is not the strongest scheme available, but for a huge number of internal, TLS-protected service links, it is genuinely enough, and it is a reasonable place to start before reaching for OAuth2 client credentials or mutual TLS. Two starters cover both sides of this: spring-boot-starter-webmvc for the server, and, new in Boot 4, spring-boot-starter-restclient for the client.

The secured endpoint

Start with the thing being protected — a trivial endpoint that returns a greeting, and should refuse anyone who has not authenticated: Source: SecuredController.java
@RestController
public class SecuredController {

    @GetMapping("/api/hello")
    public String getSecuredGreeting() {
        return "Hello, you have accessed a secured endpoint!";
    }
}
With Spring Security on the classpath, every endpoint is secured by default, so this class needs nothing else — the rule that “authenticated only” applies here comes from the security configuration next.

Configuring Basic Authentication on the server

A SecurityFilterChain bean is the current, component-based way to configure this — the older WebSecurityConfigurerAdapter has been gone since Spring Security 5.7: Source: AppSecurityConfig.java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable())
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/**").authenticated()
            .anyRequest().permitAll())
        .httpBasic(Customizer.withDefaults());

    return http.build();
}

@Bean
public UserDetailsService userDetailsService() {
    UserDetails user = User.builder()
        .username("admin")
        .password(passwordEncoder().encode("password123"))
        .roles("USER", "ADMIN")
        .build();

    return new InMemoryUserDetailsManager(user);
}

@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
CSRF is disabled because this is a stateless REST API with no browser session to protect; /api/** requires authentication while everything else is left open; and httpBasic() turns on the Basic scheme itself. None of that changed from the original article — Spring Security’s core configuration surface is untouched by the Boot 4 upgrade.

What changed: the {noop} password, checked directly rather than assumed

The original article used User.builder().password("{noop}password123"), with a comment noting it was for demonstration purposes only. Before repeating that in a rewrite, it seemed worth checking what Boot 4.1 actually does with it now — log a deprecation warning, refuse to start, anything. It does not. A throwaway application built with exactly that {noop} password, run standalone, produces no warning anywhere in its full startup log, and a Basic Auth request that successfully authenticates against it produces no warning either. NoOpPasswordEncoder is @Deprecated in Spring Security’s own source and has been for years — but that annotation is a compile-time signal to whoever writes the code, not a runtime one. Nothing tells an operator watching production logs that a demo shortcut is still live.
“For demonstration purposes only” is a comment, not an enforcement mechanism. Checked by running a throwaway app with a {noop} password and reading the full startup and authentication log: nothing warns you at runtime, at any point. This module uses PasswordEncoderFactories.createDelegatingPasswordEncoder() instead — Spring Security’s own recommended default, currently BCrypt — so the stored value is not silently reversible plaintext even in a demo.

The client, rebuilt on RestClient

This is the section that actually changes from the original article. RestTemplate is still supported, but it is no longer the client Spring recommends for new code — RestClient is, and it needs its own starter: spring-boot-starter-restclient, alongside spring-boot-starter-webmvc. Both patterns from the original post carry over directly, always built from the auto-configured RestClient.Builder injected as a constructor parameter — never from RestClient.create(), which carries none of Boot’s message converters, observability, or customizer beans. The idiomatic form sets the credentials once, at bean-creation time: Source: RestClientConfig.java
@Bean
public RestClient restClientWithDefaultHeaders(RestClient.Builder builder) {
    return builder
            .defaultHeaders(headers -> headers.setBasicAuth("admin", "password123"))
            .build();
}
Output: 01-restclient-basic-auth-default-headers.txt
Response: Hello, you have accessed a secured endpoint!
That request against a real embedded Tomcat with a real Spring Security filter chain — nothing mocked.

The alternative: BasicAuthenticationInterceptor, ported unchanged

The original article’s second method — a hand-added ClientHttpRequestInterceptor, useful when credentials need to be resolved per request rather than fixed at startup — moves over with zero code changes: Source: RestClientConfig.java
@Bean
public RestClient restClientWithInterceptor(RestClient.Builder builder) {
    return builder
            .requestInterceptor(new BasicAuthenticationInterceptor("admin", "password123"))
            .build();
}
BasicAuthenticationInterceptor implements ClientHttpRequestInterceptor — the exact same interface both RestTemplate.getInterceptors() and RestClient.Builder.requestInterceptor(...) accept. Same class, same import, same result: Output: 02-restclient-basic-auth-interceptor.txt
Response: Hello, you have accessed a secured endpoint!
Prefer defaultHeaders when the credentials are fixed at bean-creation time; keep the interceptor form when they need to be resolved per request — a token fetched from a vault, say. An interceptor runs on every call, while a defaultHeaders value is captured once.

What the defaults do not do: RestClient’s own exchange() trap

Both examples above use retrieve(), and that choice is deliberate, not incidental. Confirmed against a request with no credentials at all and against one with the wrong password: Output: 03-restclient-no-credentials-401.txt
Thrown: org.springframework.web.client.HttpClientErrorException$Unauthorized
Status: 401 UNAUTHORIZED
retrieve() throws HttpClientErrorException on a 4xx or 5xx, exactly like RestTemplate always did. RestClient also has its own exchange() method, and it is not a drop-in rename target for RestTemplate.exchange(): RestClient’s exchange() disables that default status handling, so a 401 or 500 comes back to you as an ordinary response object instead of being thrown. A team that mechanically renames every restTemplate.exchange(...) call site to restClient.exchange(...) during a migration ships code that stops noticing failed requests.
Same method name, different default behaviour. If your code currently relies on RestTemplate.exchange() throwing on a bad status, move it to RestClient.retrieve(), not RestClient.exchange(), during a migration. The RestTemplate to RestClient migration guide covers this trap in full, alongside the complete method-mapping table and the other behavioural differences that matter.

One more thing the starter rename breaks silently if you skip it

On Boot 3, spring-boot-starter-web alone was enough to get an auto-configured RestTemplateBuilder bean for free. On Boot 4.1, HTTP client support moved into its own starter, confirmed the same way every version fact in this repository is confirmed — build a throwaway project and read the real dependency tree. spring-boot-starter-webmvc alone does not pull in spring-boot-restclient. Leave spring-boot-starter-restclient off the POM and the auto-configured RestClient.Builder every example above depends on simply is not there — a NoSuchBeanDefinitionException at startup, not a subtle behavioural difference: Source: pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-restclient</artifactId>
</dependency>

Should you still reach for Basic Auth here?

Basic Auth over TLS is a reasonable default for internal service-to-service calls where both ends are under your control and you are not trying to express fine-grained scopes or short-lived tokens. Reach for OAuth2 client credentials instead once you need per-caller scopes, token expiry, or a third party consuming the API without you managing its password. For a straightforward internal call like the one this post builds, RestClient plus Basic Auth remains about as little ceremony as authenticated HTTP gets.
The two client patterns from the original article carry over to RestClient with almost no changes — the real work in this rewrite was verifying what actually moved (the starter, the recommended client) against what only looks like it should have (the {noop} warning that was never there, the exchange() semantics that quietly differ from RestTemplate‘s).

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.