【问题标题】:spring-security - delegate to different controllers depending on authenticated or not?spring-security - 根据是否经过身份验证委托给不同的控制器?
【发布时间】:2016-07-04 01:19:28
【问题描述】:

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

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

【问题讨论】:

    标签: rest spring-mvc spring-security


    【解决方案1】:

    我知道您有一个适合您的答案,但一个可能感兴趣的可能解决方案(我猜目前只是为了提供信息)是使用 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% 不会对 spring-security 身份验证内容有其他考虑

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

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

    【讨论】:

    • 我不能说我已经尝试过了——所以我不能直接比较它和我已经采取的方法。但它看起来很有趣,值得赏金:)
    【解决方案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 注释。最初我以为我可以用@Secured(AuthenticatedVoter.IS_AUTHENTICATED_FULLY) 注释AuthenticatedAccountRestController

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

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

    同样有趣的是 AccountRestControllerForwardingAccountController 中的身份验证检查的区别 - 在后者中,我不必担心 AnonymousAuthenticationToken - 对于匿名用户,我得到一个空值。

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

    为什么我希望事情变得更简单?在 JAX-RS 中,我能够通过根据用户状态返回不同子资源的资源实现类似的功能,并且这些子资源处理实际请求。我想我可以在 Spring 中做一些类似的事情。

    PS 很抱歉使用授权和认证,好像它们是可互换的术语。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-09-18
      • 2015-08-09
      • 2019-07-16
      • 2014-12-20
      • 2014-02-04
      • 2013-03-31
      • 2017-08-26
      • 1970-01-01
      相关资源
      最近更新 更多