@ExceptionHandlerで春のセキュリティ認証例外を処理する


97

私は春のMVCのを使用しています@ControllerAdviceし、@ExceptionHandlerREST APIをすべての例外を処理します。これは、Web MVCコントローラーによってスローされた例外に対しては正常に機能しますが、コントローラーメソッドが呼び出される前に実行されるため、Spring Securityカスタムフィルターによってスローされた例外に対しては機能しません。

トークンベースの認証を行うカスタムSpring Securityフィルターがあります。

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

        }

    }

}

このカスタムエントリポイントの場合:

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

}

そして、このクラスで例外をグローバルに処理します:

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

Spring Security AuthenticationExceptionの場合でも、詳細なJSON本文を返す必要があります。Spring Security AuthenticationEntryPointとSpring MVC @ExceptionHandlerを連携させる方法はありますか?

Spring Security 3.1.4とSpring MVC 3.2.4を使用しています。


9
できません... (@)ExceptionHandlerは、リクエストがによって処理された場合にのみ機能しDispatcherServletます。ただし、この例外はによってスローされるため、その前に発生しFilterます。したがって、この例外をで処理することはできません(@)ExceptionHandler
M. Deinum 2013年

はい、そうです。EntryPointのresponse.sendErrorとともにjson本体を返す方法はありますか?
Nicola

例外をキャッチしてそれに応じて戻るには、チェーンの前半にカスタムフィルターを挿入する必要があるようです。ドキュメントリストフィルタ、そのエイリアスとそれらが適用された順:docs.spring.io/spring-security/site/docs/3.1.4.RELEASE/...
Romski

1
JSONが必要な場所が1つだけの場合は、JSON内でそれを単に構築/書き込みEntryPointます。そこでオブジェクトを作成し、そこにaを挿入するMappingJackson2HttpMessageConverterことができます。
M. Deinum 2013年

@ M.Deinumエントリポイント内にjsonを構築しようとします。
Nicola

回答:


58

OK、私はAuthenticationEntryPointからjsonを自分で書くことを提案したように試してみましたが、うまくいきました。

テストのために、response.sendErrorを削除してAutenticationEntryPointを変更しました

@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() + "\" }");

    }
}

このようにして、Spring Security AuthenticationEntryPointを使用している場合でも、カスタムのJSONデータを無許可の401と共に送信できます。

明らかに、私がテスト目的で行ったようにjsonを構築するのではなく、いくつかのクラスインスタンスをシリアル化します。


3
ジャクソンの使用例:ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(response.getOutputStream()、new FailResponse(401、authException.getLocalizedMessage()、 "Access denied"、 ""));
Cyrusmith、2015年

1
質問が少し古いことは知っていますが、AuthenticationEntryPointをSecurityConfigに登録しましたか?
leventunver 2016

1
:@leventunverここでは、エントリポイント登録する方法を見つけることができstackoverflow.com/questions/24684806/...を
Nicola

37

これは非常に興味深い問題であり、Spring SecuritySpring Webフレームワークは、それらが応答を処理する方法に一貫性がありません。エラーメッセージの処理をMessageConverter簡単にサポートする必要があると思います。

コンテンツネゴシエーションに従ってMessageConverter例外をキャッチし、正しい形式で返すことができるように、Spring Security に注入するエレガントな方法を見つけようとしました。それでも、以下の私の解決策はエレガントではありませんが、少なくともSpringコードを利用します。

JacksonとJAXBライブラリを組み込む方法を知っていると思います。合計3つのステップがあります。

ステップ1-MessageConvertersを格納するスタンドアロンクラスを作成する

このクラスは魔法を演じません。メッセージコンバータとプロセッサを格納するだけですRequestResponseBodyMethodProcessor。マジックはそのプロセッサの内部にあり、コンテンツのネゴシエーションやそれに応じて応答本体を変換するなど、すべての仕事を行います。

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

手順2-AuthenticationEntryPointを作成する

多くのチュートリアルと同様に、このクラスはカスタムエラー処理を実装するために不可欠です。

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

手順3-エントリポイントを登録する

述べたように、私はそれをJava Configで行います。ここでは関連する構成を示していますが、セッションステートレスなどの他の構成があるはずです。

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

いくつかの認証失敗の場合を試してください。リクエストヘッダーにAccept:XXXが含まれている必要があり、JSON、XML、またはその他の形式で例外が発生するはずです。


1
私はキャッチしようとしてInvalidGrantExceptionいますCustomEntryPointが、あなたの私のバージョンは呼び出されていません。私が見逃している可能性のあるアイデアはありますか?
displayname

@表示名。でキャッチすることができないすべての認証例外AuthenticationEntryPointAccessDeniedHandlerなどUsernameNotFoundExceptionInvalidGrantExceptionで処理できるAuthenticationFailureHandlerよう、ここで説明しました
ウィルソン

23

私が見つけた最良の方法は、例外を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);
    }
}

