Escenario
Por exigencias de un tercero debemos adaptar nuestro servicio web para que sea compatible con autenticación por JWT. Hasta ahora las llamadas al API se hacían desde dentro de la aplicación usando las mismas credenciales del usuario autenticado por cookie. La configuración de la cadena de filtros de Spring Security es la siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .headers() .frameOptions() .disable() .addHeaderWriter(new StaticHeadersWriter("Content-Security-Policy", "frame-ancestors 'self'")) .and() .csrf() .requireCsrfProtectionMatcher(new AndRequestMatcher(CsrfFilter.DEFAULT_CSRF_MATCHER, new RegexRequestMatcher("^(?!/api/)", null))) .and() .authorizeRequests() .antMatchers("/").fullyAuthenticated() .antMatchers("/client/**").hasAnyAuthority("CLIENT", "SUPER_ADMIN", "ADMIN") .antMatchers("/admin/**").hasAnyAuthority("SUPER_ADMIN", "ADMIN") .antMatchers("/actuator/**").hasAnyAuthority("SUPER_ADMIN", "ADMIN") .antMatchers("/api/auth/**").permitAll() .antMatchers("/api/**").hasAnyAuthority("ADMIN", "SUPER_ADMIN", "CLIENT", "API_USER") .and() .formLogin() .loginPage("/user/login") .successHandler(successHandler()) .failureUrl("/user/login?error=true") .defaultSuccessUrl("/") .usernameParameter("username") .and() .logout() .logoutRequestMatcher(new AntPathRequestMatcher("/user/logout")) .deleteCookies("remember-me") .logoutSuccessUrl("/user/login") .and() .rememberMe() .userDetailsService(userDetailsService); return http.build(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Bean public SecurityFilterChain jwtFilterChain(HttpSecurity http) throws Exception { http.cors().and().csrf().disable() .antMatcher("/api/**") // This configuration will only be active if the Authorization header is present in the request .requestMatcher(new RequestHeaderRequestMatcher("Authorization")).addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class) .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .anyRequest().authenticated(); http.authenticationProvider(authenticationProvider()); return http.build(); } |
Problema
«En mi máquina funciona». Clásica excusa… Hasta que lo desplegué en un servidor remoto. Entonces, de manera sistemática, la petición con el token era redirigida a /user/login. Después de un buen rato dando palos de ciego me decido a activar trazas en el jar desplegado en el servidor, añadiendo la siguiente clave en application.properties: logging.level.org.springframework.security.web.FilterChainProxy=DEBUG. Al hacer la petición se registra la siguiente configuración:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Security filter chain: [ DisableEncodeUrlFilter WebAsyncManagerIntegrationFilter SecurityContextPersistenceFilter HeaderWriterFilter CsrfFilter LogoutFilter UsernamePasswordAuthenticationFilter RequestCacheAwareFilter SecurityContextHolderAwareRequestFilter RememberMeAuthenticationFilter AnonymousAuthenticationFilter SessionManagementFilter ExceptionTranslationFilter FilterSecurityInterceptor ] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Security filter chain: [ DisableEncodeUrlFilter WebAsyncManagerIntegrationFilter SecurityContextPersistenceFilter HeaderWriterFilter CorsFilter LogoutFilter AuthTokenFilter RequestCacheAwareFilter SecurityContextHolderAwareRequestFilter AnonymousAuthenticationFilter SessionManagementFilter ExceptionTranslationFilter FilterSecurityInterceptor ] |
Solución
Gracias a una issue en GitHub, caigo en la cuenta de que, a pesar de lo que haya podido leer en StackOverflow o en algún otro blog, el orden no ha de ser especificado a nivel de la clase, sino del Bean.
1 2 3 |
@Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain jwtFilterChain(HttpSecurity http) throws Exception |
1 2 3 4 |
@Bean @Order(Ordered.LOWEST_PRECEDENCE) public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |