spring-security - 根据认证或不认证委托给不同的控制器?

时间:2016-03-17 15:22:44

标签: rest spring-mvc spring-security

不是在每个方法中都有一个REST控制器,而是取决于当前用户是否经过身份验证而采取不同的操作我想根据用户的身份验证状态委托给完全不同的控制器实现。

即。我提供了一个包含一组方法签名的接口,每个方法签名都带有@RequestMapping注释,然后提供此接口的一个实现,用于经过身份验证的用户,另一个实现用于未经过身份验证的用户。然后,一些逻辑将为当前用户选择适当的实现并发送给它。

2 个答案:

答案 0 :(得分:4)

我知道你有一个适合你的答案,但是一个可能感兴趣的解决方案(我现在只是为了获取信息)是使用Spring自定义映射条件。

我们可以定义一个可以用来装饰我们的控制器的注释 - 可能像@AuthenticatedMapping

@Target( ElementType.TYPE )
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthenticatedMapping {} 

(你可以用不同的方式实现它,并且有一个枚举值来表示特定的角色等,如果你更喜欢这种粒度,也可以将它设置为METHOD级别注释)

然后你可以定义一个自定义RequestCondition - 这是Spring将用作为给定请求设计正确处理程序的一部分的类(就像@RequestMapping注释一样)

public class AuthenticatedMappingRequestCondition implements RequestCondition<AuthenticatedMappingRequestCondition> {


    @Override public AuthenticatedMappingRequestCondition getMatchingCondition( HttpServletRequest request ) {
        AuthenticatedMappingRequestCondition condition = null;
        //Check the user is authenticated, if so return this condition:
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if( (authentication != null && !(authentication instanceof AnonymousAuthenticationToken)){
            condition = this;
        }
        return condition;
    }

    //
    //TODO other methods need to be implemented here - all pretty simple
    //

}

所以现在我们有了映射条件,我们只需要扩展标准的Spring RequestMappingHandlerMapping,以便在考虑映射决策时包含我们的自定义条件:

public class AuthenticatedMappingRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    @Override protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        AuthenticatedMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, AuthenticatedMapping.class);
        return (typeAnnotation != null) ? new AuthenticatedMappingRequestCondition() : null;
    }
}

使用Spring连接自定义条件后,您可以使用注释装饰任何控制器,Spring将使用条件来路由请求:

@RestController
@AuthenticatedMapping
@RequestMapping("/account")
public class AuthenticatedAccountRestController {
    @RequestMapping("/someCommonRequestA")
    public String someCommonRequestA() {
        return "Got authenticated someCommonRequestA";
    }
}

@RestController
@RequestMapping("/account")
public class AnonymousAccountRestController {
    @RequestMapping("/someCommonRequestA")
    public String someCommonRequestA() {
        return "Got anonymous someCommonRequestA";
    }
}

一些警告:

  • 您需要测试缺少注释是否足以路由到匿名控制器 - 如果没有,可以使用允许的角色枚举来增强注释 - 并向该控制器显式添加@AuthenticatedMapping( Roles.ANONYMOUS )
  • 因为它是一个单一的端点,它不处理特定的安全性,所以仍然需要安装的安全措施,而不是100%没有其他考虑的弹性安全认证的东西

不建议它比你拥有的方法更好,但我不得不使用这种方法基于子域进行自定义路由(我写了here - 这就是上面的代码所基于的),当我不得不将它推广到许多控制器/端点时,我发现它是一个非常好的,惯用的Spring-y解决方案,因为样板被抽象到Spring机器并且控制器装饰得很好。

无论如何,如果没别的话可能会很有趣:)

答案 1 :(得分:2)

我认为这比证明的要容易。这是我的解决方案。

首先,我创建了一个包含控制器请求的抽象类,而不是接口:

@PreAuthorize("this.authorized")
public abstract class AccountRestController {
    @RequestMapping("/someCommonRequestA")
    public abstract String someCommonRequestA();

    public boolean getAuthorized() {
        Authentication authentication = SecurityContextHolder.getContext()
            .getAuthentication();

        return !(authentication == null ||
            authentication instanceof AnonymousAuthenticationToken);
    }
}

需要注意的是@PreAuthorize注释和getAuthorized()方法。然后我提供了一个类来处理转发到适当的控制器:

@Controller
public class ForwardingAccountController {
    @RequestMapping("/account/**")
    public String forward(HttpServletRequest request,
            Authentication authentication) {
        String prefix = authentication != null ? "authenticated" : "anonymous";
        String path = (String) request.getAttribute(
            HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);

        // You could do a redirect if you wanted to make explicit to the caller what's going on.
        return "forward:/" + prefix + "/" + path;
    }
}

然后我提供了经过身份验证的匿名实现,根据当前用户的状态委派实际行为。

对于授权用户:

@RestController
@RequestMapping("/authenticated/account")
public class AuthenticatedAccountRestController extends AccountRestController {
    @Override
    public String someCommonRequestA() {
        return "Got authenticated someCommonRequestA";
    }
}

对于未经授权的用户:

@RestController
@RequestMapping("/anonymous/account")
public class AnonymousAccountRestController extends AccountRestController {
    @Override
    public String someCommonRequestA() {
        return "Got anonymous someCommonRequestA";
    }

    @Override
    public boolean getAuthorized() {
        return true;
    }
}

了解AnonymousAccountRestController如何通过覆盖getAuthorized()来关闭授权要求。

棘手的一点是Spring Security注释。最初我以为我可以用AuthenticatedAccountRestController注释@Secured(AuthenticatedVoter.IS_AUTHENTICATED_FULLY)

但是,当在超类中定义映射时,在子类上添加注释不起作用 - 请参阅"Spring MVC controller inheritance with spring security"

受到SO答案的启发,我在超类上使用了@PreAuthorize,这样我就可以改变它在子类中的实际行为。

同样令人感兴趣的是AccountRestControllerForwardingAccountController中经过身份验证的检查的差异 - 后者我不必担心AnonymousAuthenticationToken - 我为匿名用户获取null

如果您想进行实验,可以在Github repo auth-dependent-controllers中找到这些课程。

为什么我希望事情更简单?在JAX-RS中,我能够使用返回不同子资源的资源实现类似的功能,具体取决于用户的状态,这些子资源处理实际请求。我以为我能在春天做一些相似的事情。

PS抱歉使用授权和身份验证,就像它们是可互换的术语一样。