CAS SSO с Spring Security

CAS SSO с Spring Security

1. обзор

В этой статье мы рассмотримintegrating the Central Authentication Service (CAS) with Spring Security. CAS - это служба единого входа (SSO).

Допустим, у нас есть приложения, требующие аутентификации пользователя. Наиболее распространенным методом является реализация механизма безопасности для каждого приложения. Однако было бы лучше реализовать аутентификацию пользователей для всех приложений в одном месте.

Это именно то, что делает система CAS SSO. Этотarticle дает более подробную информацию об архитектуре. Диаграмму протокола можно найтиhere.

2. Настройка и установка проекта

В настройке центральной службы аутентификации участвуют как минимум два компонента. Один из компонентов - это сервер на основе Spring, который называетсяcas-server. Другие компоненты состоят из одного или нескольких клиентов.

Клиентом может быть любое веб-приложение, использующее сервер для аутентификации.

2.1. Настройка сервера CAS

Сервер использует стиль Maven (Gradle) War Overlay для облегчения настройки и развертывания. Существует шаблон быстрого запуска, который можно клонировать и использовать.

Клонируем его:

git clone https://github.com/apereo/cas-overlay-template.git cas-server

Эта команда клонируетcas-overlay-template в каталогcas-server на локальном компьютере.

Затем давайте добавим дополнительные зависимости к корнюpom.xml. Эти зависимости позволяют регистрировать сервисы через конфигурацию JSON.

Также они облегчают подключение к базе данных:


    org.apereo.cas
    cas-server-support-json-service-registry
    ${cas.version}


    org.apereo.cas
    cas-server-support-jdbc
    ${cas.version}


    org.apereo.cas
    cas-server-support-jdbc-drivers
    ${cas.version}

Последнюю версию зависимостейcas-server-support-json-service-registry,cas-server-support-jdbc иcas-server-support-jdbc-drivers можно найти на Maven Central. Обратите внимание, что родительскийpom.xml автоматически управляет версиями артефактов.

Затем давайте создадим папкуcas-server/src/main/resources и скопируем папкуcas-server/etc. внутрь. Мы также собираемся изменить порт приложения и путь к хранилищу ключей SSL.

Мы настраиваем их, редактируя связанные записи вcas-server/src/main/resources/application.properties:

server.port=6443
server.ssl.key-store=classpath:/etc/cas/thekeystore
standalone.config=classpath:/etc/cas/config

Путь к папке конфигурации также был установлен наclasspath:/etc/cas/config. Он указывает наcas-server/src/main/resources/etc/cas/config.

Следующим шагом является создание локального хранилища ключей SSL. Хранилище ключей используется для установления HTTPS-соединений. Этот шаг важен и не может быть пропущен.

В терминале перейдите в каталогcas-server/src/main/resources/etc/cas. После этого выполните следующую команду:

keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore
-storepass changeit -validity 360 -keysize 2048

It’s important to use localhost when prompted for a first and last name, название организации и даже подразделение. Невыполнение этого требования может привести к ошибке во время SSL Handshake. Другие поля, такие как город, штат и страна, могут быть установлены соответствующим образом.

Приведенная выше команда создает хранилище ключей с именемthekeystore и паролемchangeit.. Оно хранится в текущем каталоге.

Затем сгенерированное хранилище ключей необходимо экспортировать в формат.crt для использования клиентскими приложениями. Итак, все еще в том же каталоге, выполните следующую команду, чтобы экспортировать сгенерированный файлthekeystore вthekeystore.crt.. Пароль остается неизменным:

keytool -export -alias thekeystore -file thekeystore.crt
-keystore thekeystore

Теперь давайте импортируем экспортированныйthekeystore.crt в хранилище ключей Javacacerts. Приглашение терминала должно по-прежнему находиться в каталогеcas-server/src/main/resources/etc/cas.

Оттуда выполните команду:

keytool -import -alias thekeystore -storepass changeit -file thekeystore.crt
 -keystore "C:\Program Files\Java\jdk1.8.0_152\jre\lib\security\cacerts"

Просто чтобы быть уверенным, мы также можем импортировать сертификат в JRE, который находится вне установки JDK:

keytool -import -alias thekeystore -storepass changeit -file thekeystore.crt
-keystore "C:\Program Files\Java\jre1.8.0_152\lib\security\cacerts"

Обратите внимание, что флаг-keystore указывает на расположение хранилища ключей Java на локальном компьютере. Это расположение может отличаться в зависимости от имеющейся установки Java.

Кроме того,ensure that the JRE that is referenced as the location of the key store is the same as the one that is used for the client application.

