Shouts, reshouts and real-time chat on a fully stateless security chain.

A social website with a full auth system. Users post Shouts with likes, reshouts, quotes and comments, follow each other, chat over WebSocket, and manage their accounts — avatar, banner, bio, location.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// Reads the frontend URL(s) from the environment variable (e.g. set on Railway).
// Supports a comma-separated list for multiple origins (production + preview deployments).
// Falls back to localhost for local development.
@Value("${FRONTEND_URL:http://localhost:3000}")
private String frontendUrl;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception {
http
.cors(Customizer.withDefaults())
.authorizeHttpRequests(
auth -> auth.requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/ws/**").permitAll()
.anyRequest().authenticated())
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
// Split by comma to support multiple origins, trim whitespace from each entry
List<String> allowedOrigins = Stream.of(frontendUrl.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.toList();
CorsConfiguration configuration = new CorsConfiguration();
// setAllowedOriginPatterns works correctly with allowCredentials(true)
// and supports wildcards like https://*.vercel.app
configuration.setAllowedOriginPatterns(allowedOrigins);
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true); // Required for JWT cookies
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}