Prevent basic authentication from popping up (#4556)

#### What type of PR is this?

/kind improvement
/kind api-change
/area core
/milestone 2.10.x

#### What this PR does / why we need it:

See https://github.com/halo-dev/halo/issues/4547 for more.

This PR creates header `WWW-Authenticate` like `FormLogin realm="console"` instead of `Basic realm="realm"` while unauthorized.

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/4547

#### Special notes for your reviewer:

```bash
curl --head 'http://localhost:8090/actuator/info'
HTTP/1.1 401 Unauthorized
transfer-encoding: chunked
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
WWW-Authenticate: FormLogin realm="console"
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Referrer-Policy: no-referrer
```


#### Does this PR introduce a user-facing change?

```release-note
防止浏览器弹出基础认证弹窗
```
pull/4551/head
John Niang 2023-09-07 16:52:10 +08:00 committed by GitHub
parent f3cf3ca283
commit 0098654344
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 70 additions and 1 deletions

View File

@ -29,6 +29,7 @@ import run.halo.app.core.extension.service.UserService;
import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
import run.halo.app.security.DefaultServerAuthenticationEntryPoint;
import run.halo.app.security.DefaultUserDetailService;
import run.halo.app.security.DynamicMatcherSecurityWebFilterChain;
import run.halo.app.security.authentication.SecurityConfigurer;
@ -66,7 +67,9 @@ public class WebServerSecurityConfig {
spec.principal(AnonymousUserConst.PRINCIPAL);
})
.securityContextRepository(securityContextRepository)
.httpBasic(withDefaults());
.httpBasic(withDefaults())
.exceptionHandling(
spec -> spec.authenticationEntryPoint(new DefaultServerAuthenticationEntryPoint()));
// Integrate with other configurers separately
securityConfigurers.orderedStream()

View File

@ -0,0 +1,31 @@
package run.halo.app.security;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* Default authentication entry point.
* See <a href="https://datatracker.ietf.org/doc/html/rfc7235#section-4.1">
* https://datatracker.ietf.org/doc/html/rfc7235#section-4.1</a>
* for more.
*
* @author johnniang
*/
public class DefaultServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {
return Mono.defer(() -> {
var response = exchange.getResponse();
var wwwAuthenticate = "FormLogin realm=\"console\"";
response.getHeaders().set(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
});
}
}

View File

@ -0,0 +1,35 @@
package run.halo.app.security;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.http.HttpHeaders.WWW_AUTHENTICATE;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import reactor.test.StepVerifier;
@ExtendWith(MockitoExtension.class)
class DefaultServerAuthenticationEntryPointTest {
@InjectMocks
DefaultServerAuthenticationEntryPoint entryPoint;
@Test
void commence() {
var mockReq = MockServerHttpRequest.get("/protected")
.build();
var mockExchange = MockServerWebExchange.builder(mockReq)
.build();
var commenceMono = entryPoint.commence(mockExchange,
new AuthenticationCredentialsNotFoundException("Not Found"));
StepVerifier.create(commenceMono)
.verifyComplete();
var headers = mockExchange.getResponse().getHeaders();
assertEquals("FormLogin realm=\"console\"", headers.getFirst(WWW_AUTHENTICATE));
}
}