如何在Spring的CAS服务属性中正确设置服务URL

发布于 2024-10-09 17:54:15 字数 1546 浏览 9 评论 0原文

当使用 Spring Security + CAS 时,我总是遇到发送到 CAS 的回调 URL(即服务属性)的小障碍。我查看了很多示例,例如 thisthis 但它们都使用硬编码的 URL(甚至 Spring 的 CAS 文档)。典型的片段看起来像这样...

  <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 在下面指出的那样 < a href="https://jira.springsource.org/browse/SEC-1374" rel="noreferrer">此链接,出于安全原因,我们不能信任来自客户端的 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>

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

When working with Spring Security + CAS I keep hitting a small road block with the callback URL that is sent to CAS, ie the service property. I've looked at a bunch of examples such as this and this but they all use hard coded URLs (even Spring's CAS docs). A typical snip looks something like this...

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

First, I don't want to hard code the server name or the port since I want this WAR to be deployable anywhere and I don't want my application tied to a particular DNS entry at compile time. Second, I don't understand why Spring can't auto detect my application's context and the request's URL to automagically build the URL. The first part of that statement still stand but As Raghuram pointed out below with this link, we can't trust the HTTP Host Header from the client for security reasons.

Ideally I would like service URL to be exactly what the user requested (as long as the request is valid such as a sub domain of mycompany.com) so it is seamless or at the very least I would like to only specify some path relative my applications context root and have Spring determine the service URL on the fly. Something like the following...

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

OR...

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

Is any of this possible or easy or have I missed the obvious?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

晨曦慕雪 2024-10-16 17:54:15

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

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

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

  1. 覆盖 CasAuthenticationFilter
  2. 覆盖CasAuthenticationProvider
  3. ServiceProperties 上的 setAuthenticateAllArtifacts(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) 调用。这将使服务票证验证器使用 AuthenticationDetailsS​​ource 实现,而不是硬编码的 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();
}

I know this is a bit old but I just had to solve this very problem and couldn't really find anything in the newer stacks.

We have multiple environments sharing the same CAS service (think dev, qa, uat and local development environments); we have the ability to hit each environment from more than one url (via the client side web server over a reverse proxy and directly to the back-end server itself). This means that specifying a single url is difficult at best. Maybe there's a way to do this but being able to use a dynamic ServiceProperties.getService(). I'll probably add some kind of server suffix check to ensure that the url isn't hijacked at some point.

Here's what I did to get the basic CAS flow working regardless of the URL used to access the secured resource...

  1. Override the CasAuthenticationFilter.
  2. Override the CasAuthenticationProvider.
  3. setAuthenticateAllArtifacts(true) on the ServiceProperties.

Here's the long form of my spring configuration bean:

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

Just the usual spring configuration 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;

Some externalized configuration parameters.

@Bean
public ServiceProperties serviceProperties() {
    ServiceProperties serviceProperties = new ServiceProperties();
    serviceProperties.setService(casValidationUri);
    serviceProperties.setSendRenew(false);
    serviceProperties.setAuthenticateAllArtifacts(true);
    return serviceProperties;
}

The key thing above is the setAuthenticateAllArtifacts(true) call. This will make the service ticket validator use the AuthenticationDetailsSource implementation rather than a hard-coded ServiceProperties.getService() call

@Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
    return new Cas20ServiceTicketValidator(casServerUrl);
}

Standard ticket validator..

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

Standard hook to an existing UserDetailsService

@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
    CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
    casAuthenticationProvider.setAuthenticationUserDetailsService(authenticationUserDetailsService());
    casAuthenticationProvider.setServiceProperties(serviceProperties());
    casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
    casAuthenticationProvider.setKey(casProviderKey);
    return casAuthenticationProvider;
}

Standard authentication provider

@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
    CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
    casAuthenticationFilter.setAuthenticationManager(authenticationManager());
    casAuthenticationFilter.setServiceProperties(serviceProperties());
    casAuthenticationFilter.setAuthenticationDetailsSource(dynamicServiceResolver());
    return casAuthenticationFilter;
}

Key here is the dynamicServiceResolver() setting..

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

Dynamically creates the service url from the makeDynamicUrlFromRequest() method. This bit is used upon ticket validation.

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

This part uses the same dynamic url creator when CAS wants to redirect to the login screen.

private String makeDynamicUrlFromRequest(ServiceProperties serviceProperties){
    return "https://howeverYouBuildYourOwnDynamicUrl.com";
}

This is whatever you make of it. I only passed in the ServiceProperties to hold the URI of the service that we're configured for. We use HATEAOS on the back-side and have an implementation like:

return UriComponentsBuilder.fromHttpUrl(
            linkTo(methodOn(ExposedRestResource.class)
                    .aMethodOnThatResource(null)).withSelfRel().getHref())
            .replacePath(serviceProperties.getService())
            .build(false)
            .toUriString();

Edit: here's what I did for the list of valid server suffixes..

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();
}
离鸿 2024-10-16 17:54:15

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

In Spring 2.6.5 spring you could extend org.springframework.security.ui.cas.ServiceProperties

In spring 3 the method is final you could get around this by subclassing the CasAuthenticationProvider and CasEntryPoint and then use with your own version of ServiceProperties and override the getService() method with a more dynamic implementation.

You could use the host header to calculate the the required domain and make it more secure by validating that only domains/subdomains under your control are used. Then append to this some configurable value.

Of course you would be at risk that your implementation was insecure though... so be careful.

It could end up looking like:

<bean id="serviceProperties" class="my.ServiceProperties">
    <property name="serviceRelativeUrl" value="/my_cas_callback" />
    <property name="validDomainPattern" value="*.mydomain.com" />
</bean>
浅忆 2024-10-16 17:54:15

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

use maven, add a property placeholder, and configure it in your build process

霞映澄塘 2024-10-16 17:54:15

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

示例:<属性名称=“服务”
value="https://#{T(java.net.InetAddress).getLocalHost().getHostName()}:${application.port}${cas.service}/login/cascheck"/>

I tried to subclass CasAuthenticationProvider as Pablojim suggest, but solution is very easier! with Spring Expression Language (SPEL) you can obtain the url dinamically.

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

酒几许 2024-10-16 17:54:15

我自己没有尝试过,但 Spring Security 似乎有一个解决方案 SavedRequestAwareAuthenticationSuccessHandler 显示在 鲍勃的博客

I have not tried this myself, but it seems Spring Security has a solution to this with the SavedRequestAwareAuthenticationSuccessHandler shown in the update of Bob's blog.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文