Saya menggunakan Spring MVC @ControllerAdvice
dan @ExceptionHandler
untuk menangani semua pengecualian dari REST Api. Ini berfungsi dengan baik untuk pengecualian yang dilemparkan oleh pengontrol mvc web tetapi tidak berfungsi untuk pengecualian yang dilemparkan oleh filter khusus keamanan pegas karena mereka berjalan sebelum metode pengontrol dipanggil.
Saya memiliki filter keamanan pegas khusus yang melakukan autentikasi berbasis token:
public class AegisAuthenticationFilter extends GenericFilterBean {
...
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
try {
...
} catch(AuthenticationException authenticationException) {
SecurityContextHolder.clearContext();
authenticationEntryPoint.commence(request, response, authenticationException);
}
}
}
Dengan titik masuk khusus ini:
@Component("restAuthenticationEntryPoint")
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint{
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
}
}
Dan dengan kelas ini untuk menangani pengecualian secara global:
@ControllerAdvice
public class RestEntityResponseExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({ InvalidTokenException.class, AuthenticationException.class })
@ResponseStatus(value = HttpStatus.UNAUTHORIZED)
@ResponseBody
public RestError handleAuthenticationException(Exception ex) {
int errorCode = AegisErrorCode.GenericAuthenticationError;
if(ex instanceof AegisException) {
errorCode = ((AegisException)ex).getCode();
}
RestError re = new RestError(
HttpStatus.UNAUTHORIZED,
errorCode,
"...",
ex.getMessage());
return re;
}
}
Yang perlu saya lakukan adalah mengembalikan badan JSON mendetail bahkan untuk keamanan musim semi AuthenticationException. Apakah ada cara agar keamanan pegas AuthenticationEntryPoint dan pegas mvc @ExceptionHandler berfungsi bersama?
Saya menggunakan keamanan pegas 3.1.4 dan pegas mvc 3.2.4.
sumber
(@)ExceptionHandler
hanya akan berfungsi jika permintaan ditangani olehDispatcherServlet
. Namun pengecualian ini terjadi sebelum itu karena dilemparkan oleh aFilter
. Jadi, Anda tidak akan pernah bisa menangani pengecualian ini dengan file(@)ExceptionHandler
.EntryPoint
. Anda mungkin ingin membangun objek di sana, dan memasukkan aMappingJackson2HttpMessageConverter
di sana.Jawaban:
Oke, saya mencoba seperti yang disarankan menulis json sendiri dari AuthenticationEntryPoint dan berhasil.
Hanya untuk pengujian, saya mengubah AutenticationEntryPoint dengan menghapus response.sendError
@Component("restAuthenticationEntryPoint") public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint{ public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException { response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getOutputStream().println("{ \"error\": \"" + authenticationException.getMessage() + "\" }"); } }
Dengan cara ini Anda dapat mengirim data json khusus bersama dengan 401 yang tidak sah meskipun Anda menggunakan Spring Security AuthenticationEntryPoint.
Jelas Anda tidak akan membangun json seperti yang saya lakukan untuk tujuan pengujian tetapi Anda akan membuat serial beberapa contoh kelas.
sumber
Ini adalah masalah yang sangat menarik karena kerangka kerja Spring Security dan Spring Web tidak cukup konsisten dalam cara mereka menangani respons. Saya percaya itu harus secara native mendukung penanganan pesan kesalahan dengan
MessageConverter
cara yang praktis.Saya mencoba menemukan cara elegan untuk menyuntikkan
MessageConverter
ke Keamanan Musim Semi sehingga mereka dapat menangkap pengecualian dan mengembalikannya dalam format yang tepat sesuai dengan negosiasi konten . Tetap saja, solusi saya di bawah ini tidak elegan tetapi setidaknya gunakan kode Spring.Saya berasumsi Anda tahu cara memasukkan perpustakaan Jackson dan JAXB, jika tidak, tidak ada gunanya melanjutkan. Ada total 3 Langkah.
Langkah 1 - Buat kelas mandiri, menyimpan MessageConverters
Kelas ini tidak memainkan sihir. Ini hanya menyimpan konverter pesan dan prosesor
RequestResponseBodyMethodProcessor
. Keajaiban ada di dalam prosesor yang akan melakukan semua pekerjaan termasuk negosiasi konten dan mengubah badan respons yang sesuai.public class MessageProcessor { // Any name you like // List of HttpMessageConverter private List<HttpMessageConverter<?>> messageConverters; // under org.springframework.web.servlet.mvc.method.annotation private RequestResponseBodyMethodProcessor processor; /** * Below class name are copied from the framework. * (And yes, they are hard-coded, too) */ private static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", MessageProcessor.class.getClassLoader()); private static final boolean jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", MessageProcessor.class.getClassLoader()) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", MessageProcessor.class.getClassLoader()); private static final boolean gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", MessageProcessor.class.getClassLoader()); public MessageProcessor() { this.messageConverters = new ArrayList<HttpMessageConverter<?>>(); this.messageConverters.add(new ByteArrayHttpMessageConverter()); this.messageConverters.add(new StringHttpMessageConverter()); this.messageConverters.add(new ResourceHttpMessageConverter()); this.messageConverters.add(new SourceHttpMessageConverter<Source>()); this.messageConverters.add(new AllEncompassingFormHttpMessageConverter()); if (jaxb2Present) { this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); } if (jackson2Present) { this.messageConverters.add(new MappingJackson2HttpMessageConverter()); } else if (gsonPresent) { this.messageConverters.add(new GsonHttpMessageConverter()); } processor = new RequestResponseBodyMethodProcessor(this.messageConverters); } /** * This method will convert the response body to the desire format. */ public void handle(Object returnValue, HttpServletRequest request, HttpServletResponse response) throws Exception { ServletWebRequest nativeRequest = new ServletWebRequest(request, response); processor.handleReturnValue(returnValue, null, new ModelAndViewContainer(), nativeRequest); } /** * @return list of message converters */ public List<HttpMessageConverter<?>> getMessageConverters() { return messageConverters; } }
Langkah 2 - Buat AuthenticationEntryPoint
Seperti di banyak tutorial, kelas ini penting untuk mengimplementasikan penanganan error kustom.
public class CustomEntryPoint implements AuthenticationEntryPoint { // The class from Step 1 private MessageProcessor processor; public CustomEntryPoint() { // It is up to you to decide when to instantiate processor = new MessageProcessor(); } @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { // This object is just like the model class, // the processor will convert it to appropriate format in response body CustomExceptionObject returnValue = new CustomExceptionObject(); try { processor.handle(returnValue, request, response); } catch (Exception e) { throw new ServletException(); } } }
Langkah 3 - Daftarkan titik masuk
Seperti yang disebutkan, saya melakukannya dengan Java Config. Saya baru saja menunjukkan konfigurasi yang relevan di sini, harus ada konfigurasi lain seperti sesi stateless , dll.
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling().authenticationEntryPoint(new CustomEntryPoint()); } }
Coba dengan beberapa kasus kegagalan otentikasi, ingat header permintaan harus menyertakan Terima: XXX dan Anda harus mendapatkan pengecualian dalam JSON, XML atau beberapa format lain.
sumber
InvalidGrantException
tetapi versi saya AndaCustomEntryPoint
tidak dipanggil. Tahu apa yang bisa saya lewatkan?AuthenticationEntryPoint
danAccessDeniedHandler
sepertiUsernameNotFoundException
danInvalidGrantException
dapat ditanganiAuthenticationFailureHandler
seperti yang dijelaskan di sini .Cara terbaik yang saya temukan adalah mendelegasikan pengecualian ke HandlerExceptionResolver
@Component("restAuthenticationEntryPoint") public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Autowired private HandlerExceptionResolver resolver; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { resolver.resolveException(request, response, null, exception); } }
lalu Anda dapat menggunakan @ExceptionHandler untuk memformat respons seperti yang Anda inginkan.
sumber
@ControllerAdvice
tidak akan berfungsi jika Anda telah menentukan basePackages pada anotasi. Saya harus menghapus ini seluruhnya untuk memungkinkan pawang dipanggil.@Component("restAuthenticationEntryPoint")
? Mengapa perlu nama seperti restAuthenticationEntryPoint? Apakah untuk menghindari tabrakan nama Spring?Dalam kasus Spring Boot dan
@EnableResourceServer
, itu relatif mudah dan nyaman untuk memperluasResourceServerConfigurerAdapter
daripadaWebSecurityConfigurerAdapter
di konfigurasi Java dan mendaftarkan customAuthenticationEntryPoint
dengan menimpaconfigure(ResourceServerSecurityConfigurer resources)
dan menggunakanresources.authenticationEntryPoint(customAuthEntryPoint())
di dalam metode.Sesuatu seperti ini:
@Configuration @EnableResourceServer public class CommonSecurityConfig extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.authenticationEntryPoint(customAuthEntryPoint()); } @Bean public AuthenticationEntryPoint customAuthEntryPoint(){ return new AuthFailureHandler(); } }
Ada juga nice
OAuth2AuthenticationEntryPoint
yang bisa diperpanjang (karena ini belum final) dan sebagian digunakan kembali saat mengimplementasikan customAuthenticationEntryPoint
. Secara khusus, ia menambahkan header "WWW-Authenticate" dengan detail terkait kesalahan.Semoga ini bisa membantu seseorang.
sumber
commence()
fungsi sayaAuthenticationEntryPoint
tidak dipanggil - apakah saya melewatkan sesuatu?Mengambil jawaban dari @Nicola dan @Victor Wing dan menambahkan cara yang lebih standar:
import org.springframework.beans.factory.InitializingBean; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class UnauthorizedErrorAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean { private HttpMessageConverter messageConverter; @SuppressWarnings("unchecked") @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { MyGenericError error = new MyGenericError(); error.setDescription(exception.getMessage()); ServerHttpResponse outputMessage = new ServletServerHttpResponse(response); outputMessage.setStatusCode(HttpStatus.UNAUTHORIZED); messageConverter.write(error, null, outputMessage); } public void setMessageConverter(HttpMessageConverter messageConverter) { this.messageConverter = messageConverter; } @Override public void afterPropertiesSet() throws Exception { if (messageConverter == null) { throw new IllegalArgumentException("Property 'messageConverter' is required"); } } }
Sekarang, Anda dapat memasukkan Jackson, Jaxb yang dikonfigurasi atau apa pun yang Anda gunakan untuk mengonversi badan respons pada anotasi MVC Anda atau konfigurasi berbasis XML dengan serializers, deserializers, dan sebagainya.
sumber
<property name="messageConverter" ref="myConverterBeanName"/>
tag. Saat Anda menggunakan@Configuration
kelas, cukup gunakansetMessageConverter()
metode.Kami perlu menggunakan
HandlerExceptionResolver
dalam kasus itu.@Component public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint { @Autowired //@Qualifier("handlerExceptionResolver") private HandlerExceptionResolver resolver; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { resolver.resolveException(request, response, null, authException); } }
Juga, Anda perlu menambahkan kelas penangan pengecualian untuk mengembalikan objek Anda.
@RestControllerAdvice public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(AuthenticationException.class) public GenericResponseBean handleAuthenticationException(AuthenticationException ex, HttpServletResponse response){ GenericResponseBean genericResponseBean = GenericResponseBean.build(MessageKeys.UNAUTHORIZED); genericResponseBean.setError(true); response.setStatus(HttpStatus.UNAUTHORIZED.value()); return genericResponseBean; } }
mungkin Anda mendapatkan error pada saat menjalankan proyek karena beberapa implementasi dari
HandlerExceptionResolver
, Dalam hal ini Anda harus menambahkan@Qualifier("handlerExceptionResolver")
padaHandlerExceptionResolver
sumber
GenericResponseBean
hanyalah java pojo, semoga Anda dapat membuatnya sendiriSaya dapat mengatasinya hanya dengan mengganti metode 'unsuccessfulAuthentication' di filter saya. Di sana, saya mengirim respons kesalahan ke klien dengan kode status HTTP yang diinginkan.
@Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { if (failed.getCause() instanceof RecordNotFoundException) { response.sendError((HttpServletResponse.SC_NOT_FOUND), failed.getMessage()); } }
sumber
Pembaruan: Jika Anda suka dan lebih suka melihat kode secara langsung, maka saya punya dua contoh untuk Anda, satu menggunakan Keamanan Musim Semi standar yang Anda cari, yang lain menggunakan yang setara dengan Web Reaktif dan Keamanan Reaktif:
- Normal Keamanan Web + Jwt
- Jwt Reaktif
Yang selalu saya gunakan untuk titik akhir berbasis JSON saya terlihat seperti berikut:
@Component public class JwtAuthEntryPoint implements AuthenticationEntryPoint { @Autowired ObjectMapper mapper; private static final Logger logger = LoggerFactory.getLogger(JwtAuthEntryPoint.class); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { // Called when the user tries to access an endpoint which requires to be authenticated // we just return unauthorizaed logger.error("Unauthorized error. Message - {}", e.getMessage()); ServletServerHttpResponse res = new ServletServerHttpResponse(response); res.setStatusCode(HttpStatus.UNAUTHORIZED); res.getServletResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); res.getBody().write(mapper.writeValueAsString(new ErrorResponse("You must authenticated")).getBytes()); } }
Pemeta objek menjadi kacang setelah Anda menambahkan starter web musim semi, tetapi saya lebih suka menyesuaikannya, jadi inilah implementasi saya untuk ObjectMapper:
@Bean public Jackson2ObjectMapperBuilder objectMapperBuilder() { Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); builder.modules(new JavaTimeModule()); // for example: Use created_at instead of createdAt builder.propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); // skip null fields builder.serializationInclusion(JsonInclude.Include.NON_NULL); builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); return builder; }
AuthenticationEntryPoint default yang Anda setel di kelas WebSecurityConfigurerAdapter:
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { // ............ @Autowired private JwtAuthEntryPoint unauthorizedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable() .authorizeRequests() // .antMatchers("/api/auth**", "/api/login**", "**").permitAll() .anyRequest().permitAll() .and() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler) .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.headers().frameOptions().disable(); // otherwise H2 console is not available // There are many ways to ways of placing our Filter in a position in the chain // You can troubleshoot any error enabling debug(see below), it will print the chain of Filters http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); } // .......... }
sumber
Sesuaikan filter, dan tentukan jenis kelainan apa, harus ada metode yang lebih baik dari ini
public class ExceptionFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { String msg = ""; try { filterChain.doFilter(request, response); } catch (Exception e) { if (e instanceof JwtException) { msg = e.getMessage(); } response.setCharacterEncoding("UTF-8"); response.setContentType(MediaType.APPLICATION_JSON.getType()); response.getWriter().write(JSON.toJSONString(Resp.error(msg))); return; } }
}
sumber
Saya menggunakan objectMapper. Setiap Layanan Istirahat sebagian besar bekerja dengan json, dan di salah satu konfigurasi Anda, Anda telah mengonfigurasi pemeta objek.
Kode ditulis di Kotlin, semoga akan baik-baik saja.
@Bean fun objectMapper(): ObjectMapper { val objectMapper = ObjectMapper() objectMapper.registerModule(JodaModule()) objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) return objectMapper } class UnauthorizedAuthenticationEntryPoint : BasicAuthenticationEntryPoint() { @Autowired lateinit var objectMapper: ObjectMapper @Throws(IOException::class, ServletException::class) override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) { response.addHeader("Content-Type", "application/json") response.status = HttpServletResponse.SC_UNAUTHORIZED val responseError = ResponseError( message = "${authException.message}", ) objectMapper.writeValue(response.writer, responseError) }}
sumber
Di
ResourceServerConfigurerAdapter
kelas, kode di bawah ini bekerja untuk saya.http.exceptionHandling().authenticationEntryPoint(new AuthFailureHandler()).and.csrf()..
tidak bekerja. Itu sebabnya saya menulisnya sebagai panggilan terpisah.public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.exceptionHandling().authenticationEntryPoint(new AuthFailureHandler()); http.csrf().disable() .anonymous().disable() .authorizeRequests() .antMatchers(HttpMethod.OPTIONS).permitAll() .antMatchers("/subscribers/**").authenticated() .antMatchers("/requests/**").authenticated(); }
Implementasi AuthenticationEntryPoint untuk mengetahui kedaluwarsa token dan header otorisasi yang hilang.
public class AuthFailureHandler implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json"); httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); if( e instanceof InsufficientAuthenticationException) { if( e.getCause() instanceof InvalidTokenException ){ httpServletResponse.getOutputStream().println( "{ " + "\"message\": \"Token has expired\"," + "\"type\": \"Unauthorized\"," + "\"status\": 401" + "}"); } } if( e instanceof AuthenticationCredentialsNotFoundException) { httpServletResponse.getOutputStream().println( "{ " + "\"message\": \"Missing Authorization Header\"," + "\"type\": \"Unauthorized\"," + "\"status\": 401" + "}"); } } }
sumber