После успешного добавленияthekeystore.crt в хранилище ключей Java нам необходимо перезапустить систему. Эквивалентно, мы можем уничтожить каждый экземпляр JVM, работающий на локальной машине.

Затем из корневого каталога проектаcas-server, вызывает командыbuild package иbuild run с терминала. Запуск сервера может занять некоторое время. Когда он будет готов, на консоли появится сообщение ГОТОВО.

На этом этапе при посещенииhttps://localhost:6443/cas в браузере отображается форма входа в систему. Имя пользователя по умолчанию -casuser, пароль -Mellon.

2.2. Настройка клиента CAS

Давайте воспользуемсяSpring Initializr для создания проекта со следующими зависимостями: Интернет, Безопасность, Freemarker и, возможно, DevTools.

В дополнение к зависимостям, созданным Spring Initializr, давайте добавим зависимость для модуля Spring Security CAS:


    org.springframework.security
    spring-security-cas

Последнюю версию зависимости можно найти наMaven Central. Давайте также настроим порт сервера для прослушивания порта 9000, добавив следующую запись вapplication.properties:

server.port=9000

3. Регистрация служб / клиентов на сервере CAS

Сервер не позволяет любому клиенту получить к нему доступ для аутентификации. clients/services must be registered in the CAS server services registry.

Есть несколько способов зарегистрировать службу на сервере. К ним относятся YAML, JSON, Mongo, LDAP иothers.

В зависимости от метода в файлpom.xml должны быть включены зависимости. В этой статье мы используем метод JSON Service Registry. Зависимость уже была включена в файлpom.xml в предыдущем разделе.

Давайте создадим файл JSON, содержащий определение клиентского приложения. Внутри папкиcas-server/src/main/resources создадим еще одну папку -services.. Это папкаservices, содержащая файлы JSON.

Затем мы создаем файл JSON с именемcasSecuredApp-19991.json в каталогеcas-server/src/main/resources/services со следующим содержимым:

{
    "@class" : "org.apereo.cas.services.RegexRegisteredService",
    "serviceId" : "^http://localhost:9000/login/cas",
    "name" : "CAS Spring Secured App",
    "description": "This is a Spring App that usses the CAS Server for it's authentication",
    "id" : 19991,
    "evaluationOrder" : 1
}

АтрибутserviceId определяет шаблон URL-адреса регулярного выражения для клиентского приложения, которое намеревается использовать сервер для аутентификации. В этом случае шаблон соответствует приложению, запущенному на локальном хосте и прослушивающему порт 9000.

Атрибутid должен быть уникальным, чтобы избежать конфликтов и случайного переопределения конфигураций. Имя файла конфигурации службы соответствует соглашениюserviceName-id.json. Другие настраиваемые атрибуты, такие какtheme,proxyPolicy,logo,privacyUrl и другие, можно найти вhere.

А пока давайте просто добавим еще два элемента конфигурации, чтобы включить JSON Service Registry. Одним из них является информирование сервера о каталоге, в котором находятся файлы конфигурации службы. Другой - включить инициализацию реестра служб из файлов конфигурации JSON.

Оба эти элемента конфигурации помещаются в другой файл с именемcas.properties.. Мы создаем этот файл в каталогеcas-server /src/main/resources:

cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.config.location=classpath:/services

Давайте снова выполним командуbuild run и обратите внимание на такие строки, как «Loaded [3] service(s) from [JsonServiceRegistryDao]» на консоли.

4. Конфигурация Spring Security

4.1. Настройка единого входа

Теперь, когда приложение Spring Boot зарегистрировано на сервере CAS в качестве службы. Пустьconfigure Spring Security to work in concert with the server для аутентификации пользователя. Полную последовательность взаимодействий между Spring Security и сервером можно найти вhere.

Давайте сначала настроим bean-компоненты, связанные с модулем CAS Spring Security. Это позволяет Spring Security сотрудничать с центральной службой аутентификации.

В этой связи нам нужно добавить компоненты конфигурации в классCasSecuredAppApplication точку входа в приложение Spring Boot:

@Bean
public ServiceProperties serviceProperties() {
    ServiceProperties serviceProperties = new ServiceProperties();
    serviceProperties.setService("http://localhost:9000/login/cas");
    serviceProperties.setSendRenew(false);
    return serviceProperties;
}

@Bean
@Primary
public AuthenticationEntryPoint authenticationEntryPoint(
  ServiceProperties sP) {

    CasAuthenticationEntryPoint entryPoint
      = new CasAuthenticationEntryPoint();
    entryPoint.setLoginUrl("https://localhost:6443/cas/login");
    entryPoint.setServiceProperties(sP);
    return entryPoint;
}

