使用@ExceptionHandler 处理春季安全性身份验证异常

我使用 Spring MVC 的 @ControllerAdvice@ExceptionHandler来处理 REST Api 之外的所有异常。它对 web mvc 控制器引发的异常可以正常工作,但对 Spring 安全自定义过滤器引发的异常无效,因为它们在调用控制器方法之前运行。

我有一个自定义的 Spring 安全过滤器,它执行基于令牌的认证:

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

我需要做的是返回一个详细的 JSON 主体,即使是针对春季安全 AuthenticationException。有没有办法让 spring security AuthenticationEntryPoint 和 spring mvc@ExceptionHandler 一起工作?

我用的是弹簧安全3.1.4和弹簧 mvc 3.2.4。

180719 次浏览

好的,我按照建议自己在 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,但是您会序列化一些类实例。

在 Spring 引导中,应该将其添加到 SecurityConfiguration 文件的 http.enticationEntryPoint ()部分。

这是一个非常有趣的问题,春天保安公司弹簧网框架在处理响应的方式上并不完全一致。我相信它必须以一种方便的方式支持 MessageConverter的错误消息处理。

我试图找到一种将 MessageConverter注入 Spring Security 的优雅方法,这样他们就可以捕获异常和 根据内容协商,以正确的格式返回它们。尽管如此,我下面的解决方案并不优雅,但至少利用了 Spring 代码。

我假设您知道如何包含 Jackson 和 JAXB 库,否则就没有继续下去的必要了。一共有三个步骤。

步骤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-注册入口点

正如前面提到的,我使用 JavaConfig 来完成。我只是在这里展示了相关配置,应该还有其他配置,比如会话 无国籍等等。

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

尝试处理一些身份验证失败的情况,请记住请求头应该包含 接受: XXX,并且应该以 JSON、 XML 或其他格式获得异常。

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


}

现在,您可以注入已配置的 Jackson、 Jaxb 或任何您用来在 MVC 注释或基于 XML 的配置上转换响应体的东西,以及它的序列化器、反序列化器等等。

对于 Spring Boot 和 @EnableResourceServer,在 Java 配置中扩展 ResourceServerConfigurerAdapter而不是 WebSecurityConfigurerAdapter,并通过重写 configure(ResourceServerSecurityConfigurer resources)并在方法中使用 resources.authenticationEntryPoint(customAuthEntryPoint())来注册定制的 AuthenticationEntryPoint,这是相对容易和方便的。

就像这样:

@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”报头和与错误相关的详细信息。

希望这能帮到别人。

我发现的最佳方法是将异常委托给 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 按照您希望的方式格式化响应。

我正在使用 objectMapper。每个 RestService 主要使用 json,并且在您的一个配置中已经配置了一个对象映射器。

代码是在 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)
}}

在这种情况下,我们需要使用 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的多个实现而得到一个错误,在这种情况下,必须在 HandlerExceptionResolver上添加 @Qualifier("handlerExceptionResolver")

我可以通过简单地覆盖过滤器中的方法‘ unSuccess fulAuthentication’来处理这个问题。在这里,我向客户端发送一个错误响应,其中包含所需的 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());
}
}

更新: 如果你喜欢并且更愿意直接看到代码,那么我有两个例子给你,一个使用标准的 Spring Security,这正是你所寻找的,另一个是使用相当于 Reactive Web 和 Reactive Security:
正常网络 + Jwt 安全
- = “ nofollow noReferrer”> Reactive Jwt-= “ http://github.com/melardev/JavaSpringBootRxJwtSecurityRxCrud”rel = “ nofollow noReferrer”> 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);
}
// ..........
}

自定义过滤器,并确定哪种异常,应该有一个比这更好的方法

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

}

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


}
}


可以改为使用 objectMapper 来写入值

ApiError response = new ApiError(HttpStatus.UNAUTHORIZED);
String message = messageSource.getMessage("errors.app.unauthorized", null, httpServletRequest.getLocale());
response.setMessage(message);
httpServletResponse.setContentType("application/json");
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
OutputStream out = httpServletResponse.getOutputStream();
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(out, response);
out.flush();

我只创建一个类来处理与身份验证有关的所有异常

@ 组件 公共类 JwtAuthenticationEntryPoint 实现 AuthenticationEntryPoint {

private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
logger.error("Responding with unauthorized error. Message - {}", e.getMessage());
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
}

}

如果您需要一个超级快速的解决方案,@ Christophe Bornet的目的是最简单的一个。

  1. 创建一个 Bean 将身份验证异常发送到异常解析器。
    @Bean(name = "restAuthenticationEntryPoint")
public AuthenticationEntryPoint authenticationEntryPoint(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
return (request, response, exception) -> resolver.resolveException(request, response, null, exception);
}

* 您可以将这个 bean 放在现有的安全配置类中的某个位置。

  1. 添加一个异常处理程序方法以捕获错误,这样您就可以返回所需的响应和状态。
    @ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<String> handleValidationException(AccessDeniedException e) {
return ResponseEntity.status(401).body("{\"status\":\"FAILED\", \"reason\": \"Unauthorized\"}");
}

* 您可以将它放在控制器中的 auth 端点附近。