收到 Spring MVC AccessDeniedException 500 错误,而不是 @PreAuthorized 未授权请求的自定义 401 错误

Posted

技术标签:

【中文标题】收到 Spring MVC AccessDeniedException 500 错误,而不是 @PreAuthorized 未授权请求的自定义 401 错误【英文标题】:Spring MVC AccessDeniedException 500 error received instead of custom 401 error for @PreAuthorized unauth requests 【发布时间】:2017-09-19 03:38:22 【问题描述】:

我正在编写一个 Java Spring MVC 4 REST 应用程序,它将位于前端设备(网站、移动应用程序等)和数据库之间。我在下面的代码将为每个请求创建一个新会话(因为 REST 是无状态的),查看请求的 Authorization 标头,并确认令牌有效并且请求已通过身份验证。

当用户在没有有效令牌的情况下请求安全方法时,我希望将未经授权的请求从 500 Access Is Denied 消息重定向到 401 Unauthorized 消息。

这是我目前所拥有的。

AccessDeniedHandler:

public class Unauthorized401AccessDeniedHandler implements AccessDeniedHandler 
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException)
            throws IOException, ServletException 

        response.setStatus(401);
    

WebSecurityConfigurerAdapter:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter 
    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .exceptionHandling()
                .accessDeniedHandler(new Unauthorized401AccessDeniedHandler());

    

过滤器:

public class SecurityFilter implements Filter 
    final static Logger logger = Logger.getLogger(SecurityFilter.class);

    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain chain) throws IOException, ServletException 

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        HttpSession session = request.getSession();

        String requestUri = request.getRequestURI();

        session.invalidate();
        SecurityContextHolder.clearContext();

        session = request.getSession(true); // create a new session
        SecurityContext ctx = SecurityContextHolder.createEmptyContext();


        boolean isLoggedIn = false;

        String token = null;
        String authorizationHeader = request.getHeader("authorization");
        if(authorizationHeader != null && authorizationHeader.startsWith("bearer")) 
            String encryptedToken = authorizationHeader.split(" ")[1];
            token = StringUtils.newStringUtf8(Base64.decodeBase64(encryptedToken));

            // confirm user is logged in and authorized here TBD

            isLoggedIn = true;
        

        PreAuthenticatedAuthenticationToken authentication = null;
        if(isLoggedIn) 
            SessionCredentialsModel authRequestModel = new SessionCredentialsModel();
            authRequestModel.employeeId = 323;
            authRequestModel.firstName = "Danny";
            authRequestModel.lastName = "Boy";
            authRequestModel.token = "this_is_a_test_token";

            authentication = new PreAuthenticatedAuthenticationToken(authRequestModel, token);
         else 
            authentication = new PreAuthenticatedAuthenticationToken(new SessionCredentialsModel(), null);
        

        authentication.setAuthenticated(true);
        ctx.setAuthentication(authentication);

        SecurityContextHolder.setContext(ctx);
        chain.doFilter(req, res);
    

安全模型(又名安全上下文主体):

public class SessionCredentialsModel 
    public int employeeId;
    public String firstName;
    public String lastName;
    public String token;

    public boolean isAuthenticated() 
        if(employeeId > 0 && token != null) 
            return true;
        

        return false;
    

最后是控制器:

  @RequestMapping(value = "/", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
    @PreAuthorize("principal.isAuthenticated()")
    public ResponseEntity<LoginResponseModel> create() 
        LoginResponseModel responseModel = new LoginResponseModel();
        responseModel.statusCode = 55;
        responseModel.token = "authorized model worked!";

        return new ResponseEntity<LoginResponseModel>(responseModel, HttpStatus.OK);
    

当我在没有 Authorization 标头的情况下运行该方法时,我收到了这个错误(而不是我想要得到的错误):

HTTP Status 500 - Request processing failed; nested exception is org.springframework.security.access.AccessDeniedException: Access is denied

type Exception report

message Request processing failed; nested exception is org.springframework.security.access.AccessDeniedException: Access is denied

description The server encountered an internal error that prevented it from fulfilling this request.

exception

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.security.access.AccessDeniedException: Access is denied
    org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:982)
    org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:648)
    org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
    org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    net.pacificentertainment.middletier.app.security.SecurityFilter.doFilter(SecurityFilter.java:73)
root cause

org.springframework.security.access.AccessDeniedException: Access is denied
    org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84)
    org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233)
    org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:65)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:656)
    net.pacificentertainment.middletier.app.controllers.EmployeeController$$EnhancerBySpringCGLIB$$b6765b64.create(<generated>)
    sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    java.lang.reflect.Method.invoke(Method.java:498)
    org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
    org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133)
    org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:116)
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827)
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738)
    org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
    org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:963)
    org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:897)
    org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
    org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:648)
    org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
    org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    net.pacificentertainment.middletier.app.security.SecurityFilter.doFilter(SecurityFilter.java:73)