@Bean
public TicketValidator ticketValidator() {
    return new Cas30ServiceTicketValidator(
      "https://localhost:6443/cas");
}

@Bean
public CasAuthenticationProvider casAuthenticationProvider() {

    CasAuthenticationProvider provider = new CasAuthenticationProvider();
    provider.setServiceProperties(serviceProperties());
    provider.setTicketValidator(ticketValidator());
    provider.setUserDetailsService(
      s -> new User("casuser", "Mellon", true, true, true, true,
        AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
    provider.setKey("CAS_PROVIDER_LOCALHOST_9000");
    return provider;
}

Мы настраиваем bean-компонентServiceProperties с URL-адресом входа в службу по умолчанию, которомуCasAuthenticationFilter будет внутренне сопоставлен. СвойствоsendRenewServiceProperties установлено наfalse. Как следствие, пользователю необходимо только представить учетные данные для входа на сервер один раз.

Последующая аутентификация будет выполняться автоматически, то есть без повторного запроса у пользователя имени пользователя и пароля. Это означает, что один пользователь имеет доступ к нескольким службам, которые используют один и тот же сервер для проверки подлинности.

Как мы увидим позже, если пользователь полностью выходит из системы, его билет становится недействительным. Как следствие, пользователь выходит из всех приложений, подключенных к серверу одновременно. Это называется Single Logout.

Мы настраиваем bean-компонентAuthenticationEntryPoint с URL-адресом входа на сервер по умолчанию. Обратите внимание, что этот URL отличается от URL-адреса входа в систему. Этот URL-адрес входа на сервер - это место, куда пользователь будет перенаправлен для аутентификации.

TicketValidator - это bean-компонент, который приложение-служба использует для проверки билета службы, предоставленного пользователю после успешной аутентификации на сервере.

Поток это:

  1. Пользователь пытается получить доступ к защищенной странице

  2. AuthenticationEntryPoint запускается и переносит пользователя на сервер. Адрес для входа на сервер указан вAuthenticationEntryPoint

  3. При успешной аутентификации на сервере он перенаправляет запрос обратно на указанный URL-адрес службы, а билет службы добавляется в качестве параметра запроса.

  4. CasAuthenticationFilter сопоставляется с URL-адресом, который соответствует шаблону, и, в свою очередь, запускает внутреннюю проверку билета.

  5. Если билет действителен, пользователь будет перенаправлен на первоначально запрошенный URL

Теперь нам нужно настроить Spring Security для защиты некоторых маршрутов и использовать bean-компонентCasAuthenticationEntryPoint.

Давайте создадимSecurityConfig.java, который расширяетWebSecurityConfigurerAdapter и переопределяетconfig():

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
      http
        .authorizeRequests()
        .regexMatchers("/secured.*", "/login")
        .authenticated()
        .and()
        .authorizeRequests()
        .regexMatchers("/")
        .permitAll()
        .and()
        .httpBasic()
        .authenticationEntryPoint(authenticationEntryPoint);
    }
    // ...
}

Кроме того, в классеSecurityConfig мы переопределяем следующие методы и одновременно создаем bean-компонентCasAuthenticationFilter:

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private AuthenticationProvider authenticationProvider;
    private AuthenticationEntryPoint authenticationEntryPoint;
    private SingleSignOutFilter singleSignOutFilter;
    private LogoutFilter logoutFilter;

    @Autowired
    public SecurityConfig(CasAuthenticationProvider casAuthenticationProvider, AuthenticationEntryPoint eP,
                          LogoutFilter lF
                          , SingleSignOutFilter ssF
    ) {
        this.authenticationProvider = casAuthenticationProvider;
        this.authenticationEntryPoint = eP;

        this.logoutFilter = lF;
        this.singleSignOutFilter = ssF;

    }

    // ...

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      auth.authenticationProvider(authenticationProvider);
    }

    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
      return new ProviderManager(Arrays.asList(authenticationProvider));
    }

    @Bean
    public CasAuthenticationFilter casAuthenticationFilter(ServiceProperties sP) throws Exception {
      CasAuthenticationFilter filter = new CasAuthenticationFilter();
      filter.setServiceProperties(sP);
      filter.setAuthenticationManager(authenticationManager());
      return filter;
    }
}

Давайте создадим контроллеры, которые обрабатывают запросы, направленные к/secured,/login, а также к домашней странице.

Домашняя страница отображается наIndexController, у которого есть методindex().. Этот метод просто возвращает представление индекса:

@GetMapping("/")
public String index() {
    return "index";
}

