This is a cheat sheet for Matt Raible’s Full Stack Reactive with React and Spring WebFlux workshop. This cheat sheet contains the bare minimum steps you’ll need to build the example app.
This workshop is based on the following blog post series:
You can also watch a screencast to see all the code created in this series.
-
13:00 Talk: Intro to Reactive Programming and Spring WebFlux
-
13:30 Lab: Build a Reactive API with Spring WebFlux
-
15:00 Talk: Intro to React and Streaming Data Options
-
15:30 Lab: Build a React UI with Streaming Data
Note
|
The brackets at the end of each step indicate the alias’s or IntelliJ Live Templates to use. You can find the template definitions at mraible/idea-live-templates. |
Follow the steps below or read Build Reactive APIs with Spring WebFlux for a more thorough tutorial.
-
Use start.spring.io to create an app with
data-mongodb-reactive
,webflux
,devtools
, andlombok
http https://start.spring.io/starter.zip dependencies==data-mongodb-reactive,webflux,devtools,lombok -d
-
Install MongoDB. If you’re on a Mac, you can use
brew install mongodb
. If you’re on Debian-based Linux distributions, you can useapt install mongodb
. Start it by runningmongod
in a terminal window. -
Add
Profile
,ProfileRepository
, andSampleDataInitializer
[webflux-entity
,webflux-repo
,webflux-data
]
src/main/java/com/example/demo/Profile.java
package com.example.demo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document
@Data
@AllArgsConstructor
@NoArgsConstructor
class Profile {
@Id
private String id;
private String email;
}
src/main/java/com/example/demo/ProfileRepository.java
package com.example.demo;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
interface ProfileRepository extends ReactiveMongoRepository<Profile, String> {
}
src/main/java/com/example/demo/SampleDataInitializer.java
package com.example.demo;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import java.util.UUID;
@Log4j2
@Component
@org.springframework.context.annotation.Profile("demo")
class SampleDataInitializer implements ApplicationListener<ApplicationReadyEvent> {
private final ProfileRepository repository;
public SampleDataInitializer(ProfileRepository repository) {
this.repository = repository;
}
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
repository
.deleteAll()
.thenMany(
reactor.core.publisher.Flux
.just("A", "B", "C", "D")
.map(name -> new Profile(UUID.randomUUID().toString(), name + "@email.com"))
.flatMap(repository::save)
)
.thenMany(repository.findAll())
.subscribe(profile -> log.info("saving " + profile.toString()));
}
}
-
Start and see list of profiles in console:
SPRING_PROFILES_ACTIVE=demo mvn spring-boot:run
Tip
|
If you prefer to use an IDE to run your app, you’ll need to install a Lombok plugin for it. |
-
Create
ProfileService
[webflux-service
]
src/main/java/com/example/demo/ProfileService.java
package com.example.demo;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Log4j2
@Service
class ProfileService {
private final ApplicationEventPublisher publisher;
private final ProfileRepository repository;
ProfileService(ApplicationEventPublisher publisher, ProfileRepository repository) {
this.publisher = publisher;
this.repository = repository;
}
public Flux<Profile> all() {
return this.repository.findAll();
}
public Mono<Profile> get(String id) {
return this.repository.findById(id);
}
public Mono<Profile> update(String id, String email) {
return this.repository
.findById(id)
.map(p -> new Profile(p.getId(), email))
.flatMap(this.repository::save);
}
public Mono<Profile> delete(String id) {
return this.repository
.findById(id)
.flatMap(p -> this.repository.deleteById(p.getId()).thenReturn(p));
}
public Mono<Profile> create(String email) {
return this.repository
.save(new Profile(null, email));
//.doOnSuccess(entity -> this.publisher.publishEvent(new ProfileCreatedEvent(entity)));
}
}
-
Create
ProfileRestController
with a/profiles
endpoint [webflux-controller
]
src/main/java/com/example/demo/ProfileRestController.java
package com.example.demo;
import org.reactivestreams.Publisher;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import java.net.URI;
@RestController
@RequestMapping(value = "/profiles", produces = MediaType.APPLICATION_JSON_VALUE)
@org.springframework.context.annotation.Profile("classic")
class ProfileRestController {
private final MediaType mediaType = MediaType.APPLICATION_JSON_UTF8;
private final ProfileService profileService;
ProfileRestController(ProfileService profileService) {
this.profileService = profileService;
}
@GetMapping
Publisher<Profile> getAll() {
return this.profileService.all();
}
@GetMapping("/{id}")
Publisher<Profile> getById(@PathVariable("id") String id) {
return this.profileService.get(id);
}
@PostMapping
Publisher<ResponseEntity<Profile>> create(@RequestBody Profile profile) {
return this.profileService
.create(profile.getEmail())
.map(p -> ResponseEntity.created(URI.create("/profiles/" + p.getId()))
.contentType(mediaType)
.build());
}
@DeleteMapping("/{id}")
Publisher<Profile> deleteById(@PathVariable String id) {
return this.profileService.delete(id);
}
@PutMapping("/{id}")
Publisher<ResponseEntity<Profile>> updateById(@PathVariable String id, @RequestBody Profile profile) {
return Mono.just(profile)
.flatMap(p -> this.profileService.update(id, p.getEmail()))
.map(p -> org.springframework.http.ResponseEntity
.ok()
.contentType(this.mediaType)
.build());
}
}
-
Restart server with classic profile:
SPRING_PROFILES_ACTIVE=classic mvn spring-boot:run
-
Restart and confirm http://localhost:8080/profiles works in your browser and with HTTPie
http POST :8080/profiles email='[email protected]' http PUT :8080/profiles/1 email='[email protected]' http DELETE :8080/profiles/1
-
Create
ProfileCreatedEvent
,ProfileCreatedEventPublisher
[webflux-event
,webflux-publisher
]
src/main/java/com/example/demo/ProfileCreatedEvent.java
package com.example.demo;
import org.springframework.context.ApplicationEvent;
public class ProfileCreatedEvent extends ApplicationEvent {
public ProfileCreatedEvent(Profile source) {
super(source);
}
}
src/main/java/com/example/demo/ProfileCreatedEventPublisher.java
package com.example.demo;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import reactor.core.publisher.FluxSink;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Consumer;
@Component
class ProfileCreatedEventPublisher implements
ApplicationListener<ProfileCreatedEvent>,
Consumer<FluxSink<ProfileCreatedEvent>> {
private final Executor executor;
private final BlockingQueue<ProfileCreatedEvent> queue =
new LinkedBlockingQueue<>();
ProfileCreatedEventPublisher(Executor executor) {
this.executor = executor;
}
@Override
public void onApplicationEvent(ProfileCreatedEvent event) {
this.queue.offer(event);
}
@Override
public void accept(FluxSink<ProfileCreatedEvent> sink) {
this.executor.execute(() -> {
while (true) {
try {
ProfileCreatedEvent event = queue.take();
sink.next(event);
}
catch (InterruptedException e) {
ReflectionUtils.rethrowRuntimeException(e);
}
}
});
}
}
-
Create
WebSocketConfiguration
for executor bean [webflux-websocket
]
src/main/java/com/example/demo/WebSocketConfiguration.java
package com.example.demo;
import com.fasterxml.jackson.core.JsonProcessingException;
import ObjectMapper;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketMessage;
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
import reactor.core.publisher.Flux;
import java.util.Collections;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@Log4j2
@Configuration
class WebSocketConfiguration {
@Bean
Executor executor() {
return Executors.newSingleThreadExecutor();
}
@Bean
HandlerMapping handlerMapping(WebSocketHandler wsh) {
return new SimpleUrlHandlerMapping() {
{
setUrlMap(Collections.singletonMap("/ws/profiles", wsh));
setOrder(10);
}
};
}
@Bean
WebSocketHandlerAdapter webSocketHandlerAdapter() {
return new WebSocketHandlerAdapter();
}
@Bean
WebSocketHandler webSocketHandler(
ObjectMapper objectMapper,
ProfileCreatedEventPublisher eventPublisher
) {
Flux<ProfileCreatedEvent> publish = Flux
.create(eventPublisher)
.share();
return session -> {
Flux<WebSocketMessage> messageFlux = publish
.map(evt -> {
try {
return objectMapper.writeValueAsString(evt.getSource());
}
catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
})
.map(str -> {
log.info("sending " + str);
return session.textMessage(str);
});
return session.send(messageFlux);
};
}
}
-
Create
ProfileHandler
,ProfileEndpointConfiguration
, andCaseInsensitiveRequestPredicate
[webflux-handler
,webflux-endpoint
,webflux-predicate
]
src/main/java/com/example/demo/ProfileHandler.java
package com.example.demo;
import org.reactivestreams.Publisher;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
@Component
class ProfileHandler {
private final ProfileService profileService;
ProfileHandler(ProfileService profileService) {
this.profileService = profileService;
}
Mono<ServerResponse> getById(ServerRequest r) {
return defaultReadResponse(this.profileService.get(id(r)));
}
Mono<ServerResponse> all(ServerRequest r) {
return defaultReadResponse(this.profileService.all());
}
Mono<ServerResponse> deleteById(ServerRequest r) {
return defaultReadResponse(this.profileService.delete(id(r)));
}
Mono<ServerResponse> updateById(ServerRequest r) {
Flux<Profile> id = r.bodyToFlux(Profile.class)
.flatMap(p -> this.profileService.update(id(r), p.getEmail()));
return defaultReadResponse(id);
}
Mono<ServerResponse> create(ServerRequest request) {
Flux<Profile> flux = request
.bodyToFlux(Profile.class)
.flatMap(toWrite -> this.profileService.create(toWrite.getEmail()));
return defaultWriteResponse(flux);
}
private static Mono<ServerResponse> defaultWriteResponse(Publisher<Profile> profiles) {
return Mono.from(profiles)
.flatMap(p -> ServerResponse
.created(URI.create("/profiles/" + p.getId()))
.contentType(MediaType.APPLICATION_JSON_UTF8)
.build()
);
}
private static Mono<ServerResponse> defaultReadResponse(Publisher<Profile> profiles) {
return ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(profiles, Profile.class);
}
private static String id(ServerRequest r) {
return r.pathVariable("id");
}
}
src/main/java/com/example/demo/ProfileEndpointConfiguration.java
package com.example.demo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@Configuration
class ProfileEndpointConfiguration {
@Bean
RouterFunction<ServerResponse> routes(ProfileHandler handler) {
return route(i(GET("/profiles")), handler::all)
.andRoute(i(GET("/profiles/{id}")), handler::getById)
.andRoute(i(DELETE("/profiles/{id}")), handler::deleteById)
.andRoute(i(POST("/profiles")), handler::create)
.andRoute(i(PUT("/profiles/{id}")), handler::updateById);
}
private static RequestPredicate i(RequestPredicate target) {
return new CaseInsensitiveRequestPredicate(target);
}
}
src/main/java/com/example/demo/CaseInsensitiveRequestPredicate.java
package com.example.demo;
import org.springframework.http.server.PathContainer;
import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.support.ServerRequestWrapper;
import java.net.URI;
public class CaseInsensitiveRequestPredicate implements RequestPredicate {
private final RequestPredicate target;
CaseInsensitiveRequestPredicate(RequestPredicate target) {
this.target = target;
}
@Override
public boolean test(ServerRequest request) {
return this.target.test(new LowerCaseUriServerRequestWrapper(request));
}
@Override
public String toString() {
return this.target.toString();
}
}
class LowerCaseUriServerRequestWrapper extends ServerRequestWrapper {
LowerCaseUriServerRequestWrapper(ServerRequest delegate) {
super(delegate);
}
@Override
public URI uri() {
return URI.create(super.uri().toString().toLowerCase());
}
@Override
public String path() {
return uri().getRawPath();
}
@Override
public PathContainer pathContainer() {
return PathContainer.parsePath(path());
}
}
-
Restart and confirm http://localhost:8080/profiles works in your browser and with HTTPie
-
Create
static/ws.html
to show event notifications [webflux-ws
]
src/main/resources/static/ws.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Profile notification client
</title>
</head>
<body>
<script>
var socket = new WebSocket('ws://localhost:8080/ws/profiles');
socket.addEventListener('message', function (event) {
window.alert('message from server: ' + event.data);
});
</script>
</body>
</html>
-
Restore
doOnSuccess
inProfileService
, open in your browser http://localhost:8080/ws.html, and runcreate.sh
in a terminal window
create.sh
#!/bin/bash
port=${1:-8080}
curl -H"content-type: application/json" -d'{"email":"random"}' http://localhost:${port}/profiles
-
Create
ServerSentEventController
[webflux-sse
] and tryhttp :8080/sse/profiles -S
src/main/java/com/example/demo/ServerSentEventController.java
package com.example.demo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
public class ServerSentEventController {
private final Flux<ProfileCreatedEvent> events;
private final ObjectMapper objectMapper;
public ServerSentEventController(ProfileCreatedEventPublisher eventPublisher, ObjectMapper objectMapper) {
this.events = Flux.create(eventPublisher).share();
this.objectMapper = objectMapper;
}
@GetMapping(path = "/sse/profiles", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> profiles() {
return this.events.map(pce -> {
try {
return objectMapper.writeValueAsString(pce);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
});
}
}
Follow the steps below or read Full Stack Reactive with Spring WebFlux, WebSockets, and React for a more thorough tutorial.
-
Run
npx create-react-app react-app --typescript
; view app withnpm start
-
Modify
App.tsx
and addcomponentDidMount()
to fetch profiles [react-fetch
]
src/App.tsx
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
componentDidMount() {
this.setState({isLoading: true});
fetch('//localhost:3000/profiles')
.then(response => response.json())
.then(data => this.setState({profiles: data, isLoading: false}));
}
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
</header>
</div>
);
}
}
export default App;
-
Create interfaces:
Profile
,AppProps
, andAppState
; add constructor and initialize state [react-constructor
]
src/App.tsx
interface Profile {
id: string;
email: string;
}
interface AppProps {
}
interface AppState {
isLoading: boolean;
profiles: Array<Profile>;
}
-
Add constructor and initialize state [
react-constructor
]
src/App.tsx
class App extends Component<AppProps, AppState> {
constructor(props: AppProps) {
super(props);
this.state = {
profiles: [],
isLoading: false
};
}
...
}
-
Change
render()
to show profiles [react-loading
andreact-list
]
src/App.tsx
render() {
const {profiles, isLoading} = this.state;
if (isLoading) {
return <p>Loading...</p>;
}
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<div>
<h2>Profile List</h2>
{profiles.map((profile: Profile) =>
<div key={profile.id}>
{profile.email}
</div>
)}
</div>
</header>
</div>
);
}
-
Configure proxy for React in
package.json
, restart the app, and view the list of profiles"proxy": "http://localhost:8080"
-
Create
ProfileList.tsx
and copy code fromApp.tsx
; changeApp.tsx
to use<ProfileList/>
src/ProfileList.tsx
import React, { Component } from 'react';
interface Profile {
id: string;
email: string;
}
interface ProfileListProps {
}
interface ProfileListState {
isLoading: boolean;
profiles: Array<Profile>;
}
class ProfileList extends Component<ProfileListProps, ProfileListState> {
constructor(props: ProfileListProps) {
super(props);
this.state = {
profiles: [],
isLoading: false
};
}
componentDidMount() {
this.setState({isLoading: true});
fetch('//localhost:3000/profiles')
.then(response => response.json())
.then(data => this.setState({profiles: data, isLoading: false}));
}
render() {
const {profiles, isLoading} = this.state;
if (isLoading) {
return <p>Loading...</p>;
}
return (
<div>
<h2>Profile List</h2>
{profiles.map((profile: Profile) =>
<div key={profile.id}>
{profile.email}
</div>
)}
</div>
);
}
}
export default ProfileList;
-
Modify
ProfileList.tsx
to fetch data every second [react-interval
]
src/ProfileList.tsx
private interval: any;
async fetchData() {
this.setState({isLoading: true});
const response = await fetch('http://localhost:3000/profiles');
const data = await response.json();
this.setState({profiles: data, isLoading: false});
}
async componentDidMount() {
await this.fetchData();
this.interval = setInterval(() => this.fetchData(), 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
-
Create and run
create-stream.sh
create-stream.sh
#!/bin/bash
port=${1:-8080}
count=0
profile () {
((count++))
echo "posting #${count}"
http POST http://localhost:${port}/profiles email="random${count}"
if [ $count -gt 120 ]
then
echo "count is $count, ending..."
break
fi
}
while sleep 1; do profile; done
-
Use RxJS: install rxjs first! [
react-rxjs
]
src/ProfileList.tsx
import { interval } from 'rxjs';
import { startWith, switchMap } from 'rxjs/operators';
...
async componentDidMount() {
this.setState({isLoading: true});
const request = interval(1000).pipe(
startWith(0),
switchMap(async () =>
fetch('http://localhost:3000/profiles')
.then((response) => response.json())
));
request.subscribe(data => {
this.setState({profiles: data, isLoading: false});
})
}
-
Use WebSocket [
react-websocket
]
src/ProfileList.tsx
async componentDidMount() {
this.setState({isLoading: true});
const response = await fetch('http://localhost:3000/profiles');
const data = await response.json();
this.setState({profiles: data, isLoading: false});
const socket = new WebSocket('ws://localhost:3000/ws/profiles');
socket.addEventListener('message', async (event: any) => {
const profile = JSON.parse(event.data);
this.state.profiles.push(profile);
this.setState({profiles: this.state.profiles});
});
}
-
Create
src/setupProxy.js
to set up proxy for WebSockets [react-proxy
] and restart
src/setupProxy.js
const proxy = require("http-proxy-middleware");
module.exports = app => {
app.use(proxy("/ws", {target: "http://localhost:8080", ws: true}))
}
-
Use EventSource with SSE [
react-eventsource
] and restart React app
src/ProfileList.tsx
async componentDidMount() {
this.setState({isLoading: true});
const response = await fetch('http://localhost:3000/profiles');
const data = await response.json();
this.setState({profiles: data, isLoading: false});
const eventSource = new EventSource('http://localhost:8080/sse/profiles');
eventSource.onopen = (event: any) => console.log('open', event);
eventSource.onmessage = (event: any) => {
const profile = JSON.parse(event.data).source;
this.state.profiles.push(profile);
this.setState({profiles: this.state.profiles});
};
eventSource.onerror = (event: any) => console.log('error', event);
}
-
Add
@CrossOrigin
inServerSentEventController
and restart Spring Boot app
-
Add Spring Security OIDC as dependencies [
ss-maven
]
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
-
Create OIDC app in Okta; add properties to
application.yml
[ss-application
]
src/main/resources/application.yml
oidc:
issuer-uri: https://{yourOktaDomain}/oauth2/default
client-id: {yourClientId}
client-secret: {yourClientSecret}
spring:
security:
oauth2:
client:
provider:
okta:
issuer-uri: ${oidc.issuer-uri}
registration:
okta:
client-id: ${oidc.client-id}
client-secret: ${oidc.client-secret}
resourceserver:
jwt:
issuer-uri: ${oidc.issuer-uri}
-
Try to load http://localhost:8080/profiles in your browser, you should be redirected to log in
-
Create
SecurityConfiguration.java
for resource server and CORS [ss-config
]
src/main/java/com/example/demo/SecurityConfiguration.java
package com.example.demo;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsConfigurationSource;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import java.util.Collections;
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfiguration {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
// @formatter:off
return http
.csrf()
.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
.and()
.authorizeExchange()
.pathMatchers("/ws/**").permitAll()
.anyExchange().authenticated()
.and()
.oauth2Login()
.and()
.oauth2ResourceServer()
.jwt().and().and().build();
// @formatter:on
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
configuration.setAllowedMethods(Collections.singletonList("GET"));
configuration.setAllowedHeaders(Collections.singletonList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
-
Add Okta’s React SDK using OktaDev Schematics
npm install @oktadev/schematics schematics @oktadev/schematics:add-auth
-
View changes in
App.tsx
and newHome.tsx
file -
Add
<ProfileList auth={this.props.auth}/>
inHome.tsx
-
Add CSS to
App.css
to make buttons more visible [react-css
].Buttons { margin-top: 10px; }
.Buttons button { font-size: 1em; }
-
Restart, show app and Loading… view error in your developer console
-
Update
ProfileList
to add an authorization header [react-token
]
src/ProfileList.tsx
async componentDidMount() {
this.setState({isLoading: true});
const response = await fetch('http://localhost:8080/profiles', {
headers: {
Authorization: 'Bearer ' + await this.props.auth.getAccessToken()
}
});
const data = await response.json();
this.setState({profiles: data, isLoading: false});
}
-
Modify
WebSocketConfiguration.java
to only return an ID.
src/main/java/com/example/demo/WebSocketConfiguration.java
return session -> {
Flux<WebSocketMessage> messageFlux = publish.map(evt -> {
try {
Profile profile = (Profile) evt.getSource();
Map<String, String> data = new HashMap<>();
data.put("id", profile.getId());
return objectMapper.writeValueAsString(data);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}).map(str -> {
log.info("sending " + str);
return session.textMessage(str);
});
return session.send(messageFlux);
};
-
Modify
ProfileList
to fetch a profile by ID when added. [react-websocket2
]
src/ProfileList.tsx
async componentDidMount() {
this.setState({isLoading: true});
const headers = {
headers: {Authorization: 'Bearer ' + await this.props.auth.getAccessToken()} // (1)
};
const response = await fetch('http://localhost:8080/profiles', headers); // (2)
const data = await response.json();
this.setState({profiles: data, isLoading: false});
const socket = new WebSocket('ws://localhost:8080/ws/profiles');
socket.addEventListener('message', async (event: any) => {
const message = JSON.parse(event.data);
const request = await fetch(`http://localhost:8080/profiles/${message.id}`, headers); // (3)
const profile = await request.json();
this.state.profiles.push(profile);
this.setState({profiles: this.state.profiles});
});
}
-
Create an access token using OIDC Debugger
-
Modify
./create-stream.sh
to use the access token and show profiles being added.
create-stream.sh
#!/bin/bash
port=${1:-8080}
count=0
accessToken=<your access token>
profile () {
((count++))
echo "posting #${count}"
http POST http://localhost:${port}/profiles email="random${count}" "Authorization: Bearer ${accessToken}"
if [ $count -gt 120 ]
then
echo "count is $count, ending..."
break
fi
}
while sleep 1; do profile; done
-
Fini!
Questions or comments? Please send a message to @mraible on Twitter, or ask your question on this blog post.