note The full stack trace of the root cause is available in the Apache Tomcat/8.0.43 logs.

Apache Tomcat/8.0.43

我不知道为什么我无法获得返回 401 或除 500 之外的任何其他状态代码的未经授权的请求。

你怎么看?

【问题讨论】:

【参考方案1】:

好的,伙计们,无法从社区获得任何帮助,但我确实找到了解决方案——尽管它不是直接的解决方案。

@ControllerAdvice
public class SecurityExceptionHandler extends ResponseEntityExceptionHandler 

    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<Object> handleAccessDeniedException(Exception ex, WebRequest request) 
        if(ex.getMessage().toLowerCase().indexOf("access is denied") > -1) 
            return new ResponseEntity<Object>("Unauthorized Access", new HttpHeaders(), HttpStatus.UNAUTHORIZED);
        

        return new ResponseEntity<Object>(ex.getMessage(), new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
    

我的应用程序中的这个新文件将允许我控制异常期间发生的情况。现在我可以手动检查问题,看看它是否是“访问被拒绝”,然后重定向到 401 确实有效。上面的问题是重定向到 401 的代码从未被命中。这段代码确实被执行了。

同样,这不是一个直接的解决方案,因为我们正在操纵不同的 Spring MVC 和一种黑客默认行为以使其工作。

如果有人有更优雅的解决方案,请发布。

【讨论】:

【参考方案2】:

此错误运行是因为 java 的 AuthenticationEntryPoint 可以使用 500 状态覆盖或映射错误。

你可以用下面的代码解决这个问题


@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler implements AuthenticationEntryPoint  

 @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException ) throws IOException, ServletException     
      response.setContentType(MediaType.APPLICATION_JSON_VALUE);
      response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
      response.getOutputStream().println(" \"error\": \"" + authException.getMessage() + "\" ");
    

    @ExceptionHandler(value =  AccessDeniedException.class )
    public void commence(HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex ) throws IOException 
      response.setContentType(MediaType.APPLICATION_JSON_VALUE);
      response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
      response.getOutputStream().println(" \"error\": \"" + ex.getMessage() + "\" ");
    

使用此代码,您可以捕获 AccessDeniedException,这是由 @PreAuthorize 注释引发的

【讨论】:

【参考方案3】:

我还得到了一个 HTTP 500,而我期望一个 403。对我来说,这是 Spring 5 中 Spring Security 配置错误的结果。我将过滤器链配置为允许对 /pages 端点的所有请求。

http.csrf().disable()
               .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilterBefore(preAuthFilter, BasicAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers("/pages/**", "/sessions/").permitAll()
                .anyRequest()
                .authenticated();

同时,我在同一个/pages 端点上添加了@PreAuthorize 注释。

@PreAuthorize("hasAuthority('ORG_ADMIN')")
@GetMapping("/pages/uniqueUrlName")
public ResponseEntity<Page> page(@PathVariable("uniqueUrlName") String uniqueUrlName) 
    // implementation here

这会导致 org.springframework.security.web.access.ExceptionTranslationFilter 被跳过,然后 AccessDeniedException 作为常规应用程序异常处理,在我的例子中,通过 @ControllerAdvice 类中的 @ExceptionHandler 方法处理。

删除 permitAll() 配置解决了该问题。我现在在使用未经身份验证的请求访问端点时看到 403。

【讨论】:

【参考方案4】:

我假设您没有使用 Spring Boot - 可能是您没有在容器中注册 springSecurityFilterChain 吗?

堆栈跟踪没有显示任何 Spring Security 过滤器被调用。您的自定义AccessDeniedHandler 将在位于springSecurityFilterChain 中的ExceptionTranslationFilter 中调用。但似乎您的方法安全拦截器正在抛出一个不会在任何地方被捕获的AccessDeniedException

查看reference guide。

【讨论】:

以上是关于收到 Spring MVC AccessDeniedException 500 错误,而不是 @PreAuthorized 未授权请求的自定义 401 错误的主要内容,如果未能解决你的问题,请参考以下文章

Spring mvc 发现不明确的映射。无法映射控制器 bean 方法

购物车 Spring mvc

Spring MVC - HttpMediaTypeNotAcceptableException

Spring MVC + Hibernate 4 + Spring Security

网页未找到。 Spring MVC + 码头

JSON - Spring MVC:如何将 json 数据发布到 Spring MVC 控制器