Как правильно установить URL-адрес службы в свойствах службы Spring CAS

При работе с Spring Security + CAS я продолжаю сталкиваться с небольшим препятствием с URL-адресом обратного вызова, который отправляется в 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>

Во-первых, я не хочу жестко кодировать имя сервера или порт, поскольку я хочу, чтобы этот WAR можно было развернуть где угодно, и я не хочу, чтобы мое приложение было привязано к определенной записи DNS во время компиляции. Во-вторых, я не понимаю, почему Spring не может автоматически определить контекст моего приложения и URL-адрес запроса для автоматического создания URL-адреса. Первая часть этого утверждения остается в силе, но, как указал Рагурам ниже, с помощью эту ссылку, мы не можем доверять HTTP-заголовку хоста от клиента по соображениям безопасности.

В идеале я хотел бы, чтобы URL-адрес службы был именно тем, что запросил пользователь (при условии, что запрос действителен, например, поддомен mycompany.com), чтобы он был бесшовным или, по крайней мере, я хотел бы указать только некоторый путь относительно моего root контекста приложений, и 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>

Возможно ли что-либо из этого или легко, или я пропустил очевидное?


person Andrew White    schedule 29.12.2010    source источник
comment
Возможно, эта ссылка связана и дает некоторое представление о ваших требованиях/проблемах?   -  person Raghuram    schedule 30.12.2010
comment
Что ж, я определенно кое-что узнал и исключил одно возможное решение. Поскольку я не могу полагаться на HTTP-запрос, я все же хотел бы установить службу с помощью некоторых производных значений во время развертывания, что должно быть безопасным.   -  person Andrew White    schedule 30.12.2010
comment
Я использую пружину 3; обратите внимание на ссылку на документы spring security 3   -  person Andrew White    schedule 10.01.2011


Ответы (5)


Весной Spring 2.6.5 вы можете расширить org.springframework.security.ui.cas.ServiceProperties

В весне 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>
person Pablojim    schedule 09.01.2011
comment
Согласно связанному документу, я использую Spring Security 3, в котором все эти методы помечены как окончательные (static.springsource.org/spring-security/site/docs/3.0.x/apidocs/) - person Andrew White; 10.01.2011
comment
Итак, как говорится в проблеме, вы можете просто подклассировать CasAuthenticationProvider и CasEntryPoint и предоставить собственную версию свойств службы. Я обновил ответ, чтобы сделать его более явным - person Pablojim; 10.01.2011
comment
Я думаю, что вы, вероятно, правы. У меня еще не было возможности попробовать это, но когда я это сделаю, и если не появится лучший ответ, похоже, что это будет лучший ответ. - person Andrew White; 01.02.2011

Я знаю, что это немного устарело, но мне просто нужно было решить эту проблему, и я ничего не мог найти в новых стеках.

У нас есть несколько сред, совместно использующих одну и ту же службу CAS (например, среды разработки, контроля качества, uat и локальные среды разработки); у нас есть возможность попасть в каждую среду с более чем одного URL-адреса (через веб-сервер на стороне клиента через обратный прокси-сервер и непосредственно на сам внутренний сервер). Это означает, что указать один URL-адрес в лучшем случае сложно. Возможно, есть способ сделать это, но возможность использовать динамический ServiceProperties.getService(). Я, вероятно, добавлю какую-то проверку суффикса сервера, чтобы гарантировать, что URL-адрес не будет взломан в какой-то момент.

Вот что я сделал, чтобы базовый поток CAS работал независимо от URL-адреса, используемого для доступа к защищенному ресурсу...

  1. Переопределить CasAuthenticationFilter.
  2. Переопределить CasAuthenticationProvider.
  3. setAuthenticateAllArtifacts(true) на ServiceProperties.

Вот длинная форма моего bean-компонента конфигурации spring:

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

Стандартный хук для существующего 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;
                }
            };
        }
    };
}

Динамически создает URL-адрес службы из метода makeDynamicUrlFromRequest(). Этот бит используется при проверке билета.

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

Эта часть использует тот же генератор динамических URL-адресов, когда CAS хочет перенаправить на экран входа в систему.

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();
}
person Matt    schedule 18.02.2015
comment
Самая полезная информация, которую я смог найти в этом посте. Это потрясающе. Я действительно изо всех сил пытался реализовать что-то подобное (по тем же причинам, что и вы) в нашей сложной системе. - person Schaka; 26.06.2015

используйте maven, добавьте заполнитель свойства и настройте его в процессе сборки

person chrismarx    schedule 29.03.2011

Я попытался создать подкласс CasAuthenticationProvider, как предлагает Паблоджим, но решение очень простое! с помощью Spring Expression Language (SPEL) вы можете получить URL-адрес динамически.

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

person Donald    schedule 06.09.2013
comment
Просто чтобы уточнить для всех, кто взволнован этим ответом, получаемое им имя хоста будет фактическим именем сервера, а не именем хоста из запроса. Если у вас есть разные версии приложения, работающие на разных физических серверах, это может быть идеальным вариантом. - person chrismarx; 21.01.2014

Я не пробовал это сам, но кажется, что Spring Security имеет решение для этого с помощью SavedRequestAwareAuthenticationSuccessHandler показано в обновлении блог Боба.

person stigkj    schedule 26.06.2012