Menangani semi keamanan otentikasi pengecualian dengan @ExceptionHandler

I'm menggunakan Spring MVC's @ControllerAdvice dan @ExceptionHandler untuk menangani semua pengecualian dari SISA Api. Itu bekerja dengan baik untuk pengecualian dilemparkan oleh web mvc controller tapi itu tidak bekerja untuk pengecualian dilemparkan oleh musim semi keamanan filter kustom karena mereka jalankan sebelum controller metode yang dipanggil.

Saya punya kebiasaan semi keamanan filter yang tidak berdasarkan token auth:

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 kebiasaan ini entry point:

@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 ini kelas 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;
    }
}

Apa yang saya perlu lakukan adalah untuk kembali rinci JSON tubuh bahkan untuk musim semi keamanan AuthenticationException. Apakah ada cara membuat musim semi keamanan AuthenticationEntryPoint dan spring mvc @ExceptionHandler bekerja bersama-sama?

I'm menggunakan semi keamanan 3.1.4 dan spring mvc 3.2.4.

Mengomentari pertanyaan (7)
Larutan

Ok, saya coba seperti yang disarankan menulis json diri dari AuthenticationEntryPoint dan bekerja.

Hanya untuk pengujian aku berubah AutenticationEntryPoint dengan menghapus respon.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 kustom data json bersama dengan 401 yang tidak sah bahkan jika anda menggunakan Semi Keamanan AuthenticationEntryPoint.

Jelas anda tidak akan membangun json seperti yang saya lakukan untuk tujuan pengujian, tetapi anda akan membuat beberapa kelas contoh.

Komentar (3)

Ini adalah masalah yang sangat menarik yang musim Semi Keamanan dan Spring Web kerangka ini tidak cukup konsisten dalam cara mereka menangani respon. Saya percaya hal ini untuk mendukung penanganan pesan kesalahan dengan MessageConverter dalam cara yang berguna.

Saya mencoba untuk menemukan cara yang elegan untuk menyuntikkan MessageConverter ke musim Semi Keamanan sehingga mereka bisa menangkap pengecualian dan mengembalikan mereka dalam format yang tepat sesuai dengan isi negosiasi. Masih, saya larutan di bawah ini yang tidak elegan tapi setidaknya membuat penggunaan musim Semi kode.

Saya berasumsi bahwa anda tahu cara untuk memasukkan Jackson dan JAXB perpustakaan, jika tidak, tidak ada gunanya untuk melanjutkan. Ada 3 Langkah dalam total.

Langkah 1 - Buat mandiri kelas, menyimpan MessageConverters

Kelas ini tidak memainkan sihir. Itu hanya menyimpan pesan konverter dan prosesor RequestResponseBodyMethodProcessor. Sihir adalah dalam bahwa prosesor yang akan melakukan semua pekerjaan yang termasuk konten negosiasi dan mengubah respon tubuh yang sesuai.

public class MessageProcessor { // Any name you like
    // List of HttpMessageConverter
    private List> 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>();

        this.messageConverters.add(new ByteArrayHttpMessageConverter());
        this.messageConverters.add(new StringHttpMessageConverter());
        this.messageConverters.add(new ResourceHttpMessageConverter());
        this.messageConverters.add(new SourceHttpMessageConverter());
        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> getMessageConverters() {
        return messageConverters;
    }
}

Langkah 2 - Membuat AuthenticationEntryPoint

Seperti di banyak tutorial, kelas ini sangat penting untuk menerapkan custom penanganan kesalahan.

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 - Daftar entry point

Seperti yang disebutkan, saya melakukannya dengan Jawa Config. Saya hanya menunjukkan relevan konfigurasi di sini, harus ada konfigurasi lainnya seperti sesi kewarganegaraan, dll.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling().authenticationEntryPoint(new CustomEntryPoint());
    }
}

Cobalah dengan beberapa otentikasi gagal kasus, ingat header permintaan harus mencakup: Menerima : XXX dan anda harus mendapatkan pengecualian dalam JSON, XML atau format lainnya.

Komentar (1)

Cara terbaik I've ditemukan adalah untuk mendelegasikan pengecualian untuk 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);
    }
}

kemudian anda dapat menggunakan @ExceptionHandler untuk format respon dengan cara yang anda inginkan.

Komentar (2)

Dalam kasus musim Semi Boot dan @EnableResourceServer, hal ini relatif mudah dan nyaman untuk memperpanjang ResourceServerConfigurerAdapter bukan WebSecurityConfigurerAdapter di Jawa konfigurasi dan daftar kustom AuthenticationEntryPoint dengan meng-override mengkonfigurasi(ResourceServerSecurityConfigurer sumber daya) dan menggunakan sumber daya.authenticationEntryPoint(customAuthEntryPoint()) 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's juga bagus OAuth2AuthenticationEntryPoint yang dapat diperpanjang (karena itu's tidak final) dan sebagian digunakan kembali sementara menerapkan custom AuthenticationEntryPoint. Secara khusus, ia menambahkan "WWW-Authenticate" header dengan kesalahan-rincian yang terkait.

Berharap ini akan membantu seseorang.

Komentar (1)

Mengambil jawaban dari @Nicola dan @Victor Sayap dan menambahkan lebih banyak cara 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 menyuntikkan dikonfigurasi Jackson, Jaxb atau apapun yang anda gunakan untuk mengkonversi respon tubuh pada MVC penjelasan atau XML berdasarkan konfigurasi dengan serializers, deserializers dan sebagainya.

Komentar (2)

Kita 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 handler pengecualian kelas untuk kembali objek.

@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;
    }
}

semoga anda mendapatkan error pada saat menjalankan proyek karena beberapa implementasi dari HandlerExceptionResolver, Dalam hal ini anda harus menambahkan @Qualifier("handlerExceptionResolver") on HandlerExceptionResolver

Komentar (1)

Saya mampu untuk mengatasinya hanya dengan meng-override metode 'unsuccessfulAuthentication' di filter saya. Di sana, saya mengirim sebuah kesalahan respon kepada klien dengan yang diinginkan kode status HTTP.

@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());
    }
}
Komentar (0)

I'm menggunakan objectMapper. Setiap Istirahat Layanan ini sebagian besar bekerja dengan json, dan di salah satu konfigurasi anda telah dikonfigurasi objek mapper.

Kode ini ditulis dalam Kotlin, mudah-mudahan itu akan menjadi ok.

@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)
     }}
Komentar (0)

Update: Jika anda suka dan lebih memilih untuk melihat kode secara langsung, maka saya memiliki dua contoh untuk anda, salah satunya dengan menggunakan standar Keamanan musim Semi yang adalah apa yang anda cari, yang lain adalah menggunakan setara dengan Reaktif Web dan Reaktif Keamanan:

  • Web Normal + Jwt Keamanan
  • Reaktif Jwt

    Salah satu yang saya selalu gunakan untuk JSON berbasis endpoint terlihat seperti berikut: ``jawa @Komponen public class JwtAuthEntryPoint mengimplementasikan AuthenticationEntryPoint {

@Autowired ObjectMapper mapper;

private static final Logger-logger = LoggerFactory.getLogger(JwtAuthEntryPoint.class);

@Override public void dimulai(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { // Memanggil ketika pengguna mencoba untuk mengakses sebuah endpoint yang membutuhkan untuk dapat disahkan // kita hanya kembali unauthorizaed logger.kesalahan("kesalahan yang tidak Sah. Pesan - {", e.getMessage());

ServletServerHttpResponse res = new ServletServerHttpResponse(response); res.setStatusCode(HttpStatus.Tidak SAH); res.getServletResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); res.getBody().menulis(mapper.writeValueAsString(baru ErrorResponse("Anda harus dikonfirmasi")).getBytes()); } } ``

Objek mapper menjadi bean setelah anda menambahkan spring web starter, tapi saya lebih memilih untuk menyesuaikan, jadi di sini adalah saya implementasi untuk ObjectMapper: ``jawa @Bean publik Jackson2ObjectMapperBuilder objectMapperBuilder() { Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); pembangun.modul(baru JavaTimeModule());

// contoh: Menggunakan created_at bukan createdAt pembangun.propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);

// skip null bidang pembangun.serializationInclusion(JsonInclude.Sertakan.NON_NULL); pembangun.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); kembali builder; } Default AuthenticationEntryPoint anda tetapkan dalam WebSecurityConfigurerAdapter kelas: jawa @Konfigurasi @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig meluas WebSecurityConfigurerAdapter { // ............ @Autowired pribadi JwtAuthEntryPoint unauthorizedHandler; @Override protected void mengkonfigurasi(HttpSecurity http) throws Exception { http.cors().dan().csrf().menonaktifkan() .authorizeRequests() // .antMatchers("/api/auth", "/api/login", "**").permitAll() .anyRequest().permitAll() .dan() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler) .dan() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

http.header().frameOptions().menonaktifkan(); // jika tidak H2 konsol tidak tersedia // Ada banyak cara untuk cara menempatkan Filter kami dalam posisi dalam rantai // Anda dapat memecahkan setiap kesalahan mengaktifkan debug(lihat di bawah), itu akan mencetak rantai Filter http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); } // .......... } ``

Komentar (0)