Путь/login отображается на методlogin() из классаAuthController. Он просто перенаправляет на страницу успешного входа в систему по умолчанию.

Обратите внимание, что при настройкеHttpSecurity выше мы настроили путь/login так, чтобы он требовал аутентификации. Таким образом, мы перенаправляем пользователя на сервер CAS для аутентификации.

Этот механизм немного отличается от обычной конфигурации, где путь/login не является защищенным маршрутом и возвращает форму входа в систему:

@GetMapping("/login")
public String login() {
    return "redirect:/secured";
}

Путь/secured отображается на методindex() из классаSecuredPageController. Он получает имя пользователя, прошедшего аутентификацию, и отображает его как часть приветственного сообщения:

@GetMapping
public String index(ModelMap modelMap) {
  Authentication auth = SecurityContextHolder.getContext()
    .getAuthentication();
  if(auth != null
    && auth.getPrincipal() != null
    && auth.getPrincipal() instanceof UserDetails) {
      modelMap.put("username", ((UserDetails) auth.getPrincipal()).getUsername());
  }
  return "secure/index";
}

Обратите внимание, что все представления доступны в папкеresources папкиcas-secured-app. На этом этапеcas-secured-app должен иметь возможность использовать сервер для аутентификации.

Наконец, мы выполняемbuild run из терминала и одновременно запускаем загрузочное приложение Spring. Обратите внимание, что SSL является ключевым во всем этом процессе, поэтому не следует пропускать этап генерации SSL!

4.2. Настройка единого выхода

Перейдем кthe authentication process by logging out a user из системы. Пользователь может выйти из двух мест: клиентское приложение и сервер.

Выход пользователя из клиентского приложения / службы - это первое, что нужно сделать. Это не влияет на состояние аутентификации пользователя в других приложениях, подключенных к тому же серверу. Конечно, выход пользователя с сервера также приводит к выходу пользователя из всех других зарегистрированных служб / клиентов.

Начнем с определения некоторых конфигураций bean-компонентов в классеCasSecuredAppApplicaiton:

@Bean
public SecurityContextLogoutHandler securityContextLogoutHandler() {
    return new SecurityContextLogoutHandler();
}

@Bean
public LogoutFilter logoutFilter() {
    LogoutFilter logoutFilter = new LogoutFilter(
      "https://localhost:6443/cas/logout",
      securityContextLogoutHandler());
    logoutFilter.setFilterProcessesUrl("/logout/cas");
    return logoutFilter;
}

@Bean
public SingleSignOutFilter singleSignOutFilter() {
    SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
    singleSignOutFilter.setCasServerUrlPrefix("https://localhost:6443/cas");
    singleSignOutFilter.setIgnoreInitConfiguration(true);
    return singleSignOutFilter;
}

@EventListener
public SingleSignOutHttpSessionListener singleSignOutHttpSessionListener(
  HttpSessionEvent event) {
    return new SingleSignOutHttpSessionListener();
}

Мы настраиваемlogoutFilter для перехвата URL-шаблона/logout/cas и перенаправления приложения на сервер для выхода из системы в целом. Сервер отправляет один запрос на выход из системы всем заинтересованным службам. Такой запрос обрабатываетсяSingleSignOutFilter,, что делает недействительным сеанс HTTP.

Давайте изменим конфигурациюHttpSecurity вconfig() классаSecurityConfig. CasAuthenticationFilter иLogoutFilter, которые были настроены ранее, теперь также добавляются в цепочку:

http
  .authorizeRequests()
  .regexMatchers("/secured.*", "/login")
  .authenticated()
  .and()
  .authorizeRequests()
  .regexMatchers("/")
  .permitAll()
  .and()
  .httpBasic()
  .authenticationEntryPoint(authenticationEntryPoint)
  .and()
  .logout().logoutSuccessUrl("/logout")
  .and()
  .addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class)
  .addFilterBefore(logoutFilter, LogoutFilter.class);

Для правильной работы выхода из системы мы должны реализовать методlogout(), который сначала выполняет локальный выход пользователя из системы и показывает страницу со ссылкой для дополнительного выхода пользователя из всех других служб, подключенных к серверу.

Ссылка такая же, как и ссылка, заданная как URL-адрес процесса фильтрацииLogoutFilter, который мы настроили выше:

@GetMapping("/logout")
public String logout(
  HttpServletRequest request,
  HttpServletResponse response,
  SecurityContextLogoutHandler logoutHandler) {
    Authentication auth = SecurityContextHolder
      .getContext().getAuthentication();
    logoutHandler.logout(request, response, auth );
    new CookieClearingLogoutHandler(
      AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY)
      .logout(request, response, auth);
    return "auth/logout";
}

