Spring security 3.1如何处理不同的认证异常?

Posted

技术标签:

【中文标题】Spring security 3.1如何处理不同的认证异常?【英文标题】:How to handle different authentication exceptions in Spring security 3.1? 【发布时间】:2013-11-04 23:40:55 【问题描述】:

首先我想评论一下,我已经检查了 Stack Overflow 中的其他问题,并根据答案实施了我自己的方法:https://***.com/a/14425801/2487263 和 https://***.com/a/16101649/2487263

我正在尝试在 Spring 3.2、Spring MVC 应用程序中使用 Spring security 3.1 保护 REST API,我正在使用具有简单配置的基本身份验证方法:

<http create-session="stateless" entry-point-ref="authenticationFailedEntryPoint">
    <intercept-url pattern="/**" access="ROLE_USER"/>
    <http-basic />
</http>

如您所见,我正在使用自定义入口点,我有自己的 ErrorResponse 对象,我将以 json 格式添加到 http 响应中,请参见下面的代码:

    @Component
public class AuthenticationFailedEntryPoint implements AuthenticationEntryPoint 
    static Logger log = Logger.getLogger(AuthenticationFailedEntryPoint.class);

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException 
        log.error(ExceptionUtils.getStackTrace(authException));
        ErrorResponse errorResponse = new ErrorResponse();  

            ... here I fill my errorResponse object ...

        ObjectMapper jsonMapper = new ObjectMapper();

        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(status); 

        PrintWriter out = response.getWriter();
        out.print(jsonMapper.writeValueAsString(errorResponse));   
    

我用两个测试用例尝试了这种方法:

    尝试在不提供基本身份验证标头的情况下使用一项服务:

这是请求/响应:

GET http://localhost:8081/accounts/accounts?accountNumber=1013


 -- response --
401 Unauthorized
Server:  Apache-Coyote/1.1

Content-Type:  application/json;charset=UTF-8

Content-Length:  320

Date:  Fri, 25 Oct 2013 17:11:15 GMT

Proxy-Connection:  Keep-alive

"status":401,"messages":["code":"000011","message":"You are not authorized to reach this endpoint"]

2.- 尝试使用相同的服务,但现在发送带有错误密码的基本身份验证标头:

这是请求/响应:

GET http://localhost:8081/accounts/accounts?accountNumber=1013
Authorization: Basic bXl1c2VyOmdvb2RieWU=


 -- response --
401 Unauthorized
Server:  Apache-Coyote/1.1

WWW-Authenticate:  Basic realm="Spring Security Application"

Content-Type:  text/html;charset=utf-8

Content-Length:  1053

Date:  Fri, 25 Oct 2013 17:03:09 GMT

Proxy-Connection:  Keep-alive

<html> ... ugly html generated by tc server ... </html>

正如您在第一种情况下看到的那样,已到达入口点,并在正确处理异常的情况下执行了开始方法,并返回了 json 响应。但是当密码错误时不会发生这种情况。

在日志中我发现两种情况都产生了不同的流程:

对于案例 1(无 auth 标头):

...
2013-10-25 13:11:15,830 DEBUG tomcat-http--13 org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Secure object: FilterInvocation: URL: /accounts?accountNumber=1013; Attributes: [ROLE_USER]
2013-10-25 13:11:15,830 DEBUG tomcat-http--13 org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
2013-10-25 13:11:15,830 DEBUG tomcat-http--13 org.springframework.security.access.vote.AffirmativeBased - Voter: org.springframework.security.access.vote.RoleVoter@11da1f99, returned: -1
2013-10-25 13:11:15,831 DEBUG tomcat-http--13 org.springframework.security.access.vote.AffirmativeBased - Voter: org.springframework.security.access.vote.AuthenticatedVoter@7507ef7, returned: 0
2013-10-25 13:11:15,831 DEBUG tomcat-http--13 org.springframework.security.web.access.ExceptionTranslationFilter - Access is denied (user is anonymous); redirecting to authentication entry point
org.springframework.security.access.AccessDeniedException: Access is denied
...

案例2(密码错误):

