Java 如何在Spring';s CAS服务属性

Java 如何在Spring';s CAS服务属性,java,spring,spring-security,cas,Java,Spring,Spring Security,Cas,在使用SpringSecurity+CAS时,我一直在使用发送到CAS的回调URL(即服务属性)遇到一个小障碍。我看过很多例子,比如和,但它们都使用硬编码的URL(甚至)。一个典型的剪子看起来像这样 <bean id="serviceProperties" class="org.springframework.security.ui.cas.ServiceProperties"> <property name="service" value="http://loca

在使用SpringSecurity+CAS时,我一直在使用发送到CAS的回调URL(即服务属性)遇到一个小障碍。我看过很多例子,比如和,但它们都使用硬编码的URL(甚至)。一个典型的剪子看起来像这样

  <bean id="serviceProperties" class="org.springframework.security.ui.cas.ServiceProperties">
    <property name="service" value="http://localhost:8080/click/j_spring_cas_security_check" />
  </bean>

首先,我不想硬编码服务器名称或端口,因为我希望这个WAR可以部署到任何地方,我不希望我的应用程序在编译时绑定到特定的DNS条目。其次,我不明白为什么Spring不能自动检测应用程序的上下文和请求的URL来自动构建URL。该声明的第一部分仍然有效,但正如Raghuram在下文中指出的,出于安全原因,我们不能信任来自客户端的HTTP主机头

理想情况下,我希望服务URL与用户请求的完全一致(只要请求有效,例如mycompany.com的子域),因此它是无缝的,或者至少我希望只指定一些与我的应用程序上下文根相关的路径,并让Spring动态确定服务URL。像下面这样的

  <bean id="serviceProperties" class="org.springframework.security.ui.cas.ServiceProperties">
    <property name="service" value="/my_cas_callback" />
  </bean>

或者



这是可能的还是容易的,或者我错过了显而易见的吗?

在Spring 2.6.5 Spring中,您可以扩展org.springframework.security.ui.cas.ServiceProperties

在Spring3中,该方法是最终的,您可以通过子类化CasAuthenticationProvider和CasEntryPoint来解决这个问题,然后与您自己版本的ServiceProperties一起使用,并使用更动态的实现重写getService()方法

您可以使用主机头计算所需域的名称,并通过验证仅使用您控制下的域/子域,使其更加安全。然后将一些可配置的值附加到该值

当然,您将面临实现不安全的风险。。。所以要小心

它可能最终看起来像:

<bean id="serviceProperties" class="my.ServiceProperties">
    <property name="serviceRelativeUrl" value="/my_cas_callback" />
    <property name="validDomainPattern" value="*.mydomain.com" />
</bean>


使用maven,添加一个属性占位符,并在构建过程中对其进行配置

我自己没有尝试过,但Spring Security似乎有一个解决方案,如更新中所示。

我尝试按照Pablojim的建议将CasAuthenticationProvider子类化,但解决方案非常简单!使用SpringExpressionLanguage(SPEL),您可以通过友好方式获取url


示例:
我知道这有点旧,但我必须解决这个问题,在较新的堆栈中找不到任何东西

我们有多个共享同一CAS服务的环境(想想开发、qa、uat和本地开发环境);我们能够从多个url(通过反向代理上的客户端web服务器,并直接到后端服务器本身)访问每个环境。这意味着指定一个url充其量是困难的。也许有办法做到这一点,但是可以使用动态的
ServiceProperties.getService()
。我可能会添加一些服务器后缀检查,以确保url在某个时候不会被劫持