Вид выхода из системы:



    Cas Secured App - Logout


You have logged out of Cas Secured Spring Boot App Successfully


Log out of all other Services

5. Подключение сервера CAS к базе данных

Мы использовали статические учетные данные пользователя для аутентификации. Однако в производственных средах учетные данные пользователя хранятся в базе данных большую часть времени. Итак, теперьwe show how to connect our server to a MySQL database (database name: test) работает локально.

Мы делаем это, добавляя следующие данные в файлapplication.properties в каталогеcas-server/src/main/resources:

cas.authn.accept.users=
cas.authn.accept.name=

cas.authn.jdbc.query[0].sql=SELECT * FROM users WHERE email = ?
cas.authn.jdbc.query[0].url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialect
cas.authn.jdbc.query[0].user=root
cas.authn.jdbc.query[0].password=root
cas.authn.jdbc.query[0].ddlAuto=none
cas.authn.jdbc.query[0].driverClass=com.mysql.cj.jdbc.Driver
cas.authn.jdbc.query[0].fieldPassword=password
cas.authn.jdbc.query[0].passwordEncoder.type=NONE

Помните, что полное содержаниеapplication.properties можно найти в исходном коде. Leaving the value of cas.authn.accept.users blank deactivates the use of static user repositories by the server.

Кроме того, мы определяем оператор SQL, который получает пользователей из базы данных. Возможность настройки самого SQL делает хранилище пользователей в базе данных очень гибким.

Согласно приведенному выше SQL запись пользователя хранится в таблицеusers. Столбецemail представляет собой принципала пользователя (имя пользователя). Далее по настройке мы устанавливаем имя поля пароля,cas.authn.jdbc.query[0].fieldPassword.. Мы устанавливаем его на значениеpassword, чтобы еще больше повысить гибкость.

Другими атрибутами, которые мы настроили, являются пользователь базы данных (root) и пароль (пустой), диалект и соединение JDBCString. Список поддерживаемых баз данных, доступных драйверов и диалектов можно найтиhere .

Another essential attribute is the encryption type used for storing the password. В этом случае он установлен на NONE.

Однако сервер поддерживает больше механизмов шифрования, таких как Bcrypt. Эти механизмы шифрования, которые можно найтиhere, вместе с другими настраиваемыми свойствами.

Запуск сервера (build run) теперь включает аутентификацию пользователей с учетными данными, которые присутствуют в настроенной базе данных. Note again that the principal in the database that the server uses must be the same as that of the client applications.

В этом случае приложение Spring Boot должно иметь то же значение ([email protected]) для принципала (username), что и база данных, подключенная к серверу.

Затем давайте изменимUserDetails, подключенный к bean-компонентуCasAuthenticationProvider, настроенному в классеCasSecuredAppApplication приложения Spring Boot:

@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
    CasAuthenticationProvider provider = new CasAuthenticationProvider();
    provider.setServiceProperties(serviceProperties());
    provider.setTicketValidator(ticketValidator());
    provider.setUserDetailsService((s) -> new User(
      "[email protected]", "testU",
      true, true, true, true,
    AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
    provider.setKey("CAS_PROVIDER_LOCALHOST_9000");
    return provider;
}

Еще одна вещь, на которую следует обратить внимание, это то, что, хотяUserDetails задан паролем, он не используется. Однако, если имя пользователя отличается от имени сервера, аутентификация не удастся.

Для успешной аутентификации приложения с учетными данными, хранящимися в базе данных, запустите сервер MySQL, работающий на 127.0.0.1, и порт 3306 с именем пользователя root и паролем root.

Затем используйте файл SQLcas-server\src\main esources\create_test_db_and_users_tbl.sql,, который является частьюsource code, чтобы создать таблицуusers в базе данныхtest.

По умолчанию он содержит адрес электронной почты[email protected] и парольMellon.. Помните, что мы всегда можем изменить настройки подключения к базе данных вapplication.properties..

Запустите CAS Server еще раз сbuild run,, перейдите кhttps://localhost:6443/cas и используйте эти учетные данные для аутентификации. Те же учетные данные будут работать и для защищенного приложения Spring Boot.

6. Заключение

Мы подробно рассмотрели, как использовать систему единого входа на сервере CAS с Spring Security и многие задействованные файлы конфигурации.

Существует множество других аспектов сервера, которые могут быть настроены в диапазоне от тем и типов протоколов до политик аутентификации. Все это можно найтиhere в документации.

Исходный код сервера в этой статье и его файлы конфигурации можно найтиhere, а код приложения Spring Boot можно найтиhere.

Related