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.provisioning.InMemoryUserDetailsManager;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
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.JwtDaoAuthenticationProvider;
|
||||||
import run.halo.app.identity.authentication.JwtGenerator;
|
import run.halo.app.identity.authentication.JwtGenerator;
|
||||||
import run.halo.app.identity.authentication.JwtUsernamePasswordAuthenticationFilter;
|
import run.halo.app.identity.authentication.JwtUsernamePasswordAuthenticationFilter;
|
||||||
import run.halo.app.identity.authentication.OAuth2AuthorizationService;
|
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.JwtAccessDeniedHandler;
|
||||||
import run.halo.app.identity.entrypoint.JwtAuthenticationEntryPoint;
|
import run.halo.app.identity.entrypoint.JwtAuthenticationEntryPoint;
|
||||||
import run.halo.app.infra.properties.JwtProperties;
|
import run.halo.app.infra.properties.JwtProperties;
|
||||||
|
@ -60,6 +63,8 @@ public class WebSecurityConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
ProviderSettings providerSettings = providerSettings();
|
||||||
|
ProviderContextFilter providerContextFilter = new ProviderContextFilter(providerSettings);
|
||||||
http
|
http
|
||||||
.authorizeHttpRequests((authorize) -> authorize
|
.authorizeHttpRequests((authorize) -> authorize
|
||||||
.antMatchers("/api/v1/oauth2/login").permitAll()
|
.antMatchers("/api/v1/oauth2/login").permitAll()
|
||||||
|
@ -69,6 +74,7 @@ public class WebSecurityConfig {
|
||||||
.httpBasic(Customizer.withDefaults())
|
.httpBasic(Customizer.withDefaults())
|
||||||
.addFilterBefore(new JwtUsernamePasswordAuthenticationFilter(authenticationManager()),
|
.addFilterBefore(new JwtUsernamePasswordAuthenticationFilter(authenticationManager()),
|
||||||
UsernamePasswordAuthenticationFilter.class)
|
UsernamePasswordAuthenticationFilter.class)
|
||||||
|
.addFilterAfter(providerContextFilter, SecurityContextPersistenceFilter.class)
|
||||||
.sessionManagement(
|
.sessionManagement(
|
||||||
(session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
(session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.exceptionHandling((exceptions) -> exceptions
|
.exceptionHandling((exceptions) -> exceptions
|
||||||
|
@ -123,4 +129,9 @@ public class WebSecurityConfig {
|
||||||
.build();
|
.build();
|
||||||
return new InMemoryUserDetailsManager(user);
|
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()
|
Set<String> scopes = usernamePasswordAuthenticationToken.getAuthorities().stream()
|
||||||
.map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
|
.map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
|
||||||
|
|
||||||
ProviderContext providerContext =
|
|
||||||
new ProviderContext(ProviderSettings.builder().build(), () -> "/issuer");
|
|
||||||
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||||
.principal(authentication)
|
.principal(authentication)
|
||||||
.providerContext(providerContext)
|
.providerContext(ProviderContextHolder.getProviderContext())
|
||||||
.authorizedScopes(scopes);
|
.authorizedScopes(scopes);
|
||||||
|
|
||||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization);
|
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