...
2013-10-25 13:03:08,941 DEBUG tomcat-http--11 org.springframework.security.web.authentication.www.BasicAuthenticationFilter - Basic Authentication Authorization header found for user 'myuser'
2013-10-25 13:03:08,941 DEBUG tomcat-http--11 org.springframework.security.authentication.ProviderManager - Authentication attempt using org.springframework.security.authentication.dao.DaoAuthenticationProvider
2013-10-25 13:03:09,544 DEBUG tomcat-http--11 org.springframework.security.authentication.dao.DaoAuthenticationProvider - Authentication failed: password does not match stored value
2013-10-25 13:03:09,545 DEBUG tomcat-http--11 org.springframework.security.web.authentication.www.BasicAuthenticationFilter - Authentication request for failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials
2013-10-25 13:00:30,136 DEBUG tomcat-http--9 org.springframework.security.web.context.SecurityContextPersistenceFilter - SecurityContextHolder now cleared, as request processing completed
...

第一种情况抛出一个 AccessDeniedException,它被捕获并发送到我的入口点中的开始方法,但第二种情况抛出一个 BadCredentialsException 不会进入入口点。

奇怪的是这里的开始方法应该收到一个AuthenticationException,但AccessDeniedException不是AuthenticationException而是BadCredentialsException,请参阅Spring security 3.1 API文档中的继承树:

java.lang.Object
  extended by java.lang.Throwable
      extended by java.lang.Exception
          extended by java.lang.RuntimeException
              extended by org.springframework.security.access.AccessDeniedException

java.lang.Object
  extended by java.lang.Throwable
      extended by java.lang.Exception
          extended by java.lang.RuntimeException
              extended by org.springframework.security.core.AuthenticationException
                  extended by org.springframework.security.authentication.BadCredentialsException

为什么调用 begin 方法时出现类型不正确的异常,以及为什么在有类型正确的 BadCredentialsException 时不调用?

已编辑 --- 由@Luke 实现了答案

所描述的两个解决方案使用问题中显示的自定义 AuthenticationEntryPoint,需要修改配置,选择以下两个选项之一:

    添加自定义 BASIC_AUTH_FILTER:

    <http create-session="stateless" entry-point-ref="authenticationFailedEntryPoint">
        <intercept-url pattern="/**" access="ROLE_USER"/>
        <custom-filter position="BASIC_AUTH_FILTER" ref="authenticationFilter" /> 
    </http>
    
    <beans:bean id="authenticationFilter" class="org.springframework.security.web.authentication.www.BasicAuthenticationFilter">
        <beans:constructor-arg name="authenticationManager" ref="authenticationManager" />
        <beans:constructor-arg name="authenticationEntryPoint" ref="authenticationFailedEntryPoint" />
    </beans:bean>
    

    或将入口点添加到 http-basic 元素,IMO 是最干净的解决方案:

    <http create-session="stateless" entry-point-ref="authenticationFailedEntryPoint">
        <intercept-url pattern="/**" access="ROLE_USER"/>
        <http-basic entry-point-ref="authenticationFailedEntryPoint" />
    </http>
    

【问题讨论】:

关于问题末尾评论的异常问题,似乎 AccesDeniedException 正在由 ExceptionTranlationFilter 转换为 AuthenticationException。这就是调用自定义入口点中的开始方法的原因 raspacorp - 尽管我们有@ExceptionalHandler,但如何执行“开始”方法? @pashtika 这是我大约两年前遇到的一个问题,通过使用该自定义入口点已解决。我记得异常处理程序没有捕获该异常,因为它发生在早期步骤中。但是我不知道这在框架的最新版本中是否已修复,在这种情况下,只需添加适当的异常处理程序就可以了。 【参考方案1】:

我认为问题在于,在基本身份验证失败后,直接从BasicAuthenticationFilter 调用入口点,默认情况下将是内置实现。

您还需要设置 entry-point-ref attribute on the http-basic 元素来解决此问题。

或者,您可以将基本身份验证过滤器定义为 bean 并完全避免命名空间。

【讨论】:

以上是关于Spring security 3.1如何处理不同的认证异常?的主要内容,如果未能解决你的问题,请参考以下文章

Spring Security登录成功后如何处理用户信息

如何处理 spring security + angular 中的 401 错误?

如何处理 grails spring-security-rest 插件中的自定义身份验证异常?

如何处理具有相同实体 ID 的两个 IDP

Spring MVC系列Spring MVC中请求转发中出现异常如何处理

Spring Data REST CORS - 如何处理预检选项请求?