Tangani pengecualian autentikasi keamanan pegas dengan @ExceptionHandler


101

Saya menggunakan Spring MVC @ControllerAdvicedan @ExceptionHandleruntuk 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.


9
Anda tidak dapat ... Ini (@)ExceptionHandlerhanya akan berfungsi jika permintaan ditangani oleh DispatcherServlet. Namun pengecualian ini terjadi sebelum itu karena dilemparkan oleh a Filter. Jadi, Anda tidak akan pernah bisa menangani pengecualian ini dengan file (@)ExceptionHandler.
M. Deinum

Oke, kamu benar. Apakah ada cara untuk mengembalikan badan json bersama dengan response.sendError dari EntryPoint?
Nicola

Sepertinya Anda perlu memasukkan filter khusus sebelumnya dalam rangkaian untuk menangkap Pengecualian dan mengembalikannya sesuai. Dokumentasi mencantumkan filter, aliasnya, dan urutan penerapannya: docs.spring.io/spring-security/site/docs/3.1.4.RELEASE/…
Romski

1
Jika satu-satunya lokasi Anda memerlukan JSON maka cukup buat / tulis di dalam file EntryPoint. Anda mungkin ingin membangun objek di sana, dan memasukkan a MappingJackson2HttpMessageConverterdi sana.
M. Deinum

@ M.Deinum Saya akan mencoba membangun json di dalam titik masuk.
Nicola

Jawaban:


64

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.


3
Contoh menggunakan Jackson: ObjectMapper mapper = new ObjectMapper (); mapper.writeValue (response.getOutputStream (), FailResponse baru (401, authException.getLocalizedMessage (), "Akses ditolak", ""));
Cyrusmith

1
Saya tahu pertanyaannya agak lama, tetapi apakah Anda mendaftarkan AuthenticationEntryPoint ke SecurityConfig?
leventunver

1
@leventunver Di sini Anda dapat menemukan cara mendaftarkan titik masuk: stackoverflow.com/questions/24684806/… .
Nicola

37

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 MessageConvertercara yang praktis.

Saya mencoba menemukan cara elegan untuk menyuntikkan MessageConverterke 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.


1
Saya mencoba menangkap InvalidGrantExceptiontetapi versi saya Anda CustomEntryPointtidak dipanggil. Tahu apa yang bisa saya lewatkan?
Stefan Falk

@nama tampilan. Semua pengecualian otentikasi yang tidak dapat ditangkap oleh AuthenticationEntryPoint dan AccessDeniedHandlerseperti UsernameNotFoundExceptiondan InvalidGrantExceptiondapat ditangani AuthenticationFailureHandlerseperti yang dijelaskan di sini .
Wilson

26

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.


10
Bekerja seperti pesona. Jika Spring menampilkan kesalahan yang mengatakan ada 2 definisi kacang untuk autowirering, Anda harus menambahkan penjelasan qualifier: @Autowired @Qualifier ("handlerExceptionResolver") private HandlerExceptionResolver resolver;
Daividh

2
Ketahuilah bahwa dengan meneruskan penangan null, Anda @ControllerAdvicetidak akan berfungsi jika Anda telah menentukan basePackages pada anotasi. Saya harus menghapus ini seluruhnya untuk memungkinkan pawang dipanggil.
Jarmex

Mengapa Anda memberi @Component("restAuthenticationEntryPoint")? Mengapa perlu nama seperti restAuthenticationEntryPoint? Apakah untuk menghindari tabrakan nama Spring?
programmer

@Jarmex Jadi di tempat null, apa yang Anda lulus? itu semacam penangan, kan? Haruskah saya melewatkan kelas yang telah dianotasi dengan @ControllerAdvice? Terima kasih
programmer

@ pemrogram, saya harus sedikit merestrukturisasi aplikasi untuk menghapus parameter anotasi basePackages untuk menyiasatinya - tidak ideal!
Jarmex

5

Dalam kasus Spring Boot dan @EnableResourceServer, itu relatif mudah dan nyaman untuk memperluas ResourceServerConfigurerAdapterdaripada WebSecurityConfigurerAdapterdi konfigurasi Java dan mendaftarkan custom AuthenticationEntryPointdengan menimpa configure(ResourceServerSecurityConfigurer resources)dan menggunakan resources.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 OAuth2AuthenticationEntryPointyang bisa diperpanjang (karena ini belum final) dan sebagian digunakan kembali saat mengimplementasikan custom AuthenticationEntryPoint. Secara khusus, ia menambahkan header "WWW-Authenticate" dengan detail terkait kesalahan.

Semoga ini bisa membantu seseorang.


Saya mencoba ini tetapi commence()fungsi saya AuthenticationEntryPointtidak dipanggil - apakah saya melewatkan sesuatu?
Stefan Falk

4

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.


Saya sangat baru dalam boot musim semi: tolong beri tahu saya "cara meneruskan objek messageConverter ke titik authenticationEntry"
Kona Suresh

Melalui setter. Saat Anda menggunakan XML, Anda harus membuat <property name="messageConverter" ref="myConverterBeanName"/>tag. Saat Anda menggunakan @Configurationkelas, cukup gunakan setMessageConverter()metode.
Gabriel Villacis

4

Kami perlu menggunakan HandlerExceptionResolverdalam 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


GenericResponseBeanhanyalah java pojo, semoga Anda dapat membuatnya sendiri
Vinit Solanki

2

Saya 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());
    }
}

1

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);
    }
// ..........
}

1

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

}


0

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)
     }}

0

Di ResourceServerConfigurerAdapterkelas, 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"
              + "}");
    }

  }
}


tidak berfungsi .. masih menampilkan pesan default
aswzen
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.