mirror of https://github.com/halo-dev/halo
feat: Add ProviderContextHolder for token generate (#1852)
* feat: Add auhentication provider and password authentication filter * refactor: JwtUsernamePasswordAuthenticationFilter and add test case * chore: delete unused class * fix: code style * refactor: web secutity config * feat: Add ProviderContextHolder for token generate * chore: optimize importspull/1853/head
parent
66be1d1ba7
commit
9dd778bdab
|
@ -29,10 +29,13 @@ import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
|||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
|
||||
import run.halo.app.identity.authentication.JwtDaoAuthenticationProvider;
|
||||
import run.halo.app.identity.authentication.JwtGenerator;
|
||||
import run.halo.app.identity.authentication.JwtUsernamePasswordAuthenticationFilter;
|
||||
import run.halo.app.identity.authentication.OAuth2AuthorizationService;
|
||||
import run.halo.app.identity.authentication.ProviderContextFilter;
|
||||
import run.halo.app.identity.authentication.ProviderSettings;
|
||||
import run.halo.app.identity.entrypoint.JwtAccessDeniedHandler;
|
||||
import run.halo.app.identity.entrypoint.JwtAuthenticationEntryPoint;
|
||||
import run.halo.app.infra.properties.JwtProperties;
|
||||
|
@ -60,6 +63,8 @@ public class WebSecurityConfig {
|
|||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
ProviderSettings providerSettings = providerSettings();
|
||||
ProviderContextFilter providerContextFilter = new ProviderContextFilter(providerSettings);
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.antMatchers("/api/v1/oauth2/login").permitAll()
|
||||
|
@ -69,6 +74,7 @@ public class WebSecurityConfig {
|
|||
.httpBasic(Customizer.withDefaults())
|
||||
.addFilterBefore(new JwtUsernamePasswordAuthenticationFilter(authenticationManager()),
|
||||
UsernamePasswordAuthenticationFilter.class)
|
||||
.addFilterAfter(providerContextFilter, SecurityContextPersistenceFilter.class)
|
||||
.sessionManagement(
|
||||
(session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.exceptionHandling((exceptions) -> exceptions
|
||||
|
@ -123,4 +129,9 @@ public class WebSecurityConfig {
|
|||
.build();
|
||||
return new InMemoryUserDetailsManager(user);
|
||||
}
|
||||
|
||||
@Bean
|
||||
ProviderSettings providerSettings() {
|
||||
return ProviderSettings.builder().build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,11 +60,9 @@ public class JwtDaoAuthenticationProvider extends DaoAuthenticationProvider {
|
|||
Set<String> scopes = usernamePasswordAuthenticationToken.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
|
||||
|
||||
ProviderContext providerContext =
|
||||
new ProviderContext(ProviderSettings.builder().build(), () -> "/issuer");
|
||||
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||
.principal(authentication)
|
||||
.providerContext(providerContext)
|
||||
.providerContext(ProviderContextHolder.getProviderContext())
|
||||
.authorizedScopes(scopes);
|
||||
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization);
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
package run.halo.app.identity.authentication;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* A {@code Filter} that associates the {@link ProviderContext} to the
|
||||
* {@link ProviderContextHolder}.
|
||||
*
|
||||
* @author guqing
|
||||
* @see ProviderContext
|
||||
* @see ProviderContextHolder
|
||||
* @see ProviderSettings
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public final class ProviderContextFilter extends OncePerRequestFilter {
|
||||
private final ProviderSettings providerSettings;
|
||||
|
||||
/**
|
||||
* Constructs a {@code ProviderContextFilter} using the provided parameters.
|
||||
*
|
||||
* @param providerSettings the provider settings
|
||||
*/
|
||||
public ProviderContextFilter(ProviderSettings providerSettings) {
|
||||
Assert.notNull(providerSettings, "providerSettings cannot be null");
|
||||
this.providerSettings = providerSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
try {
|
||||
ProviderContext providerContext = new ProviderContext(
|
||||
this.providerSettings, () -> resolveIssuer(this.providerSettings, request));
|
||||
ProviderContextHolder.setProviderContext(providerContext);
|
||||
filterChain.doFilter(request, response);
|
||||
} finally {
|
||||
ProviderContextHolder.resetProviderContext();
|
||||
}
|
||||
}
|
||||
|
||||
private static String resolveIssuer(ProviderSettings providerSettings,
|
||||
HttpServletRequest request) {
|
||||
return providerSettings.getIssuer() != null
|
||||
? providerSettings.getIssuer() : getContextPath(request);
|
||||
}
|
||||
|
||||
private static String getContextPath(HttpServletRequest request) {
|
||||
return UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
|
||||
.replacePath(request.getContextPath())
|
||||
.replaceQuery(null)
|
||||
.fragment(null)
|
||||
.build()
|
||||
.toUriString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package run.halo.app.identity.authentication;
|
||||
|
||||
/**
|
||||
* A holder of {@link ProviderContext} that associates it with the current thread using a {@code
|
||||
* ThreadLocal}.
|
||||
*
|
||||
* @author guqing
|
||||
* @see ProviderContext
|
||||
* @see ProviderContextFilter
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public final class ProviderContextHolder {
|
||||
private static final ThreadLocal<ProviderContext> holder = new ThreadLocal<>();
|
||||
|
||||
private ProviderContextHolder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ProviderContext} bound to the current thread.
|
||||
*
|
||||
* @return the {@link ProviderContext}
|
||||
*/
|
||||
public static ProviderContext getProviderContext() {
|
||||
return holder.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the given {@link ProviderContext} to the current thread.
|
||||
*
|
||||
* @param providerContext the {@link ProviderContext}
|
||||
*/
|
||||
public static void setProviderContext(ProviderContext providerContext) {
|
||||
if (providerContext == null) {
|
||||
resetProviderContext();
|
||||
} else {
|
||||
holder.set(providerContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the {@link ProviderContext} bound to the current thread.
|
||||
*/
|
||||
public static void resetProviderContext() {
|
||||
holder.remove();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package run.halo.app.authentication;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import run.halo.app.identity.authentication.ProviderContext;
|
||||
import run.halo.app.identity.authentication.ProviderContextFilter;
|
||||
import run.halo.app.identity.authentication.ProviderContextHolder;
|
||||
import run.halo.app.identity.authentication.ProviderSettings;
|
||||
|
||||
/**
|
||||
* Tests for {@link ProviderContextFilter}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class ProviderContextFilterTest {
|
||||
|
||||
@AfterEach
|
||||
public void cleanup() {
|
||||
ProviderContextHolder.resetProviderContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenProviderSettingsNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new ProviderContextFilter(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("providerSettings cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenIssuerConfiguredThenUsed() throws Exception {
|
||||
String issuer = "https://provider.com";
|
||||
ProviderSettings providerSettings = ProviderSettings.builder().issuer(issuer).build();
|
||||
ProviderContextFilter filter = new ProviderContextFilter(providerSettings);
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
|
||||
request.setServletPath("/");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
doAnswer(invocation -> {
|
||||
ProviderContext providerContext = ProviderContextHolder.getProviderContext();
|
||||
assertThat(providerContext).isNotNull();
|
||||
assertThat(providerContext.providerSettings()).isSameAs(providerSettings);
|
||||
assertThat(providerContext.getIssuer()).isEqualTo(issuer);
|
||||
return null;
|
||||
}).when(filterChain).doFilter(any(), any());
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
assertThat(ProviderContextHolder.getProviderContext()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenIssuerNotConfiguredThenResolveFromRequest() throws Exception {
|
||||
ProviderSettings providerSettings = ProviderSettings.builder().build();
|
||||
ProviderContextFilter filter = new ProviderContextFilter(providerSettings);
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
|
||||
request.setServletPath("/");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
doAnswer(invocation -> {
|
||||
ProviderContext providerContext = ProviderContextHolder.getProviderContext();
|
||||
assertThat(providerContext).isNotNull();
|
||||
assertThat(providerContext.providerSettings()).isSameAs(providerSettings);
|
||||
assertThat(providerContext.getIssuer()).isEqualTo("http://localhost");
|
||||
return null;
|
||||
}).when(filterChain).doFilter(any(), any());
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
assertThat(ProviderContextHolder.getProviderContext()).isNull();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue