【问题标题】:How to avoid Spring Data Rest throwing HTTP 400 when an AccessDeniedException occurs on a controller当控制器上发生 AccessDeniedException 时如何避免 Spring Data Rest 抛出 HTTP 400
【发布时间】:2017-07-28 08:20:51
【问题描述】:

我有一个简单的一对多关系的 spring 数据休息应用程序。

(1 个组织包含零个或多个员工)。

在没有任何安全措施的情况下,我可以像这样将员工添加到组织中:

curl -v -H "Content-Type:application/json" -d '{"name":"name1","organization":"http://localhost:8080/api/organizations/1"}'  http://localhost:8080/api/employee

当员工有效负载被推送到休息控制器时,Spring Data Rest 会将组织 uri 转换为组织实体,将其连接到员工,一切正常。

但是,假设我开始保护我的组织 API,并且我只想允许授权用户查看组织(基于某些业务逻辑)。

@RepositoryRestResource
public interface OrganizationController extends CrudRepository<Organization, Long> {

    @PreAuthorize("@securityService.isAllowedToSeeTheOrganization(#id)")
    Organization findOne(@P("id") Long id);

}

SecurityService 实现了一些业务逻辑,以确定用户是否能够根据其个人资料检索组织:

@Service
public class SecurityService {

    public boolean isAllowedToSeeOrganization(Long organizationId) {
        return isAdmin() || belongsToOrganization(organizationId);
    }

    private boolean isAdmin() {
        return SecurityContextHolder.getContext().getAuthentication().getAuthorities().contains(new SimpleGrantedAuthority(ADMIN.getRoleName()));
    }

    private boolean belongsToOrganization(Long organizationId) {
        return organizationId == Long.parseLong(getUserDetails().get(ORGANIZATION_ID_FIELD).toString());

    }

    protected Map<String,Object> getUserDetails() {
        JwtAuthentication jwtAuthentication = (JwtAuthentication) SecurityContextHolder.getContext().getAuthentication();
        return jwtAuthentication.getJwtClaimsSet().getClaims();
    }

}

这当然也会对上述调用产生影响,因为它将不再能够将组织 uri 转换为实体。

但是,API 调用失败并显示 HTTP 400(错误请求)和以下正文,而不是抛出 403 Forbidden:

{
    "cause":{
        "cause":{
            "cause":null,
            "message":"Access is denied"
        },
        "message":"Access is denied (through reference chain: com.example.Employee[\"organization\"])"
    },
    "message":"Could not read document: Access is denied (through reference chain: com.example.Employee[\"organization\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Access is denied (through reference chain: com.example.Employee[\"organization\"])"
}

在日志中:

Caused by: com.fasterxml.jackson.databind.JsonMappingException: Access is denied (through reference chain: com.example.Employee["organization"])
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:388) ~[jackson-databind-2.8.5.jar:2.8.5]
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:348) ~[jackson-databind-2.8.5.jar:2.8.5]
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.wrapAndThrow(BeanDeserializerBase.java:1599) ~[jackson-databind-2.8.5.jar:2.8.5]
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:359) ~[jackson-databind-2.8.5.jar:2.8.5]
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:148) ~[jackson-databind-2.8.5.jar:2.8.5]
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3798) ~[jackson-databind-2.8.5.jar:2.8.5]
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2922) ~[jackson-databind-2.8.5.jar:2.8.5]
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:237) ~[spring-web-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    ... 97 common frames omitted
Caused by: org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-4.1.4.RELEASE.jar:4.1.4.RELEASE]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233) ~[spring-security-core-4.1.4.RELEASE.jar:4.1.4.RELEASE]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:65) ~[spring-security-core-4.1.4.RELEASE.jar:4.1.4.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at com.sun.proxy.$Proxy156.findOne(Unknown Source) ~[na:na]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_40]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_40]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_40]
    at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_40]
    at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:216) ~[spring-core-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.data.repository.support.ReflectionRepositoryInvoker.invoke(ReflectionRepositoryInvoker.java:265) ~[spring-data-commons-1.12.6.RELEASE.jar:na]
    at org.springframework.data.repository.support.ReflectionRepositoryInvoker.invokeFindOne(ReflectionRepositoryInvoker.java:140) ~[spring-data-commons-1.12.6.RELEASE.jar:na]
    at org.springframework.data.repository.support.CrudRepositoryInvoker.invokeFindOne(CrudRepositoryInvoker.java:91) ~[spring-data-commons-1.12.6.RELEASE.jar:na]
    at org.springframework.data.rest.core.support.UnwrappingRepositoryInvokerFactory$UnwrappingRepositoryInvoker.invokeFindOne(UnwrappingRepositoryInvokerFactory.java:130) ~[spring-data-rest-core-2.5.6.RELEASE.jar:na]
    at org.springframework.data.rest.core.UriToEntityConverter.convert(UriToEntityConverter.java:123) ~[spring-data-rest-core-2.5.6.RELEASE.jar:na]
    at org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module$UriStringDeserializer.deserialize(PersistentEntityJackson2Module.java:516) ~[spring-data-rest-webmvc-2.5.6.RELEASE.jar:na]
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:499) ~[jackson-databind-2.8.5.jar:2.8.5]
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:101) ~[jackson-databind-2.8.5.jar:2.8.5]
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:357) ~[jackson-databind-2.8.5.jar:2.8.5]

它将内部信息泄露给客户的事实让我觉得有问题。

我只是希望 api 在这种情况下返回 403 Forbidden,或者有一些方法来自定义此错误消息。

我该怎么做呢?

【问题讨论】:

  • 能分享一下securityService、employee和spring security config的代码吗?似乎JSON映射有错误,这就是抛出400的原因。
  • 添加了堆栈跟踪/安全服务实现。 Data Rest Uri / Entity 转换器触发存储库调用。不允许身份验证对象执行该调用 (PreAuthorize),并引发 AccessDeniedException。 AccessDeniedException 被推上堆栈,Spring 向客户端抛出一个 http 400。我仍然希望这里是 403 而不是 400。

标签: spring-security spring-data spring-data-rest


【解决方案1】:

当然,您应该使用标准的Spring exception handling mechanisms,但您的问题比平时要复杂一些。我已经在演示项目中尝试了我的解决方案,这应该可以工作。

在您的情况下,AccessDeniedException 被包裹在 JsonMappingException 中,而 JsonMappingException 本身被包裹在 HttpMessageNotReadableException 中。这是 Spring 异常处理得到的:

HttpMessageNotReadableException
|---JsonMappingException
    |---AccessDeniedException

根据 Spring 问题跟踪器中的this ticket @ExceptionHandler 方法可以匹配自 Spring 4.3 以来的包装异常,但最多只能匹配一层。对AccessDeniedException 使用@ExceptionHandler 是行不通的,因为它嵌套在Spring 收到的异常下方两层。

您可以更改 Spring 在异常链中寻找匹配处理程序的深度,但如果只是针对这个问题,我只需定义一个执行此操作的异常处理程序:

@ControllerAdvice
public class ExceptionAdvice {

    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ResponseEntity<String> accessDenied(Exception e) throws Exception {
        Throwable cause = e.getCause();
        if (cause != null) {
            Throwable nestedCause = cause.getCause();
            if (AccessDeniedException.class.isAssignableFrom(nestedCause.getClass())) {
                return new ResponseEntity<>(HttpStatus.FORBIDDEN);
            }
        }
        throw e;
    }
}

您可以自定义匹配(也许在完整的异常链中搜索想要的异常?)并添加返回消息。

请注意,处理程序是在其自己的类中定义的 - 因为您的 EmployeeController 可能是一个接口,就像您的 OrganizationController 一样,所以不能在其中定义 @ExceptionHandler 方法(默认方法也不起作用)。

【讨论】:

    猜你喜欢
    • 2019-09-14
    • 1970-01-01
    • 1970-01-01
    • 2017-06-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-30
    相关资源
    最近更新 更多