
Introduction
Summary: I’m documenting this so that I can refer back to it in the future. Recently, I’ve been working with Spring WebFlux, and although I’ve previously used JWTs, this is my first time writing about them. I often forget the implementation details when switching between languages or frameworks, so I’m recording this while it’s still fresh in my mind.
JSON Web Tokens (JWTs) are a compact way to prove a user’s identity between two parties—usually a browser and your API.
Token life-cycle
A JWT is self-contained: it carries the user’s ID, roles, and an expiry inside the token itself.
To balance security and convenience I use two clocks:
Clock | What it limits | Typical value | What happens when it hits zero |
---|---|---|---|
Access expiry | How long the current token stays valid | 15–30 min | Client must call /auth/refresh |
Refresh expiry | How long the refresh token can mint new access tokens | 7–30 days | User must log in again |
Flow:
/login ──► access + refresh token
│
(before access expiry)
▼
/refresh ──► new access token
After the refresh expiry passes, even the refresh endpoint refuses to issue new tokens—forcing a fresh login.
Building blocks
- Model – database tables for users & login history
- Repository – Reactive CRUD interfaces provided by Spring Data R2DBC
- Service – business logic (password checks, audit log)
- Auth manager – validates credentials or JWT claims
- Filter – extracts the Authorization: Bearer … header
- Security config – wires it all together
Essential code
Entity Classes
These classes represent database tables as Java objects using Spring Data R2DBC. @Table
maps the class to a table, and Lombok annotations (@Data
, @Builder
, etc.) generate getters, setters, constructors, and a builder.
@Table("users")
@Data @Builder @NoArgsConstructor @AllArgsConstructor
public class UserEntity {
@Id private Long id;
private String username;
private String password; // hashed
private String roles; // e.g. "USER,ADMIN"
private boolean enabled;
}
@Table("login_history")
@Data @Builder @NoArgsConstructor @AllArgsConstructor
public class LoginHistory {
@Id private Long id;
private Long userId;
private LocalDateTime loginTime;
private String ipAddress;
private boolean successful;
}
Repositories
Spring Data’s ReactiveCrudRepository
provides reactive CRUD operations. By extending these interfaces, Spring auto-generates the implementation at runtime.
public interface UserRepository
extends ReactiveCrudRepository<UserEntity, Long> {
Mono<UserEntity> findByUsername(String username);
}
public interface LoginHistoryRepository
extends ReactiveCrudRepository<LoginHistory, Long> {}
JWT filter (excerpt)
ServerAuthenticationConverter
is a Spring Security WebFlux interface for converting HTTP requests into Authentication
objects. In this converter, we extract and validate the JWT token from the Authorization header.
@Slf4j
public class JwtAuthConverter implements ServerAuthenticationConverter {
@Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
String header = exchange.getRequest()
.getHeaders()
.getFirst(HttpHeaders.AUTHORIZATION);
if (header == null || !header.startsWith("Bearer ")) return Mono.empty();
try {
String token = header.substring(7);
Jws<Claims> claims = JwtUtil.validateTokenAndGetClaims(token);
String username = claims.getBody().getSubject();
List<String> roles = claims.getBody().get("roles", List.class);
List<GrantedAuthority> authorities = roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return Mono.just(new UsernamePasswordAuthenticationToken(
username, null, authorities));
} catch (Exception ex) {
log.warn("Invalid token: {}", ex.getMessage());
return Mono.empty();
}
}
}
Security configuration (simplified)
The @Configuration
class defines beans for Spring’s application context. @EnableWebFluxSecurity
activates WebFlux security support. We configure a SecurityWebFilterChain
bean to set up CSRF, CORS, endpoint permissions, and insert our JWT filter.
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
JwtAuthWebFilter jwt = new JwtAuthWebFilter(new JwtReactiveAuthManager());
jwt.setServerAuthenticationConverter(new JwtAuthConverter());
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.cors(ServerHttpSecurity.CorsSpec::disable)
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.exceptionHandling(e -> e.authenticationEntryPoint(
(exch, ex) -> {
exch.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exch.getResponse().setComplete();
}))
.authorizeExchange(ex -> ex
.pathMatchers(
"/api/v1/auth/login",
"/api/v1/auth/refresh",
"/api/v1/auth/validate",
"/actuator/**")
.permitAll()
.pathMatchers("/api/v1/task/**").authenticated())
.addFilterAt(jwt, SecurityWebFiltersOrder.AUTHENTICATION)
.build();
}
}
Reactive authentication manager
Implementing ReactiveAuthenticationManager
allows custom authentication logic in WebFlux. This manager checks credentials, handles disabled accounts, and records login attempts via the UserService
.
@Component
@RequiredArgsConstructor
public class DatabaseReactiveAuthManager implements ReactiveAuthenticationManager {
private final UserService users;
@Override
public Mono<Authentication> authenticate(Authentication auth) {
String username = auth.getName();
String rawPwd = auth.getCredentials().toString();
return users.findByUsername(username)
.switchIfEmpty(Mono.error(new BadCredentialsException("User not found")))
.flatMap(u -> {
if (!users.checkPassword(rawPwd, u.getPassword()))
return users.recordLoginAttempt(u, false, null)
.then(Mono.error(new BadCredentialsException("Bad password")));
if (!u.isEnabled())
return Mono.error(new DisabledException("Account disabled"));
List<GrantedAuthority> roles = Arrays.stream(u.getRoles().split(","))
.map(String::trim)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return users.recordLoginAttempt(u, true, null)
.thenReturn(new UsernamePasswordAuthenticationToken(
username, null, roles));
});
}
}
Configuration properties
These YAML properties configure Spring Security’s JWT support, specifying the signing secret and token lifetimes.
security:
encryption:
secret: _"encryption_secret"_
jwt:
expiration-time: __1_440_000__ # 24 minutes (ms)
refresh-expiration-time: __604_800_000__ # 7 days (ms)
Tip – generate a strong secret:
# This one-off command uses Docker to generate a bcrypt hash for a random 32-byte secret
docker run --rm python:3.9-slim \
python - <<'PY'
import bcrypt, os
print(bcrypt.hashpw(os.urandom(32), bcrypt.gensalt()).decode())
PY
Recap
- Two expiries keep sessions short yet user-friendly.
- A reactive filter parses the token and attaches authorities.
- A database auth manager handles login, password checks, and audit logs.
- Spring WebFlux security is glued together with one concise
SecurityWebFilterChain
.
References
Personal Update
Setup JioFiber at home, disconnected Airtel in Chennai, and ported all my SIMs to Jio except mom’s. Jio offers unlimited data (4G/5G) on individual plans for $8.77 per month. Nowadays I don’t have enough time to do anything. No no no…not gonna start a rant here…lol. Talked to a friend about old WiFi hacking days…(more in the next few posts).