Add custom-validation, etag-caching, restclient-basic-auth: Boot 4.1 API pass
Three companion modules verifying and rewriting the Boot 4.1.1 / Framework 7.0.9 story for three older articles: the javax->jakarta.validation namespace fix plus Jakarta Validation 3.1 record-validation clarification, ETag/ conditional-request APIs re-verified unchanged plus the starter rename, and RestTemplate Basic Auth rebuilt on RestClient with the exchange() trap called out. 19 real passing tests generate every transcript quoted from the three companion articles. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01EQNA6DJ9VgCtW6zhCE8Xud
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# 1. {noop} passwords: still work, still deprecated, no runtime warning
|
||||
|
||||
[README](../README.md) | Next: [RestClient with Basic Auth, two ways](02-restclient-basic-auth-patterns.md)
|
||||
|
||||
Source: [`AppSecurityConfig.java`](../src/main/java/com/ankurm/restclientbasicauth/config/AppSecurityConfig.java).
|
||||
|
||||
## What was checked, and why
|
||||
|
||||
The original article used `User.builder().password("{noop}password123")` with the comment "for
|
||||
demonstration purposes only." Before repeating that pattern in a rewrite, it seemed worth checking
|
||||
whether Boot 4.1 actually does anything different 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 with
|
||||
`java -jar`, produces no warning in the full startup log, and a subsequent 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 logs in production that a demo shortcut is still live.
|
||||
|
||||
That is precisely the failure mode worth naming: a comment reading "for demonstration purposes
|
||||
only" is not enforced by anything at runtime. This module uses
|
||||
`PasswordEncoderFactories.createDelegatingPasswordEncoder()` (Spring Security's own recommended
|
||||
default, currently BCrypt) instead, so the encoded value in the user store is not silently
|
||||
reversible plaintext even in a demo.
|
||||
|
||||
## Going deeper
|
||||
|
||||
- [Spring Security password storage reference](https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html) (rel="nofollow")
|
||||
- Next: [RestClient with Basic Auth, two ways](02-restclient-basic-auth-patterns.md)
|
||||
@@ -0,0 +1,69 @@
|
||||
# 2. RestClient with Basic Auth, two ways
|
||||
|
||||
[Prev: {noop} passwords](01-password-encoding.md) | [README](../README.md) | Next: [The starter split, and the exchange() trap](03-starter-split-and-exchange-trap.md)
|
||||
|
||||
Source: [`RestClientConfig.java`](../src/main/java/com/ankurm/restclientbasicauth/client/RestClientConfig.java),
|
||||
[`ApiClient.java`](../src/main/java/com/ankurm/restclientbasicauth/client/ApiClient.java).
|
||||
Test: [`RestClientBasicAuthTest.java`](../src/test/java/com/ankurm/restclientbasicauth/RestClientBasicAuthTest.java).
|
||||
Transcripts: [`docs/output/01-restclient-basic-auth-default-headers.txt`](output/01-restclient-basic-auth-default-headers.txt),
|
||||
[`docs/output/02-restclient-basic-auth-interceptor.txt`](output/02-restclient-basic-auth-interceptor.txt).
|
||||
|
||||
The original article's two `RestTemplate` patterns -- `RestTemplateBuilder.basicAuthentication(...)`
|
||||
and a hand-added `BasicAuthenticationInterceptor` -- both have a direct `RestClient` equivalent.
|
||||
Both are built on the **auto-configured `RestClient.Builder`** injected as a constructor parameter,
|
||||
never `RestClient.create()`, which carries none of Boot's message converters, observability, or
|
||||
customizer beans -- the same rule the
|
||||
[RestTemplate to RestClient migration guide](https://ankurm.com/resttemplate-to-restclient-migration-guide/)
|
||||
covers in more general depth.
|
||||
|
||||
## Idiomatic on RestClient: `defaultHeaders` + `setBasicAuth`
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public RestClient restClientWithDefaultHeaders(RestClient.Builder builder) {
|
||||
return builder
|
||||
.defaultHeaders(headers -> headers.setBasicAuth("admin", "password123"))
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
Response: Hello, you have accessed a secured endpoint!
|
||||
```
|
||||
|
||||
([`docs/output/01-restclient-basic-auth-default-headers.txt`](output/01-restclient-basic-auth-default-headers.txt))
|
||||
-- a real HTTP call against a real embedded Tomcat with a real Spring Security filter chain, not a
|
||||
mock.
|
||||
|
||||
## Ported unchanged: `BasicAuthenticationInterceptor`
|
||||
|
||||
```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, so this class needs no changes at all to move from one client to the other. Same result:
|
||||
[`docs/output/02-restclient-basic-auth-interceptor.txt`](output/02-restclient-basic-auth-interceptor.txt).
|
||||
|
||||
Prefer `defaultHeaders` when the credentials are fixed at bean-creation time; keep the interceptor
|
||||
form when credentials must be resolved per request (a token fetched from a vault, say) --
|
||||
an interceptor runs on every call, a `defaultHeaders` value is captured once.
|
||||
|
||||
## The negative cases, checked too
|
||||
|
||||
`docs/output/03-restclient-no-credentials-401.txt` and `04-restclient-wrong-password-401.txt`
|
||||
confirm what actually happens on the failure path: `retrieve()` throws
|
||||
`HttpClientErrorException.Unauthorized` on a 401, exactly like `RestTemplate` did -- this is the
|
||||
default `retrieve()` behaviour, not the `exchange()` behaviour covered in the next chapter.
|
||||
|
||||
## Going deeper
|
||||
|
||||
- [`RestClient` Javadoc](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestClient.html) (rel="nofollow")
|
||||
- Prev: [{noop} passwords](01-password-encoding.md)
|
||||
- Next: [The starter split, and the exchange() trap](03-starter-split-and-exchange-trap.md)
|
||||
@@ -0,0 +1,40 @@
|
||||
# 3. The Boot 4 starter split, and the exchange() trap
|
||||
|
||||
[Prev: RestClient with Basic Auth, two ways](02-restclient-basic-auth-patterns.md) | [README](../README.md)
|
||||
|
||||
Source: [`pom.xml`](../pom.xml).
|
||||
|
||||
## `spring-boot-starter-restclient` is not optional in Boot 4
|
||||
|
||||
The original article's `spring-boot-starter-web` dependency was, on Boot 3, sufficient 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:
|
||||
|
||||
```
|
||||
$ mvn dependency:tree # against spring-boot-starter-parent:4.1.1 + spring-boot-starter-webmvc only
|
||||
```
|
||||
|
||||
`spring-boot-starter-webmvc` alone does **not** pull in `spring-boot-restclient`. Leave
|
||||
`spring-boot-starter-restclient` off this module's `pom.xml` and the auto-configured
|
||||
`RestClient.Builder` this chapter's code depends on is simply not there -- a `NoSuchBeanDefinitionException`
|
||||
at startup, not a subtle behavioural difference. This module declares it explicitly.
|
||||
|
||||
## The trap this module deliberately avoids
|
||||
|
||||
`RestClient` has its own `exchange()` method, and it means something different from
|
||||
`RestTemplate.exchange()`: **RestClient's `exchange()` disables the default status handlers**, so a
|
||||
4xx or 5xx response is silently returned to you instead of thrown. Every example in this module
|
||||
uses `retrieve()` for exactly that reason -- `retrieve()` keeps the throw-on-4xx/5xx behaviour this
|
||||
Basic Auth demo relies on (see [`docs/output/03-restclient-no-credentials-401.txt`](output/03-restclient-no-credentials-401.txt)).
|
||||
A team that mechanically renames `restTemplate.exchange(...)` call sites to
|
||||
`restClient.exchange(...)` during a migration ships code that stops noticing failed requests. The
|
||||
[RestTemplate to RestClient migration guide](https://ankurm.com/resttemplate-to-restclient-migration-guide/)
|
||||
covers this trap, the full method-mapping table, and the three behavioural differences that matter
|
||||
in more depth than a Basic Auth-focused rewrite has room for.
|
||||
|
||||
## Going deeper
|
||||
|
||||
- [RestTemplate to RestClient Migration Guide](https://ankurm.com/resttemplate-to-restclient-migration-guide/) -- the exchange() trap, timeouts, and the full mapping table
|
||||
- [Spring Boot 4 HTTP Service Clients (@HttpExchange)](https://ankurm.com/spring-boot-4-http-service-clients/) -- the declarative layer built on top of RestClient
|
||||
- Prev: [RestClient with Basic Auth, two ways](02-restclient-basic-auth-patterns.md)
|
||||
@@ -0,0 +1,5 @@
|
||||
// RestClient.Builder builder = ...;
|
||||
// RestClient client = builder.defaultHeaders(h -> h.setBasicAuth("admin", "password123")).build();
|
||||
// client.get().uri(baseUrl + "/api/hello").retrieve().body(String.class);
|
||||
|
||||
Response: Hello, you have accessed a secured endpoint!
|
||||
@@ -0,0 +1,6 @@
|
||||
// RestClient.Builder builder = ...;
|
||||
// RestClient client = builder.requestInterceptor(
|
||||
// new BasicAuthenticationInterceptor("admin", "password123")).build();
|
||||
// client.get().uri(baseUrl + "/api/hello").retrieve().body(String.class);
|
||||
|
||||
Response: Hello, you have accessed a secured endpoint!
|
||||
@@ -0,0 +1,8 @@
|
||||
// RestClient client = builder.build(); // no basic auth
|
||||
// client.get().uri(baseUrl + "/api/hello").retrieve().body(String.class);
|
||||
|
||||
Thrown: org.springframework.web.client.HttpClientErrorException$Unauthorized
|
||||
Status: 401 UNAUTHORIZED
|
||||
|
||||
# retrieve() throws HttpClientErrorException on 4xx by default -- same default as
|
||||
# RestTemplate, unlike RestClient's own exchange() method, which disables that default.
|
||||
@@ -0,0 +1,2 @@
|
||||
Thrown: org.springframework.web.client.HttpClientErrorException$Unauthorized
|
||||
Status: 401 UNAUTHORIZED
|
||||
Reference in New Issue
Block a user