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 imports
pull/1853/head
guqing 2022-04-18 14:58:13 +08:00 committed by GitHub
parent 66be1d1ba7
commit 9dd778bdab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 209 additions and 3 deletions

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}