次に、@ ExceptionHandlerを使用して、応答を希望どおりにフォーマットできます。


9
魅力のように機能します。Springが自動ワイヤリングの2つのBean定義があることを示すエラーをスローする場合は、修飾子アノテーションを追加する必要があります。@Autowired @Qualifier( "handlerExceptionResolver")private HandlerExceptionResolver resolver;
Daividh

1
nullハンドラーを渡すことにより@ControllerAdvice、アノテーションにbasePackagesを指定した場合は機能しないことに注意してください。ハンドラーを呼び出せるようにするには、これを完全に削除する必要がありました。
Jarmex、

どうしてあげたの@Component("restAuthenticationEntryPoint")?restAuthenticationEntryPointのような名前が必要なのはなぜですか?春の名前の衝突を避けるためですか?
プログラマ

@Jarmexでは、nullの代わりに、何を渡しましたか?ある種のハンドラーですよね?@ControllerAdviceで注釈が付けられたクラスを渡すだけですか?ありがとう
プログラマ

@theprogrammer、私はそれを回避するためにbasePackagesアノテーションパラメータを削除するためにアプリケーションを少し再構築する必要がありました-理想的ではありません!
Jarmex

5

Spring Bootおよびの場合、Java構成の代わり@EnableResourceServerに拡張し、メソッド内でオーバーライドして使用することでカスタムを登録することは比較的簡単で便利です。ResourceServerConfigurerAdapterWebSecurityConfigurerAdapterAuthenticationEntryPointconfigure(ResourceServerSecurityConfigurer resources)resources.authenticationEntryPoint(customAuthEntryPoint())

このようなもの:

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

OAuth2AuthenticationEntryPoint(最終ではないため)拡張して、カスタムの実装中に部分的に再利用できる素晴らしいものもありますAuthenticationEntryPoint。特に、エラー関連の詳細を含む「WWW-Authenticate」ヘッダーを追加します。

これが誰かを助けることを願っています。


私はこれを試していcommence()ますが、私の機能がAuthenticationEntryPoint呼び出されていません-何か不足していますか?
displayname

4

@Nicolaおよび@Victor Wingから回答を受け取り、より標準化された方法を追加します。

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

}

これで、構成済みのジャクソン、Jaxb、またはMVCアノテーションの応答本文の変換に使用するもの、またはシリアライザ、デシリアライザなどを使用したXMLベースの構成を注入できます。


私は春のブーツに非常に新しいです:「messageConverterオブジェクトをauthenticationEntryポイントに渡す方法」を教えてください
Kona Suresh

セッターを通して。XMLを使用する場合は、<property name="messageConverter" ref="myConverterBeanName"/>タグを作成する必要があります。あなたが使用する場合は@Configuration、クラスを単に使用setMessageConverter()する方法を。
Gabriel Villacis、2018年

4

HandlerExceptionResolverその場合に使用する必要があります。

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

また、オブジェクトを返すには、例外ハンドラクラスを追加する必要があります。

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

あなたがための複数の実装のプロジェクトを実行している時にエラーが出ることがありHandlerExceptionResolver、その場合は、あなたが追加する必要が@Qualifier("handlerExceptionResolver")HandlerExceptionResolver


GenericResponseBean
ジャバポージョ

2

フィルターでメソッド 'unsuccessfulAuthentication'をオーバーライドするだけでそれを処理できました。そこで、必要な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());
    }
}

1

更新:あなたのような直接コードを確認したい場合は、その後、私は2つのあなたのための例、あなたが探しているもの、もう一つは、反応性のWebと反応性のセキュリティと同等のものを使用しているされている標準のSpring Securityを使用して1持っている:
- ノーマルWeb + Jwt Security
- Reactive Jwt

私が常にJSONベースのエンドポイントに使用するものは、次のようになります。

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

Spring Web Starterを追加すると、オブジェクトマッパーはBeanになりますが、私はそれをカスタマイズしたいので、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;
    }

WebSecurityConfigurerAdapterクラスで設定したデフォルトのAuthenticationEntryPoint:

@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

フィルターをカスタマイズして、どのような異常があるかを判断し、これよりも良い方法があるはずです

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

私はobjectMapperを使用しています。すべてのRestサービスは主にjsonで動作しており、構成の1つでオブジェクトマッパーをすでに構成しています。

コードはKotlinで書かれていますが、うまくいけばうまくいきます。

@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

ではResourceServerConfigurerAdapter、クラス、以下のコードは、私のために働い切り取ら。http.exceptionHandling().authenticationEntryPoint(new AuthFailureHandler()).and.csrf()..動作しませんでした。それが、私が別の電話として書いた理由です。

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

トークンの期限切れと欠落している認証ヘッダーをキャッチするためのAuthenticationEntryPointの実装。


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

  }
}

動作しません..それでもデフォルトのメッセージを表示します
aswzen
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.