以下是我为使基本CAS流正常工作所做的工作,而不考虑用于访问安全资源的URL

  • 覆盖
    CasAuthenticationFilter
  • 覆盖
    CasAuthenticationProvider
  • ServiceProperties
    上的设置AuthenticateAllartifacts(true)
  • 下面是我的spring配置bean的详细形式:

    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
    public class CasSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
    只是普通的spring配置bean

    @Value("${cas.server.url:https://localhost:9443/cas}")
    private String casServerUrl;
    
    @Value("${cas.service.validation.uri:/webapi/j_spring_cas_security_check}")
    private String casValidationUri;
    
    @Value("${cas.provider.key:whatever_your_key}")
    private String casProviderKey;
    
    一些外部化的配置参数

    @Bean
    public ServiceProperties serviceProperties() {
        ServiceProperties serviceProperties = new ServiceProperties();
        serviceProperties.setService(casValidationUri);
        serviceProperties.setSendRenew(false);
        serviceProperties.setAuthenticateAllArtifacts(true);
        return serviceProperties;
    }
    
    上面的关键是
    setAuthenticateAllArtifacts(true)
    调用。这将使服务票证验证器使用
    AuthenticationDetailsSource
    实现,而不是硬编码的
    ServiceProperties.getService()
    调用

    @Bean
    public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
        return new Cas20ServiceTicketValidator(casServerUrl);
    }
    
    标准票证验证器

    @Resource
    private UserDetailsService userDetailsService;
    
    @Bean
    public AuthenticationUserDetailsService authenticationUserDetailsService() {
        return new AuthenticationUserDetailsService() {
            @Override
            public UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException {
                String username = (token.getPrincipal() == null) ? "NONE_PROVIDED" : token.getName();
                return userDetailsService.loadUserByUsername(username);
            }
        };
    }
    
    现有UserDetailsService的标准挂钩

    @Bean
    public CasAuthenticationProvider casAuthenticationProvider() {
        CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
        casAuthenticationProvider.setAuthenticationUserDetailsService(authenticationUserDetailsService());
        casAuthenticationProvider.setServiceProperties(serviceProperties());
        casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
        casAuthenticationProvider.setKey(casProviderKey);
        return casAuthenticationProvider;
    }
    
    标准身份验证提供程序

    @Bean
    public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
        CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
        casAuthenticationFilter.setAuthenticationManager(authenticationManager());
        casAuthenticationFilter.setServiceProperties(serviceProperties());
        casAuthenticationFilter.setAuthenticationDetailsSource(dynamicServiceResolver());
        return casAuthenticationFilter;
    }
    
    这里的关键是
    dynamicServiceResolver()
    设置

    @Bean
    AuthenticationDetailsSource<HttpServletRequest,
            ServiceAuthenticationDetails> dynamicServiceResolver() {
        return new AuthenticationDetailsSource<HttpServletRequest, ServiceAuthenticationDetails>() {
            @Override
            public ServiceAuthenticationDetails buildDetails(HttpServletRequest context) {
                final String url = makeDynamicUrlFromRequest(serviceProperties());
                return new ServiceAuthenticationDetails() {
                    @Override
                    public String getServiceUrl() {
                        return url;
                    }
                };
            }
        };
    }
    
    当CAS想要重定向到登录屏幕时,此部分使用相同的动态url创建者

    private String makeDynamicUrlFromRequest(ServiceProperties serviceProperties){
        return "https://howeverYouBuildYourOwnDynamicUrl.com";
    }
    
    不管你怎么想。我只传入ServiceProperties来保存我们配置的服务的URI。我们在后端使用HATEAOS,并有如下实现:

    return UriComponentsBuilder.fromHttpUrl(
                linkTo(methodOn(ExposedRestResource.class)
                        .aMethodOnThatResource(null)).withSelfRel().getHref())
                .replacePath(serviceProperties.getService())
                .build(false)
                .toUriString();
    
    编辑:下面是我为有效服务器后缀列表所做的操作

    private List<String> validCasServerHostEndings;
    
    @Value("${cas.valid.server.suffixes:company.com,localhost}")
    private void setValidCasServerHostEndings(String endings){
        validCasServerHostEndings = new ArrayList<>();
        for (String ending : StringUtils.split(endings, ",")) {
            if (StringUtils.isNotBlank(ending)){
                validCasServerHostEndings.add(StringUtils.trim(ending));
            }
        }
    }
    
    private String makeDynamicUrlFromRequest(ServiceProperties serviceProperties){
        UriComponents url = UriComponentsBuilder.fromHttpUrl(
                linkTo(methodOn(ExposedRestResource.class)
                        .aMethodOnThatResource(null)).withSelfRel().getHref())
                .replacePath(serviceProperties.getService())
                .build(false);
        boolean valid = false;
        for (String validCasServerHostEnding : validCasServerHostEndings) {
            if (url.getHost().endsWith(validCasServerHostEnding)){
                valid = true;
                break;
            }
        }
        if (!valid){
            throw new AccessDeniedException("The server is unable to authenticate the requested url.");
        }
        return url.toString();
    }
    
    私有列表有效的asserverHostEndings;
    @值(${cas.valid.server.后缀:company.com,localhost}”)
    私有void setValidCaserverHostEndings(字符串结尾){
    validCasServerHostEndings=新的ArrayList();
    对于(字符串结尾:StringUtils.split(结尾,“,”)){
    if(StringUtils.isNotBlank(结束)){
    ValidCaserverHostEndings.add(StringUtils.trim(ending));
    }
    }
    }
    私有字符串MakeDynamicCurlFromRequest(ServiceProperties ServiceProperties){
    UriComponents url=UriComponentsBuilder.fromHttpUrl(
    链接到(methodOn(ExposedRestResource.class)
    .withSelfRel().getHref())
    .replacePath(serviceProperties.getService())
    .build(假);
    布尔有效=假;
    for(字符串validCasServerHostEnding:validCasServerHostEndings){
    if(url.getHost().endsWith(validCasServerHostEnding)){
    有效=真;
    打破
    }
    }
    如果(!有效){
    抛出新的AccessDeniedException(“服务器无法验证请求的url”);
    }
    返回url.toString();
    }
    
    根据链接文档,我使用的是Spring Security 3,它将所有这些方法标记为final(),因此,正如问题所述,您可以将CasAuthenticationProvider和CasEntryPoint子类化,并提供您自己版本的服务属性。我更新了答案,让它更明确我想你
    return UriComponentsBuilder.fromHttpUrl(
                linkTo(methodOn(ExposedRestResource.class)
                        .aMethodOnThatResource(null)).withSelfRel().getHref())
                .replacePath(serviceProperties.getService())
                .build(false)
                .toUriString();
    
    private List<String> validCasServerHostEndings;
    
    @Value("${cas.valid.server.suffixes:company.com,localhost}")
    private void setValidCasServerHostEndings(String endings){
        validCasServerHostEndings = new ArrayList<>();
        for (String ending : StringUtils.split(endings, ",")) {
            if (StringUtils.isNotBlank(ending)){
                validCasServerHostEndings.add(StringUtils.trim(ending));
            }
        }
    }
    
    private String makeDynamicUrlFromRequest(ServiceProperties serviceProperties){
        UriComponents url = UriComponentsBuilder.fromHttpUrl(
                linkTo(methodOn(ExposedRestResource.class)
                        .aMethodOnThatResource(null)).withSelfRel().getHref())
                .replacePath(serviceProperties.getService())
                .build(false);
        boolean valid = false;
        for (String validCasServerHostEnding : validCasServerHostEndings) {
            if (url.getHost().endsWith(validCasServerHostEnding)){
                valid = true;
                break;
            }
        }
        if (!valid){
            throw new AccessDeniedException("The server is unable to authenticate the requested url.");
        }
        return url.toString();
    }