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.
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.
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.
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<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;
}
}
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();
}
}
}
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.
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.
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.
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.
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
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());
}
}
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)
}}
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:
@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); } // .......... } ``