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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/4555206/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-30 06:56:24  来源:igfitidea点击:

How to correctly set the service URL in Spring's CAS service properties

javaspringspring-securitycas

提问by Andrew White

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 thisand thisbut they all use hard coded URLs (even Spring's CAS docs). A typical snip looks something like this...

在使用 Spring Security + CAS 时,我一直在使用发送到 CAS 的回调 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>

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.

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

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

理想情况下,我希望服务 URL 正是用户请求的(只要请求有效,例如 mycompany.com 的子域),因此它是无缝的,或者至少我只想指定一些相对于我的路径应用程序上下文根并让 Spring 动态确定服务 URL。类似于以下内容...

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

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

采纳答案by Pablojim

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

在 Spring 2.6.5 spring 中你可以扩展 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.

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

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>

回答by Matt

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 hiHymaned at some point.

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

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

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

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

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

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

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

Just the usual spring configuration bean.

只是通常的 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;

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 AuthenticationDetailsSourceimplementation rather than a hard-coded ServiceProperties.getService()call

上面的关键是setAuthenticateAllArtifacts(true)调用。这将使服务票证验证器使用AuthenticationDetailsSource实现而不是硬编码的ServiceProperties.getService()调用

@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

到现有 UserDetailsS​​ervice 的标准挂钩

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

这里的关键是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;
                }
            };
        }
    };
}

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

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

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

当 CAS 想要重定向到登录屏幕时,这部分使用相同的动态 url 创建器。

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:

这就是你怎么做的。我只传入了 ServiceProperties 来保存我们为其配置的服务的 URI。我们在后端使用 HATEAOS 并有一个实现,如:

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

回答by chrismarx

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

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

回答by Donald

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

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

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

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

回答by stigkj

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

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