我正在使用Spring MVC的@ControllerAdvice
和@ExceptionHandler
来处理REST Api的所有异常。它适用于Web mvc控制器抛出的异常,但它不适用于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主体,即使是Spring安全性AuthenticationException也是如此。有没有办法让spring security AuthenticationEntryPoint和spring mvc @ExceptionHandler一起工作?
我正在使用spring security 3.1.4和spring mvc 3.2.4。
答案 0 :(得分:42)
好的,我按照建议尝试从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,但你会序列化一些类实例。
答案 1 :(得分:32)
这是一个非常有趣的问题, Spring Security 和 Spring Web 框架在处理响应的方式上并不完全一致。我相信它必须以一种方便的方式本地支持使用MessageConverter
处理错误消息。
我试图找到一种优雅的方式将MessageConverter
注入Spring Security,以便他们可以捕获异常并根据内容协商以正确的格式返回它们。不过,我的解决方案并不优雅,但至少可以使用Spring代码。
我假设您知道如何包含Jackson和JAXB库,否则没有必要继续。总共有3个步骤。
这门课没有任何魔力。它只存储消息转换器和处理器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;
}
}
与许多教程一样,此类对于实现自定义错误处理至关重要。
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();
}
}
}
如上所述,我使用Java Config进行操作。我只是在这里显示相关配置,应该有其他配置,例如session 无状态等。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().authenticationEntryPoint(new CustomEntryPoint());
}
}
尝试使用某些身份验证失败的情况,请记住请求标头应包含接受:XXX ,您应该以JSON,XML或其他格式获取异常。
答案 2 :(得分:15)
我发现的最好方法是将异常委托给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以您希望的方式格式化响应。
答案 3 :(得分:4)
在Spring Boot和@EnableResourceServer
的情况下,在Java配置中扩展ResourceServerConfigurerAdapter
而不是WebSecurityConfigurerAdapter
并通过覆盖注册自定义AuthenticationEntryPoint
相对简单方便configure(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
时会被部分重用。特别是,它增加了&#34; WWW-Authenticate&#34;标题包含与错误相关的详细信息。
希望这会对某人有所帮助。
答案 4 :(得分:3)
从@Nicola和@Victor Wing获取答案并添加更标准化的方式:
<script>
document.write("JS is enabled!");
</script>
<noscript>JS is disabled !</noscript>
现在,您可以使用其序列化程序,反序列化程序等注入已配置的Jackson,Jaxb或您用于转换MVC注释或基于XML的配置上的响应主体的任何内容。
答案 5 :(得分:2)
在这种情况下,我们需要使用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
答案 6 :(得分:1)
我能够通过简单地在过滤器中覆盖方法“ unsuccessfulAuthentication”来处理该问题。在那里,我使用所需的HTTP状态代码向客户端发送错误响应。
public class World {
private Map<String, Player> players = new HashMap<>();
public Collection<Player> getPlayers() { ... }
public Optional<Player> getPlayer(String nickname) { ... }
// ...
}
答案 7 :(得分: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;
}
}
}
答案 8 :(得分:0)
我正在使用objectMapper。每个Rest Service主要使用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)
}}
答案 9 :(得分:0)
更新:如果您喜欢并且更喜欢直接看代码,那么我为您提供了两个示例,一个示例使用的是您正在寻找的标准Spring Security,另一个示例使用的是等效的响应式Web和响应式安全性:
-Normal 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);
}
// ..........
}
答案 10 :(得分: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"
+ "}");
}
}
}
答案 11 :(得分:0)
您可以使用 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();