8

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

  <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>

或者...

  <bean id="serviceProperties" class="org.springframework.security.ui.cas.ServiceProperties">
    <property name="service" value="${container.and.app.derived.value.here}" />
  </bean>

这是可能的还是容易的,还是我错过了显而易见的事情?

4

5 回答 5

6

我知道这有点老了,但我只需要解决这个问题,在较新的堆栈中找不到任何东西。

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

这是我为使基本 CAS 流程正常工作而所做的工作,而不管用于访问安全资源的 URL 是什么...

  1. 覆盖CasAuthenticationFilter.
  2. 覆盖CasAuthenticationProvider.
  3. setAuthenticateAllArtifacts(true)ServiceProperties

这是我的 spring 配置 bean 的长格式:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
public class CasSecurityConfiguration extends WebSecurityConfigurerAdapter {

只是通常的弹簧配置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);
        }
    };
}

现有 UserDetailsS​​ervice 的标准挂钩

@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;
                }
            };
        }
    };
}

makeDynamicUrlFromRequest()从该方法动态创建服务 url 。该位在票证验证时使用。

@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {

    CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint() {
        @Override
        protected String createServiceUrl(final HttpServletRequest request, final HttpServletResponse response) {
            return CommonUtils.constructServiceUrl(null, response, makeDynamicUrlFromRequest(serviceProperties())
                    , null, serviceProperties().getArtifactParameter(), false);
        }
    };
    casAuthenticationEntryPoint.setLoginUrl(casServerUrl + "/login");
    casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
    return casAuthenticationEntryPoint;
}

当 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();
}
于 2015-02-18T19:55:19.367 回答
4

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

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

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

当然,您可能会面临实施不安全的风险……所以要小心。

它最终可能看起来像:

<bean id="serviceProperties" class="my.ServiceProperties">
    <property name="serviceRelativeUrl" value="/my_cas_callback" />
    <property name="validDomainPattern" value="*.mydomain.com" />
</bean>
于 2011-01-09T19:28:27.910 回答
2

使用 maven,添加属性占位符,并在构建过程中配置它

于 2011-03-29T21:28:26.070 回答
0

我尝试按照 Pablojim 的建议对 CasAuthenticationProvider 进行子类化,但解决方案非常简单!使用 Spring Expression Language (SPEL),您可以动态地获取 url。

例子:<property name="service" value="https://#{T(java.net.InetAddress).getLocalHost().getHostName()}:${application.port}${cas.service}/login/cascheck"/>

于 2013-09-06T09:38:43.757 回答
-1

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

于 2012-06-26T12:30:45.273 回答