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 get = pathMatchers(HttpMethod.GET, "/**");
|
||||||
var notFavicon = new NegatedServerWebExchangeMatcher(
|
var notFavicon = new NegatedServerWebExchangeMatcher(
|
||||||
pathMatchers(
|
pathMatchers(
|
||||||
"/favicon.*", "/login/**", "/signup/**", "/password-reset/**", "/challenges/**"
|
"/favicon.*", "/login/**", "/signup/**", "/password-reset/**", "/challenges/**",
|
||||||
|
"/oauth2/**", "/social/**"
|
||||||
));
|
));
|
||||||
var html = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
|
var html = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
|
||||||
html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
|
html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.springframework.web.server.WebFilter;
|
||||||
import org.springframework.web.server.WebFilterChain;
|
import org.springframework.web.server.WebFilterChain;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.user.service.UserConnectionService;
|
import run.halo.app.core.user.service.UserConnectionService;
|
||||||
|
import run.halo.app.security.LoginHandlerEnhancer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A filter to map OAuth2 authentication to authenticated user.
|
* A filter to map OAuth2 authentication to authenticated user.
|
||||||
|
@ -47,6 +48,8 @@ class MapOAuth2AuthenticationFilter implements WebFilter {
|
||||||
|
|
||||||
private final ServerLogoutHandler logoutHandler;
|
private final ServerLogoutHandler logoutHandler;
|
||||||
|
|
||||||
|
private final LoginHandlerEnhancer loginHandlerEnhancer;
|
||||||
|
|
||||||
private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
|
private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
|
@ -56,10 +59,12 @@ class MapOAuth2AuthenticationFilter implements WebFilter {
|
||||||
public MapOAuth2AuthenticationFilter(
|
public MapOAuth2AuthenticationFilter(
|
||||||
ServerSecurityContextRepository securityContextRepository,
|
ServerSecurityContextRepository securityContextRepository,
|
||||||
UserConnectionService connectionService,
|
UserConnectionService connectionService,
|
||||||
ReactiveUserDetailsService userDetailsService) {
|
ReactiveUserDetailsService userDetailsService,
|
||||||
|
LoginHandlerEnhancer loginHandlerEnhancer) {
|
||||||
this.connectionService = connectionService;
|
this.connectionService = connectionService;
|
||||||
this.securityContextRepository = securityContextRepository;
|
this.securityContextRepository = securityContextRepository;
|
||||||
this.userDetailsService = userDetailsService;
|
this.userDetailsService = userDetailsService;
|
||||||
|
this.loginHandlerEnhancer = loginHandlerEnhancer;
|
||||||
var logoutHandler = new SecurityContextServerLogoutHandler();
|
var logoutHandler = new SecurityContextServerLogoutHandler();
|
||||||
logoutHandler.setSecurityContextRepository(securityContextRepository);
|
logoutHandler.setSecurityContextRepository(securityContextRepository);
|
||||||
this.logoutHandler = logoutHandler;
|
this.logoutHandler = logoutHandler;
|
||||||
|
@ -116,7 +121,10 @@ class MapOAuth2AuthenticationFilter implements WebFilter {
|
||||||
.map(userDetails -> authenticated(userDetails, oauth2Token))
|
.map(userDetails -> authenticated(userDetails, oauth2Token))
|
||||||
.flatMap(haloOAuth2Token -> {
|
.flatMap(haloOAuth2Token -> {
|
||||||
var securityContext = new SecurityContextImpl(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
|
// because this happens after the filter, there is no need to
|
||||||
// write SecurityContext to the context
|
// 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.security.web.server.context.ServerSecurityContextRepository;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import run.halo.app.core.user.service.UserConnectionService;
|
import run.halo.app.core.user.service.UserConnectionService;
|
||||||
|
import run.halo.app.security.LoginHandlerEnhancer;
|
||||||
import run.halo.app.security.authentication.SecurityConfigurer;
|
import run.halo.app.security.authentication.SecurityConfigurer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,17 +26,21 @@ class OAuth2SecurityConfigurer implements SecurityConfigurer {
|
||||||
|
|
||||||
private final ReactiveUserDetailsService userDetailsService;
|
private final ReactiveUserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
private final LoginHandlerEnhancer loginHandlerEnhancer;
|
||||||
|
|
||||||
public OAuth2SecurityConfigurer(ServerSecurityContextRepository securityContextRepository,
|
public OAuth2SecurityConfigurer(ServerSecurityContextRepository securityContextRepository,
|
||||||
UserConnectionService connectionService, ReactiveUserDetailsService userDetailsService) {
|
UserConnectionService connectionService, ReactiveUserDetailsService userDetailsService,
|
||||||
|
LoginHandlerEnhancer loginHandlerEnhancer) {
|
||||||
this.securityContextRepository = securityContextRepository;
|
this.securityContextRepository = securityContextRepository;
|
||||||
this.connectionService = connectionService;
|
this.connectionService = connectionService;
|
||||||
this.userDetailsService = userDetailsService;
|
this.userDetailsService = userDetailsService;
|
||||||
|
this.loginHandlerEnhancer = loginHandlerEnhancer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure(ServerHttpSecurity http) {
|
public void configure(ServerHttpSecurity http) {
|
||||||
var mapOAuth2Filter = new MapOAuth2AuthenticationFilter(
|
var mapOAuth2Filter = new MapOAuth2AuthenticationFilter(
|
||||||
securityContextRepository, connectionService, userDetailsService
|
securityContextRepository, connectionService, userDetailsService, loginHandlerEnhancer
|
||||||
);
|
);
|
||||||
http.addFilterBefore(mapOAuth2Filter, SecurityWebFiltersOrder.AUTHENTICATION);
|
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 static org.springframework.web.reactive.function.server.RequestPredicates.path;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -10,6 +11,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
|
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
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())
|
.before(HaloUtils.noCache())
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
|
@ -293,7 +293,15 @@
|
||||||
overflow: hidden;
|
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);
|
gap: var(--spacing-sm);
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
|
@ -313,7 +321,7 @@
|
||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pill-items li:hover a {
|
.pill-items li:hover {
|
||||||
color: #111827;
|
color: #111827;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -113,13 +113,35 @@
|
||||||
</div>
|
</div>
|
||||||
<ul class="pill-items">
|
<ul class="pill-items">
|
||||||
<li th:each="provider : ${socialAuthProviders}">
|
<li th:each="provider : ${socialAuthProviders}">
|
||||||
<a th:href="${provider.spec.authenticationUrl}">
|
<form
|
||||||
<img th:src="${provider.spec.logo}" th:alt="|${provider.spec.displayName}'s icon|" />
|
class="social-auth-provider-form"
|
||||||
<span th:text="${provider.spec.displayName}"></span>
|
th:action="|/login/social/${provider.metadata.name}?remember-me=false|"
|
||||||
</a>
|
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>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</th:block>
|
</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>
|
||||||
|
|
||||||
<div th:remove="tag" th:fragment="passwordResetMethods">
|
<div th:remove="tag" th:fragment="passwordResetMethods">
|
||||||
|
|
Loading…
Reference in New Issue