[英]Handle Security exceptions in Spring Boot Resource Server
如何讓我的自定義ResponseEntityExceptionHandler
或OAuth2ExceptionRenderer
處理純資源服務器上 Spring 安全性引發的異常?
我們實施了一個
@ControllerAdvice
@RestController
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
所以每當資源服務器上出現錯誤時,我們希望它回答
{
"message": "...",
"type": "...",
"status": 400
}
資源服務器使用 application.properties 設置:
security.oauth2.resource.userInfoUri: http://localhost:9999/auth/user
對我們的身份驗證服務器進行身份驗證和授權請求。
但是,任何 spring 安全錯誤將始終繞過我們的異常處理程序
@ExceptionHandler(InvalidTokenException.class)
public ResponseEntity<Map<String, Object>> handleInvalidTokenException(InvalidTokenException e) {
return createErrorResponseAndLog(e, 401);
}
並生產
{
"timestamp": "2016-12-14T10:40:34.122Z",
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/api/templates/585004226f793042a094d3a9/schema"
}
或者
{
"error": "invalid_token",
"error_description": "5d7e4ab5-4a88-4571-b4a4-042bce0a076b"
}
那么如何為資源服務器配置安全異常處理呢? 我所找到的只是關於如何通過實現自定義OAuth2ExceptionRenderer
來自定義OAuth2ExceptionRenderer
驗證服務器的OAuth2ExceptionRenderer
。 但是我找不到將它連接到資源服務器的安全鏈的位置。
我們唯一的配置/設置是這樣的:
@SpringBootApplication
@Configuration
@ComponentScan(basePackages = {"our.packages"})
@EnableAutoConfiguration
@EnableResourceServer
如之前的評論中所述,請求在到達 MVC 層之前被安全框架拒絕,因此@ControllerAdvice
不是此處的選項。
Spring Security 框架中有 3 個接口,您可能對此感興趣:
您可以創建這些接口中的每一個的實現,以便自定義為各種事件發送的響應:成功登錄、失敗登錄、嘗試訪問權限不足的受保護資源。
以下將在登錄嘗試失敗時返回 JSON 響應:
@Component
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler
{
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException ex) throws IOException, ServletException
{
response.setStatus(HttpStatus.FORBIDDEN.value());
Map<String, Object> data = new HashMap<>();
data.put("timestamp", new Date());
data.put("status",HttpStatus.FORBIDDEN.value());
data.put("message", "Access Denied");
data.put("path", request.getRequestURL().toString());
OutputStream out = response.getOutputStream();
com.fasterxml.jackson.databind.ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(out, data);
out.flush();
}
}
您還需要向安全框架注冊您的實現。 在 Java 配置中,這如下所示:
@Configuration
@EnableWebSecurity
@ComponentScan("...")
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
@Override
public void configure(HttpSecurity http) throws Exception
{
http
.addFilterBefore(corsFilter(), ChannelProcessingFilter.class)
.logout()
.deleteCookies("JESSIONID")
.logoutUrl("/api/logout")
.logoutSuccessHandler(logoutSuccessHandler())
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/api/login")
.failureHandler(authenticationFailureHandler())
.successHandler(authenticationSuccessHandler())
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler());
}
/**
* @return Custom {@link AuthenticationFailureHandler} to send suitable response to REST clients in the event of a
* failed authentication attempt.
*/
@Bean
public AuthenticationFailureHandler authenticationFailureHandler()
{
return new RestAuthenticationFailureHandler();
}
/**
* @return Custom {@link AuthenticationSuccessHandler} to send suitable response to REST clients in the event of a
* successful authentication attempt.
*/
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler()
{
return new RestAuthenticationSuccessHandler();
}
/**
* @return Custom {@link AccessDeniedHandler} to send suitable response to REST clients in the event of an attempt to
* access resources to which the user has insufficient privileges.
*/
@Bean
public AccessDeniedHandler accessDeniedHandler()
{
return new RestAccessDeniedHandler();
}
}
如果您正在使用@EnableResourceServer
,您可能還會發現在@Configuration
類中擴展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”標頭。
您無法使用 Spring MVC 異常處理程序注釋(例如@ControllerAdvice
因為 Spring 安全過濾器在 Spring MVC 之前就開始使用了。
如果您使用類似於在 Spring Security Oauth2 中使用 RemoteTokenServices 配置資源服務器的配置使用令牌驗證 URL,它會在未經授權的情況下返回 HTTP 狀態 401:
@Primary
@Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl("https://token-validation-url.com");
tokenService.setTokenName("token");
return tokenService;
}
如其他答案 ( https://stackoverflow.com/a/44372313/5962766 ) 中所述實現自定義authenticationEntryPoint
將不起作用,因為RemoteTokenService使用 400 狀態並為其他狀態(如 401)拋出未處理的異常:
public RemoteTokenServices() {
restTemplate = new RestTemplate();
((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
@Override
// Ignore 400
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400) {
super.handleError(response);
}
}
});
}
所以你需要在RemoteTokenServices
配置中設置自定義RestTemplate
,它可以處理 401 而不拋出異常:
@Primary
@Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl("https://token-validation-url.com");
tokenService.setTokenName("token");
RestOperations restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
@Override
// Ignore 400 and 401
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
super.handleError(response);
}
}
});
}
tokenService.setRestTemplate(restTemplate);
return tokenService;
}
並為HttpComponentsClientHttpRequestFactory添加依賴項:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
OAuth2ExceptionRenderer 用於授權服務器。 正確答案可能會像這篇文章中詳述的那樣處理它(即,忽略它是 oauth 並將其視為任何其他 spring 安全身份驗證機制): https : //stackoverflow.com/a/26502321/5639571
當然,這將捕獲與 oauth 相關的異常(在到達資源端點之前拋出),但是資源端點內發生的任何異常仍然需要 @ExceptionHandler 方法。
我們可以使用這個安全處理程序將處理程序傳遞給 spring mvc @ControllerAdvice
@Component
public class AuthExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler {
private static final Logger LOG = LoggerFactory.getLogger(AuthExceptionHandler.class);
private final HandlerExceptionResolver resolver;
@Autowired
public AuthExceptionHandler(@Qualifier("handlerExceptionResolver") final HandlerExceptionResolver resolver) {
this.resolver = resolver;
}
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
LOG.error("Responding with unauthorized error. Message - {}", authException.getMessage());
resolver.resolveException(request, response, null, authException);
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
LOG.error("Responding with access denied error. Message - {}", accessDeniedException.getMessage());
resolver.resolveException(request, response, null, accessDeniedException);
}
}
然后使用@ControllerAdvice
定義異常,以便我們可以在一個地方管理全局異常處理程序。
這個有可能。 由於最初的問題是針對需要返回自定義 JSON 響應的 REST 控制器,因此我將逐步編寫一個對我有用的完整答案。 首先,似乎您無法使用擴展ControllResponseEntityExceptionHandler
的@ControllerAdvice
處理此問題。 您需要一個單獨的處理程序來擴展AccessDeniedHandler
。 請按照以下步驟操作。
AccessDeniedHandler
的自定義處理程序類@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
private static final String JSON_TYPE = "application/json";
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
MyErrorList errors = new MyErrorList();
errors.addError(new MyError("", "You do not have permission to access this resource."));
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType(JSON_TYPE);
OutputStream output = response.getOutputStream();
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(output, errors);
output.flush();
}
}
上面的“MyError”是一個簡單的 POJO,用於表示錯誤 json 結構,而 MyErrorList 是另一個包含“MyError”列表的 POJO。
@Autowired
private VOMSAccessDeniedHandler accessDeniedHandler;
accessDeniedHandler
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)
使用Step 2和Step 3 ,您的SecurityConfiguration
應如下所示(請注意,我省略了與此問題無關的代碼以縮短此答案的長度):
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private MyAccessDeniedHandler accessDeniedHandler;
// Other stuff
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/register").permitAll()
.antMatchers("/authenticate").permitAll()
.antMatchers("/public").permitAll()
.anyRequest().authenticated()
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse res,
AuthenticationException authException) throws IOException, ServletException {
ApiException ex = new ApiException(HttpStatus.FORBIDDEN, "Invalid Token", authException);
ObjectMapper mapper = new ObjectMapper();
res.setContentType("application/json;charset=UTF-8");
res.setStatus(403);
res.getWriter().write(mapper.writeValueAsString(ex));
}
}
Spring 3.0 以后,您可以使用@ControllerAdvice
(在類級別)並從CustomGlobalExceptionHandler
擴展org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
類
@ExceptionHandler({com.test.CustomException1.class,com.test.CustomException2.class})
public final ResponseEntity<CustomErrorMessage> customExceptionHandler(RuntimeException ex){
return new ResponseEntity<CustomErrorMessage>(new CustomErrorMessage(false,ex.getMessage(),404),HttpStatus.BAD_REQUEST);
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.