Просмотр исходного кода

Add Supplier JwtDecoders

Closes gh-9991
Josh Cummings 3 лет назад
Родитель
Сommit
4e7c9bee46

+ 32 - 0
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderInitializationException.java

@@ -0,0 +1,32 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.oauth2.jwt;
+
+/**
+ * An exception thrown when a {@link JwtDecoder} or {@link ReactiveJwtDecoder}'s lazy
+ * initialization fails.
+ *
+ * @author Josh Cummings
+ * @since 5.6
+ */
+public class JwtDecoderInitializationException extends RuntimeException {
+
+	public JwtDecoderInitializationException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+}

+ 61 - 0
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/SupplierJwtDecoder.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.oauth2.jwt;
+
+import java.util.function.Supplier;
+
+/**
+ * A {@link JwtDecoder} that lazily initializes another {@link JwtDecoder}
+ *
+ * @author Josh Cummings
+ * @since 5.6
+ */
+public final class SupplierJwtDecoder implements JwtDecoder {
+
+	private final Supplier<JwtDecoder> jwtDecoderSupplier;
+
+	private volatile JwtDecoder delegate;
+
+	public SupplierJwtDecoder(Supplier<JwtDecoder> jwtDecoderSupplier) {
+		this.jwtDecoderSupplier = jwtDecoderSupplier;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Jwt decode(String token) throws JwtException {
+		if (this.delegate == null) {
+			synchronized (this.jwtDecoderSupplier) {
+				if (this.delegate == null) {
+					try {
+						this.delegate = this.jwtDecoderSupplier.get();
+					}
+					catch (Exception ex) {
+						throw wrapException(ex);
+					}
+				}
+			}
+		}
+		return this.delegate.decode(token);
+	}
+
+	private JwtDecoderInitializationException wrapException(Exception ex) {
+		return new JwtDecoderInitializationException("Failed to lazily resolve the supplied JwtDecoder instance", ex);
+	}
+
+}

+ 59 - 0
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/SupplierReactiveJwtDecoder.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.oauth2.jwt;
+
+import java.time.Duration;
+import java.util.function.Supplier;
+
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+
+/**
+ * A {@link ReactiveJwtDecoder} that lazily initializes another {@link ReactiveJwtDecoder}
+ *
+ * @author Josh Cummings
+ * @since 5.6
+ */
+public final class SupplierReactiveJwtDecoder implements ReactiveJwtDecoder {
+
+	private static final Duration FOREVER = Duration.ofMillis(Long.MAX_VALUE);
+
+	private Mono<ReactiveJwtDecoder> jwtDecoderMono;
+
+	public SupplierReactiveJwtDecoder(Supplier<ReactiveJwtDecoder> supplier) {
+		// @formatter:off
+		this.jwtDecoderMono = Mono.fromSupplier(supplier)
+				.subscribeOn(Schedulers.boundedElastic())
+				.publishOn(Schedulers.parallel())
+				.onErrorMap(this::wrapException)
+				.cache((delegate) -> FOREVER, (ex) -> Duration.ZERO, () -> Duration.ZERO);
+		// @formatter:on
+	}
+
+	private JwtDecoderInitializationException wrapException(Throwable t) {
+		return new JwtDecoderInitializationException("Failed to lazily resolve the supplied JwtDecoder instance", t);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Mono<Jwt> decode(String token) throws JwtException {
+		return this.jwtDecoderMono.flatMap((decoder) -> decoder.decode(token));
+	}
+
+}

+ 80 - 0
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/SupplierJwtDecoderTests.java

@@ -0,0 +1,80 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.oauth2.jwt;
+
+import java.util.function.Supplier;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link SupplierJwtDecoder}
+ *
+ * @author Josh Cummings
+ */
+public class SupplierJwtDecoderTests {
+
+	@Test
+	public void decodeWhenUninitializedThenSupplierInitializes() {
+		JwtDecoder jwtDecoder = mock(JwtDecoder.class);
+		SupplierJwtDecoder supplierJwtDecoder = new SupplierJwtDecoder(() -> jwtDecoder);
+		supplierJwtDecoder.decode("token");
+		verify(jwtDecoder).decode("token");
+	}
+
+	@Test
+	public void decodeWhenInitializationFailsThenInitializationException() {
+		Supplier<JwtDecoder> broken = mock(Supplier.class);
+		given(broken.get()).willThrow(RuntimeException.class);
+		JwtDecoder jwtDecoder = new SupplierJwtDecoder(broken);
+		assertThatExceptionOfType(JwtDecoderInitializationException.class).isThrownBy(() -> jwtDecoder.decode("token"));
+		verify(broken).get();
+	}
+
+	@Test
+	public void decodeWhenInitializedThenCaches() {
+		JwtDecoder jwtDecoder = mock(JwtDecoder.class);
+		Supplier<JwtDecoder> supplier = mock(Supplier.class);
+		given(supplier.get()).willReturn(jwtDecoder);
+		JwtDecoder supplierJwtDecoder = new SupplierJwtDecoder(supplier);
+		supplierJwtDecoder.decode("token");
+		supplierJwtDecoder.decode("token");
+		verify(supplier, times(1)).get();
+		verify(jwtDecoder, times(2)).decode("token");
+	}
+
+	@Test
+	public void decodeWhenInitializationInitiallyFailsThenRecoverable() {
+		JwtDecoder jwtDecoder = mock(JwtDecoder.class);
+		Supplier<JwtDecoder> broken = mock(Supplier.class);
+		given(broken.get()).willThrow(RuntimeException.class);
+		JwtDecoder supplierJwtDecoder = new SupplierJwtDecoder(broken);
+		assertThatExceptionOfType(JwtDecoderInitializationException.class)
+				.isThrownBy(() -> supplierJwtDecoder.decode("token"));
+		reset(broken);
+		given(broken.get()).willReturn(jwtDecoder);
+		supplierJwtDecoder.decode("token");
+		verify(jwtDecoder).decode("token");
+	}
+
+}

+ 83 - 0
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/SupplierReactiveJwtDecoderTests.java

@@ -0,0 +1,83 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.oauth2.jwt;
+
+import java.util.function.Supplier;
+
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Mono;
+
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link SupplierReactiveJwtDecoder}
+ */
+public class SupplierReactiveJwtDecoderTests {
+
+	@Test
+	public void decodeWhenUninitializedThenSupplierInitializes() {
+		ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
+		given(jwtDecoder.decode("token")).willReturn(Mono.empty());
+		SupplierReactiveJwtDecoder supplierReactiveJwtDecoder = new SupplierReactiveJwtDecoder(() -> jwtDecoder);
+		supplierReactiveJwtDecoder.decode("token").block();
+		verify(jwtDecoder).decode("token");
+	}
+
+	@Test
+	public void decodeWhenInitializationFailsThenInitializationException() {
+		Supplier<ReactiveJwtDecoder> broken = mock(Supplier.class);
+		given(broken.get()).willThrow(RuntimeException.class);
+		ReactiveJwtDecoder jwtDecoder = new SupplierReactiveJwtDecoder(broken);
+		assertThatExceptionOfType(JwtDecoderInitializationException.class)
+				.isThrownBy(() -> jwtDecoder.decode("token").block());
+		verify(broken).get();
+	}
+
+	@Test
+	public void decodeWhenInitializedThenCaches() {
+		ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
+		Supplier<ReactiveJwtDecoder> supplier = mock(Supplier.class);
+		given(supplier.get()).willReturn(jwtDecoder);
+		given(jwtDecoder.decode("token")).willReturn(Mono.empty());
+		ReactiveJwtDecoder supplierReactiveJwtDecoder = new SupplierReactiveJwtDecoder(supplier);
+		supplierReactiveJwtDecoder.decode("token").block();
+		supplierReactiveJwtDecoder.decode("token").block();
+		verify(supplier, times(1)).get();
+		verify(jwtDecoder, times(2)).decode("token");
+	}
+
+	@Test
+	public void decodeWhenInitializationInitiallyFailsThenRecoverable() {
+		ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
+		Supplier<ReactiveJwtDecoder> broken = mock(Supplier.class);
+		given(broken.get()).willThrow(RuntimeException.class);
+		given(jwtDecoder.decode("token")).willReturn(Mono.empty());
+		ReactiveJwtDecoder supplierReactiveJwtDecoder = new SupplierReactiveJwtDecoder(broken);
+		assertThatExceptionOfType(JwtDecoderInitializationException.class)
+				.isThrownBy(() -> supplierReactiveJwtDecoder.decode("token").block());
+		reset(broken);
+		given(broken.get()).willReturn(jwtDecoder);
+		supplierReactiveJwtDecoder.decode("token").block();
+		verify(jwtDecoder).decode("token");
+	}
+
+}