【问题标题】:How to correctly set the service URL in Spring's CAS service properties如何在 Spring 的 CAS 服务属性中正确设置服务 URL
【发布时间】:2011-06-01 02:39:18
【问题描述】:

在使用 Spring Security + CAS 时,我一直遇到一个小障碍,即发送到 CAS 的回调 URL,即服务属性。我看过很多例子,例如thisthis,但它们都使用硬编码的URL(甚至Spring's 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 在下面用 @ 指出的那样987654324@,出于安全原因,我们不能信任来自客户端的 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>

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

【问题讨论】:

  • 也许this link 是相关的,并且可以对您的需求/问题提供一些见解?
  • 好吧,我当然学到了一些东西并排除了一种可能的解决方案。由于我不能依赖 HTTP 请求,我仍然希望在部署时通过一些派生值设置服务,这应该是安全的。
  • 我使用的是spring 3;注意 spring security 3 文档的链接

标签: java spring spring-security cas


【解决方案1】:

在 Spring 2.6.5 中,您可以扩展 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>

【讨论】:

  • 根据链接文档,我正在使用 Spring Security 3,其中所有这些方法都标记为 final (static.springsource.org/spring-security/site/docs/3.0.x/apidocs/…)
  • 因此,正如问题所述,您可以将 CasAuthenticationProvider 和 CasEntryPoint 子类化,并为您提供自己的 Service 属性版本。我已经更新了答案以使其更加明确
  • 我认为您可能是正确的。我还没有机会尝试这个,但是当我这样做时,除非有更好的答案出现,否则这看起来将是最好的答案。
【解决方案2】:

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

【讨论】:

    【解决方案3】:

    我自己没有尝试过,但似乎 Spring Security 有一个解决方案,SavedRequestAwareAuthenticationSuccessHandler 显示在 Bob's blog 的更新中。

    【讨论】:

      【解决方案4】:

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

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

      【讨论】:

      • 只是为了澄清任何对此答案感到兴奋的人,此检索的主机名将是实际服务器的名称,而不是请求中的主机名。如果您在不同的物理服务器上运行不同版本的应用程序,这可能是理想的
      【解决方案5】:

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

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

      无论用于访问受保护资源的 URL 是什么,我都是这样做的,以使基本的 CAS 流程正常工作...

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

      这是我的 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);
              }
          };
      }
      

      现有 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();
      }
      

      【讨论】:

      • 我可以在这篇文章中找到最有用的信息。这真太了不起了。我真的很难在我们复杂的系统中实现这样的东西(出于同样的原因)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-04-20
      • 2014-08-18
      • 1970-01-01
      • 1970-01-01
      • 2016-10-20
      • 2017-02-07
      相关资源
      最近更新 更多