mirror of https://github.com/halo-dev/halo
Add social login endpoint for remember-me support (#7670)
#### What type of PR is this? /kind feature /kind api-change /area core /milestone 2.21.x #### What this PR does / why we need it: This PR adds a new endpoint `POST /login/social/{auth_provider_name}?remember-me=true` to make the social login support remember-me mechanism. #### Does this PR introduce a user-facing change? ```release-note 支持社交登录时选择是否保持登录 ```pull/7677/head
parent
9607ee4912
commit
576dda9d74
|
@ -106,7 +106,8 @@ public class HaloServerRequestCache extends WebSessionServerRequestCache {
|
|||
var get = pathMatchers(HttpMethod.GET, "/**");
|
||||
var notFavicon = new NegatedServerWebExchangeMatcher(
|
||||
pathMatchers(
|
||||
"/favicon.*", "/login/**", "/signup/**", "/password-reset/**", "/challenges/**"
|
||||
"/favicon.*", "/login/**", "/signup/**", "/password-reset/**", "/challenges/**",
|
||||
"/oauth2/**", "/social/**"
|
||||
));
|
||||
var html = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
|
||||
html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.springframework.web.server.WebFilter;
|
|||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.user.service.UserConnectionService;
|
||||
import run.halo.app.security.LoginHandlerEnhancer;
|
||||
|
||||
/**
|
||||
* A filter to map OAuth2 authentication to authenticated user.
|
||||
|
@ -47,6 +48,8 @@ class MapOAuth2AuthenticationFilter implements WebFilter {
|
|||
|
||||
private final ServerLogoutHandler logoutHandler;
|
||||
|
||||
private final LoginHandlerEnhancer loginHandlerEnhancer;
|
||||
|
||||
private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
|
||||
|
||||
@Setter
|
||||
|
@ -56,10 +59,12 @@ class MapOAuth2AuthenticationFilter implements WebFilter {
|
|||
public MapOAuth2AuthenticationFilter(
|
||||
ServerSecurityContextRepository securityContextRepository,
|
||||
UserConnectionService connectionService,
|
||||
ReactiveUserDetailsService userDetailsService) {
|
||||
ReactiveUserDetailsService userDetailsService,
|
||||
LoginHandlerEnhancer loginHandlerEnhancer) {
|
||||
this.connectionService = connectionService;
|
||||
this.securityContextRepository = securityContextRepository;
|
||||
this.userDetailsService = userDetailsService;
|
||||
this.loginHandlerEnhancer = loginHandlerEnhancer;
|
||||
var logoutHandler = new SecurityContextServerLogoutHandler();
|
||||
logoutHandler.setSecurityContextRepository(securityContextRepository);
|
||||
this.logoutHandler = logoutHandler;
|
||||
|
@ -116,7 +121,10 @@ class MapOAuth2AuthenticationFilter implements WebFilter {
|
|||
.map(userDetails -> authenticated(userDetails, oauth2Token))
|
||||
.flatMap(haloOAuth2Token -> {
|
||||
var securityContext = new SecurityContextImpl(haloOAuth2Token);
|
||||
return securityContextRepository.save(exchange, securityContext);
|
||||
return securityContextRepository.save(exchange, securityContext)
|
||||
.then(
|
||||
loginHandlerEnhancer.onLoginSuccess(exchange, haloOAuth2Token)
|
||||
);
|
||||
// because this happens after the filter, there is no need to
|
||||
// write SecurityContext to the context
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@ import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
|||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.core.user.service.UserConnectionService;
|
||||
import run.halo.app.security.LoginHandlerEnhancer;
|
||||
import run.halo.app.security.authentication.SecurityConfigurer;
|
||||
|
||||
/**
|
||||
|
@ -25,17 +26,21 @@ class OAuth2SecurityConfigurer implements SecurityConfigurer {
|
|||
|
||||
private final ReactiveUserDetailsService userDetailsService;
|
||||
|
||||
private final LoginHandlerEnhancer loginHandlerEnhancer;
|
||||
|
||||
public OAuth2SecurityConfigurer(ServerSecurityContextRepository securityContextRepository,
|
||||
UserConnectionService connectionService, ReactiveUserDetailsService userDetailsService) {
|
||||
UserConnectionService connectionService, ReactiveUserDetailsService userDetailsService,
|
||||
LoginHandlerEnhancer loginHandlerEnhancer) {
|
||||
this.securityContextRepository = securityContextRepository;
|
||||
this.connectionService = connectionService;
|
||||
this.userDetailsService = userDetailsService;
|
||||
this.loginHandlerEnhancer = loginHandlerEnhancer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(ServerHttpSecurity http) {
|
||||
var mapOAuth2Filter = new MapOAuth2AuthenticationFilter(
|
||||
securityContextRepository, connectionService, userDetailsService
|
||||
securityContextRepository, connectionService, userDetailsService, loginHandlerEnhancer
|
||||
);
|
||||
http.addFilterBefore(mapOAuth2Filter, SecurityWebFiltersOrder.AUTHENTICATION);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package run.halo.app.security.preauth;
|
|||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.path;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
@ -10,6 +11,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
|
@ -115,6 +117,24 @@ class PreAuthLoginEndpoint {
|
|||
))
|
||||
));
|
||||
})
|
||||
.POST("/social/{authProviderName}", request -> {
|
||||
var authProviderName = request.pathVariable("authProviderName");
|
||||
return authProviderService.getEnabledProviders()
|
||||
.filter(ap -> Objects.equals(authProviderName, ap.getMetadata().getName()))
|
||||
.filter(ap -> !AuthProvider.AuthType.FORM.equals(ap.getSpec().getAuthType()))
|
||||
.next()
|
||||
.switchIfEmpty(Mono.error(() -> new ServerWebInputException(
|
||||
"Auth provider " + authProviderName + " not found or not enabled."
|
||||
)))
|
||||
.flatMap(ap -> {
|
||||
var authenticationUrl = ap.getSpec().getAuthenticationUrl();
|
||||
return rememberMeRequestCache.saveRememberMe(request.exchange())
|
||||
.then(Mono.defer(() -> ServerResponse.status(HttpStatus.FOUND)
|
||||
.location(URI.create(authenticationUrl))
|
||||
.build()
|
||||
));
|
||||
});
|
||||
})
|
||||
.before(HaloUtils.noCache())
|
||||
.build());
|
||||
}
|
||||
|
|
|
@ -293,7 +293,15 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pill-items li a {
|
||||
.pill-items li button {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pill-items li a,
|
||||
.pill-items li button {
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--text-sm);
|
||||
color: #1f2937;
|
||||
|
@ -313,7 +321,7 @@
|
|||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.pill-items li:hover a {
|
||||
.pill-items li:hover {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
|
|
|
@ -113,13 +113,35 @@
|
|||
</div>
|
||||
<ul class="pill-items">
|
||||
<li th:each="provider : ${socialAuthProviders}">
|
||||
<a th:href="${provider.spec.authenticationUrl}">
|
||||
<form
|
||||
class="social-auth-provider-form"
|
||||
th:action="|/login/social/${provider.metadata.name}?remember-me=false|"
|
||||
method="post"
|
||||
>
|
||||
<button type="submit">
|
||||
<img th:src="${provider.spec.logo}" th:alt="|${provider.spec.displayName}'s icon|" />
|
||||
<span th:text="${provider.spec.displayName}"></span>
|
||||
</a>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</th:block>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const rememberMeCheckbox = document.getElementById("remember-me");
|
||||
const socialAuthProviders = document.querySelectorAll(".social-auth-provider-form");
|
||||
|
||||
rememberMeCheckbox.addEventListener("change", function (e) {
|
||||
const rememberMe = e.target.checked;
|
||||
|
||||
socialAuthProviders.forEach((form) => {
|
||||
const url = new URL(form.action, window.location.origin);
|
||||
url.searchParams.set("remember-me", rememberMe ? "true" : "false");
|
||||
form.action = url.pathname + url.search;
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div th:remove="tag" th:fragment="passwordResetMethods">
|
||||
|
|
Loading…
Reference in New Issue