Add websocket-authentication sample
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
package com.example.greeting;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class GreetingApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(GreetingApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.greeting;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.springframework.graphql.client.WebSocketGraphQlClient;
|
||||
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
|
||||
|
||||
public class GreetingClient {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient
|
||||
.builder(URI.create("ws://localhost:8080/graphql"), new ReactorNettyWebSocketClient())
|
||||
.interceptor(JwtGraphQlClientInterceptor.create())
|
||||
.build();
|
||||
|
||||
graphQlClient.document("subscription {greetings}")
|
||||
.retrieveSubscription("greetings")
|
||||
.toEntity(String.class)
|
||||
.take(5)
|
||||
.doOnNext(System.out::println)
|
||||
.blockLast();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.example.greeting;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
@Controller
|
||||
public class GreetingController {
|
||||
|
||||
@QueryMapping
|
||||
String greeting(Authentication authentication) {
|
||||
return "Hello " + authentication.getName() + "!";
|
||||
}
|
||||
|
||||
@SubscriptionMapping
|
||||
Flux<String> greetings(Authentication authentication) {
|
||||
return Flux.interval(Duration.ofMillis(50))
|
||||
.map((num) -> "Hello " + authentication.getName() + num + "!");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.example.greeting;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Map;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.graphql.client.WebSocketGraphQlClientInterceptor;
|
||||
import org.springframework.security.converter.RsaKeyConverters;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
|
||||
public final class JwtGraphQlClientInterceptor implements WebSocketGraphQlClientInterceptor {
|
||||
|
||||
private final String token;
|
||||
|
||||
|
||||
private JwtGraphQlClientInterceptor(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Object> connectionInitPayload() {
|
||||
return Mono.just(Map.of("Authorization", "Bearer " + this.token));
|
||||
}
|
||||
|
||||
|
||||
public static JwtGraphQlClientInterceptor create() {
|
||||
return new JwtGraphQlClientInterceptor(initToken());
|
||||
}
|
||||
|
||||
private static String initToken() {
|
||||
ClassPathResource privResource = new ClassPathResource("simple.priv");
|
||||
ClassPathResource pubResource = new ClassPathResource("simple.pub");
|
||||
|
||||
try (InputStream priv = privResource.getInputStream(); InputStream pub = pubResource.getInputStream()) {
|
||||
RSAPublicKey publicKey = RsaKeyConverters.x509().convert(pub);
|
||||
RSAPrivateKey privateKey = RsaKeyConverters.pkcs8().convert(priv);
|
||||
RSAKey key = new RSAKey.Builder(publicKey).privateKey(privateKey).build();
|
||||
JwtEncoder encoder = new NimbusJwtEncoder(new ImmutableJWKSet<>(new JWKSet(key)));
|
||||
JwtClaimsSet set = JwtClaimsSet.builder().subject("Markey").claim("scope", "greeting:read").build();
|
||||
return encoder.encode(JwtEncoderParameters.from(set)).getTokenValue();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.example.greeting;
|
||||
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.graphql.server.WebSocketGraphQlInterceptor;
|
||||
import org.springframework.graphql.server.support.BearerTokenAuthenticationExtractor;
|
||||
import org.springframework.graphql.server.webflux.AuthenticationWebSocketInterceptor;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet")
|
||||
public class WebFluxSecurityConfig {
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain webFilters(ServerHttpSecurity http) {
|
||||
http.authorizeExchange((authorize) -> authorize.anyExchange().permitAll());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebSocketGraphQlInterceptor authenticationInterceptor(@Value("classpath:simple.pub") RSAPublicKey pub) {
|
||||
return new AuthenticationWebSocketInterceptor(
|
||||
new BearerTokenAuthenticationExtractor(),
|
||||
new JwtReactiveAuthenticationManager(NimbusReactiveJwtDecoder.withPublicKey(pub).build()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.example.greeting;
|
||||
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.graphql.server.WebSocketGraphQlInterceptor;
|
||||
import org.springframework.graphql.server.support.BearerTokenAuthenticationExtractor;
|
||||
import org.springframework.graphql.server.webmvc.AuthenticationWebSocketInterceptor;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnClass(name = "org.springframework.web.servlet.DispatcherServlet")
|
||||
public class WebMvcSecurityConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filters(HttpSecurity http) throws Exception {
|
||||
http.authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebSocketGraphQlInterceptor authenticationInterceptor(@Value("classpath:simple.pub") RSAPublicKey pub) {
|
||||
return new AuthenticationWebSocketInterceptor(
|
||||
new BearerTokenAuthenticationExtractor(),
|
||||
new ProviderManager(new JwtAuthenticationProvider(NimbusJwtDecoder.withPublicKey(pub).build())));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NonNullApi
|
||||
package com.example.greeting;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
@@ -0,0 +1,10 @@
|
||||
spring.application.name=websocket-authentication
|
||||
|
||||
spring.graphql.websocket.path=/graphql
|
||||
spring.graphql.graphiql.enabled=true
|
||||
|
||||
logging.level.com.example.greeting=DEBUG
|
||||
logging.level.org.springframework.graphql=TRACE
|
||||
logging.level.org.springframework.security=TRACE
|
||||
|
||||
spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:simple.pub
|
||||
@@ -0,0 +1,7 @@
|
||||
type Query {
|
||||
greeting: String!
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
greetings: String!
|
||||
}
|
||||
28
websocket-authentication/src/main/resources/simple.priv
Normal file
28
websocket-authentication/src/main/resources/simple.priv
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDcWWomvlNGyQhA
|
||||
iB0TcN3sP2VuhZ1xNRPxr58lHswC9Cbtdc2hiSbe/sxAvU1i0O8vaXwICdzRZ1JM
|
||||
g1TohG9zkqqjZDhyw1f1Ic6YR/OhE6NCpqERy97WMFeW6gJd1i5inHj/W19GAbqK
|
||||
LhSHGHqIjyo0wlBf58t+qFt9h/EFBVE/LAGQBsg/jHUQCxsLoVI2aSELGIw2oSDF
|
||||
oiljwLaQl0n9khX5ZbiegN3OkqodzCYHwWyu6aVVj8M1W9RIMiKmKr09s/gf31Nc
|
||||
3WjvjqhFo1rTuurWGgKAxJLL7zlJqAKjGWbIT4P6h/1Kwxjw6X23St3OmhsG6HIn
|
||||
+jl1++MrAgMBAAECggEBAMf820wop3pyUOwI3aLcaH7YFx5VZMzvqJdNlvpg1jbE
|
||||
E2Sn66b1zPLNfOIxLcBG8x8r9Ody1Bi2Vsqc0/5o3KKfdgHvnxAB3Z3dPh2WCDek
|
||||
lCOVClEVoLzziTuuTdGO5/CWJXdWHcVzIjPxmK34eJXioiLaTYqN3XKqKMdpD0ZG
|
||||
mtNTGvGf+9fQ4i94t0WqIxpMpGt7NM4RHy3+Onggev0zLiDANC23mWrTsUgect/7
|
||||
62TYg8g1bKwLAb9wCBT+BiOuCc2wrArRLOJgUkj/F4/gtrR9ima34SvWUyoUaKA0
|
||||
bi4YBX9l8oJwFGHbU9uFGEMnH0T/V0KtIB7qetReywkCgYEA9cFyfBIQrYISV/OA
|
||||
+Z0bo3vh2aL0QgKrSXZ924cLt7itQAHNZ2ya+e3JRlTczi5mnWfjPWZ6eJB/8MlH
|
||||
Gpn12o/POEkU+XjZZSPe1RWGt5g0S3lWqyx9toCS9ACXcN9tGbaqcFSVI73zVTRA
|
||||
8J9grR0fbGn7jaTlTX2tnlOTQ60CgYEA5YjYpEq4L8UUMFkuj+BsS3u0oEBnzuHd
|
||||
I9LEHmN+CMPosvabQu5wkJXLuqo2TxRnAznsA8R3pCLkdPGoWMCiWRAsCn979TdY
|
||||
QbqO2qvBAD2Q19GtY7lIu6C35/enQWzJUMQE3WW0OvjLzZ0l/9mA2FBRR+3F9A1d
|
||||
rBdnmv0c3TcCgYEAi2i+ggVZcqPbtgrLOk5WVGo9F1GqUBvlgNn30WWNTx4zIaEk
|
||||
HSxtyaOLTxtq2odV7Kr3LGiKxwPpn/T+Ief+oIp92YcTn+VfJVGw4Z3BezqbR8lA
|
||||
Uf/+HF5ZfpMrVXtZD4Igs3I33Duv4sCuqhEvLWTc44pHifVloozNxYfRfU0CgYBN
|
||||
HXa7a6cJ1Yp829l62QlJKtx6Ymj95oAnQu5Ez2ROiZMqXRO4nucOjGUP55Orac1a
|
||||
FiGm+mC/skFS0MWgW8evaHGDbWU180wheQ35hW6oKAb7myRHtr4q20ouEtQMdQIF
|
||||
snV39G1iyqeeAsf7dxWElydXpRi2b68i3BIgzhzebQKBgQCdUQuTsqV9y/JFpu6H
|
||||
c5TVvhG/ubfBspI5DhQqIGijnVBzFT//UfIYMSKJo75qqBEyP2EJSmCsunWsAFsM
|
||||
TszuiGTkrKcZy9G0wJqPztZZl2F2+bJgnA6nBEV7g5PA4Af+QSmaIhRwqGDAuROR
|
||||
47jndeyIaMTNETEmOnms+as17g==
|
||||
-----END PRIVATE KEY-----
|
||||
9
websocket-authentication/src/main/resources/simple.pub
Normal file
9
websocket-authentication/src/main/resources/simple.pub
Normal file
@@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3FlqJr5TRskIQIgdE3Dd
|
||||
7D9lboWdcTUT8a+fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRv
|
||||
c5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4/1tfRgG6ii4Uhxh6
|
||||
iI8qNMJQX+fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2
|
||||
kJdJ/ZIV+WW4noDdzpKqHcwmB8FsrumlVY/DNVvUSDIipiq9PbP4H99TXN1o746o
|
||||
RaNa07rq1hoCgMSSy+85SagCoxlmyE+D+of9SsMY8Ol9t0rdzpobBuhyJ/o5dfvj
|
||||
KwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.example.greeting;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.graphql.test.tester.GraphQlTester;
|
||||
import org.springframework.graphql.test.tester.WebSocketGraphQlTester;
|
||||
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
class GreetingApplicationTests {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Value("http://localhost:${local.server.port}${spring.graphql.websocket.path}")
|
||||
private String baseUrl;
|
||||
|
||||
private GraphQlTester graphQlTester;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
URI url = URI.create(baseUrl);
|
||||
this.graphQlTester = WebSocketGraphQlTester.builder(url, new ReactorNettyWebSocketClient())
|
||||
.interceptor(JwtGraphQlClientInterceptor.create())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void greeting() {
|
||||
this.graphQlTester.document("{greeting}")
|
||||
.execute()
|
||||
.path("greeting")
|
||||
.entity(String.class).isEqualTo("Hello Markey!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void greetings() {
|
||||
Flux<String> flux = this.graphQlTester.document("subscription {greetings}")
|
||||
.executeSubscription()
|
||||
.toFlux("greetings", String.class);
|
||||
|
||||
StepVerifier.create(flux)
|
||||
.expectNext("Hello Markey0!", "Hello Markey1!", "Hello Markey2!", "Hello Markey3!")
|
||||
.thenCancel()
|
||||
.verify();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user