Перейти к основному содержимому

CAS SSO с Spring Security

· 9 мин. чтения

1. Обзор

В этом руководстве мы рассмотрим службу Apereo Central Authentication Service (CAS) и увидим, как служба Spring Boot может использовать ее для аутентификации. CAS — это корпоративное решение единого входа (SSO) с открытым исходным кодом.

Что такое ССО? Когда вы входите в YouTube, Gmail и Карты с одинаковыми учетными данными, это единый вход. Мы собираемся продемонстрировать это, настроив сервер CAS и приложение Spring Boot. Приложение Spring Boot будет использовать CAS для аутентификации.

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

2.1. Установка CAS и зависимости

Сервер использует стиль Maven (Gradle) War Overlay для упрощения настройки и развертывания:

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

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

Некоторые из аспектов, которые мы рассмотрим, включают регистрацию службы JSON и подключение к базе данных JDBC. Итак, мы добавим их модули в раздел зависимостей файла build.gradle :

compile "org.apereo.cas:cas-server-support-json-service-registry:${casServerVersion}"
compile "org.apereo.cas:cas-server-support-jdbc:${casServerVersion}"

Обязательно проверим последнюю версию casServer .

2.2. Конфигурация CAS-сервера

Прежде чем мы сможем запустить сервер CAS, нам нужно добавить некоторые базовые конфигурации. Начнем с создания папки cas-server/src/main/resources и в этой папке. За этим последует создание application.properties в папке:

server.port=8443
spring.main.allow-bean-definition-overriding=true
server.ssl.key-store=classpath:/etc/cas/thekeystore
server.ssl.key-store-password=changeit

Давайте приступим к созданию файла хранилища ключей, упомянутого в приведенной выше конфигурации. Во-первых, нам нужно создать папки /etc/cas и /etc/cas/config в cas-server/src/main/resources .

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

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

Чтобы у нас не было ошибки рукопожатия SSL, мы должны использовать localhost в качестве значения имени и фамилии. Мы должны использовать то же самое для названия организации и подразделения. Кроме того, нам нужно импортировать хранилище ключей в JDK/JRE, которое мы будем использовать для запуска нашего клиентского приложения:

keytool -importkeystore -srckeystore thekeystore -destkeystore $JAVA11_HOME/jre/lib/security/cacerts

Пароль для исходного и целевого хранилища ключей — changeit . В системах Unix нам может потребоваться запустить эту команду с правами администратора ( sudo ). После импорта мы должны перезапустить все запущенные экземпляры Java или перезагрузить систему.

Мы используем JDK11, потому что это требуется CAS версии 6.1.x. Кроме того, мы определили переменную окружения $JAVA11_HOME, указывающую на его домашний каталог. Теперь мы можем запустить сервер CAS:

./gradlew run -Dorg.gradle.java.home=$JAVA11_HOME

Когда приложение запустится, на терминале появится надпись READY, и сервер будет доступен по адресу https://localhost:8443 .

2.3. Конфигурация пользователя сервера CAS

Мы еще не можем войти в систему, так как мы не настроили ни одного пользователя. CAS имеет различные методы управления конфигурацией , включая автономный режим. Давайте создадим папку конфигурации cas-server/src/main/resources/etc/cas/config , в которой мы создадим файл свойств cas.properties . Теперь мы можем определить статического пользователя в файле свойств:

cas.authn.accept.users=casuser::Mellon

Мы должны сообщить расположение папки конфигурации серверу CAS, чтобы настройки вступили в силу. Давайте обновим tasks.gradle , чтобы мы могли передавать местоположение в качестве аргумента JVM из командной строки:

task run(group: "build", description: "Run the CAS web application in embedded container mode") {
dependsOn 'build'
doLast {
def casRunArgs = new ArrayList<>(Arrays.asList(
"-server -noverify -Xmx2048M -XX:+TieredCompilation -XX:TieredStopAtLevel=1".split(" ")))
if (project.hasProperty('args')) {
casRunArgs.addAll(project.args.split('\\s+'))
}
javaexec {
main = "-jar"
jvmArgs = casRunArgs
args = ["build/libs/${casWebApplicationBinaryName}"]
logger.info "Started ${commandLine}"
}
}
}

Затем мы сохраняем файл и запускаем:

./gradlew run
-Dorg.gradle.java.home=$JAVA11_HOME
-Pargs="-Dcas.standalone.configurationDirectory=/cas-server/src/main/resources/etc/cas/config"

Обратите внимание, что значением cas.standalone.configurationDirectory является абсолютный путь . Теперь мы можем перейти на https://localhost:8443 и войти в систему с именем пользователя casuser и паролем Mellon .

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

Мы будем использовать Spring Initializr для создания клиентского приложения Spring Boot. Он будет иметь зависимости Web , Security , Freemarker и DevTools . Кроме того, мы также добавим зависимость для модуля CAS Spring Security в его pom.xml :

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
<versionId>5.3.0.RELEASE</versionId>
</dependency>

Наконец, давайте добавим следующие свойства Spring Boot для настройки приложения:

server.port=8900
spring.freemarker.suffix=.ftl

4. Регистрация службы сервера CAS

Клиентские приложения должны зарегистрироваться на сервере CAS перед любой аутентификацией . Сервер CAS поддерживает использование клиентских реестров YAML, JSON, MongoDB и LDAP.

В этом руководстве мы будем использовать метод JSON Service Registry. Создадим еще одну папку cas-server/src/main/resources/etc/cas/services . Именно в этой папке будут храниться файлы JSON реестра службы.

Мы создадим файл JSON, содержащий определение нашего клиентского приложения. Имя файла casSecuredApp-8900.json соответствует шаблону s erviceName-Id.json :

{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "http://localhost:8900/login/cas",
"name" : "casSecuredApp",
"id" : 8900,
"logoutType" : "BACK_CHANNEL",
"logoutUrl" : "http://localhost:8900/exit/cas"
}

Атрибут serviceId определяет шаблон URL регулярного выражения для клиентского приложения. Шаблон должен соответствовать URL-адресу клиентского приложения.

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

Мы также настраиваем тип выхода из системы как BACK_CHANNEL и URL-адрес как http://localhost:8900/exit/cas , чтобы мы могли выполнить один выход позже.

Прежде чем сервер CAS сможет использовать наш файл конфигурации JSON, мы должны включить реестр JSON в нашем cas.properties :

cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.json.location=classpath:/etc/cas/services

5. Конфигурация единого входа клиента CAS

Следующим шагом для нас является настройка Spring Security для работы с сервером CAS. Мы также должны проверить полный поток взаимодействий , называемый CAS-последовательностью.

Давайте добавим следующие конфигурации bean-компонентов в класс CasSecuredApplication нашего приложения Spring Boot:

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

@Bean
public ServiceProperties serviceProperties() {
logger.info("service properties");
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService("http://cas-client:8900/login/cas");
serviceProperties.setSendRenew(false);
return serviceProperties;
}

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

@Bean
public CasAuthenticationProvider casAuthenticationProvider(
TicketValidator ticketValidator,
ServiceProperties serviceProperties) {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties(serviceProperties);
provider.setTicketValidator(ticketValidator);
provider.setUserDetailsService(
s -> new User("test@test.com", "Mellon", true, true, true, true,
AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
provider.setKey("CAS_PROVIDER_LOCALHOST_8900");
return provider;
}

Компонент ServiceProperties имеет тот же URL-адрес, что и serviceId в casSecuredApp-8900.json . Это важно, потому что он идентифицирует этот клиент для сервера CAS.

Для свойства sendRenew объекта ServiceProperties установлено значение false . Это означает, что пользователю нужно предоставить учетные данные для входа на сервер только один раз.

Bean - компонент AuthenticationEntryPoint будет обрабатывать исключения аутентификации. Таким образом, он перенаправит пользователя на URL-адрес входа на сервер CAS для аутентификации.

Таким образом, процесс аутентификации выглядит следующим образом:

  1. Пользователь пытается получить доступ к защищенной странице, что вызывает исключение аутентификации.
  2. Исключение вызывает AuthenticationEntryPoint . В ответ AuthenticationEntryPoint перенаправит пользователя на страницу входа на сервер CAS — https://localhost:8443/login.
  3. При успешной аутентификации сервер перенаправляет обратно клиенту с билетом
  4. CasAuthenticationFilter подхватит перенаправление и вызовет CasAuthenticationProvider.
  5. CasAuthenticationProvider будет использовать TicketValidator для подтверждения представленного билета на сервере CAS.
  6. Если билет действителен, пользователь получит перенаправление на запрошенный безопасный URL-адрес.

Наконец, давайте настроим HttpSecurity для защиты некоторых маршрутов в WebSecurityConfig . В процессе мы также добавим точку входа аутентификации для обработки исключений:

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

6. Конфигурация единого выхода клиента CAS

До сих пор мы имели дело с единым входом в систему; давайте теперь рассмотрим единый выход CAS (SLO).

Приложения, использующие CAS для управления аутентификацией пользователей, могут выполнить выход пользователя из двух мест:

  • Клиентское приложение может локально вывести пользователя из себя — это не повлияет на статус входа пользователя в другие приложения, использующие тот же CAS-сервер.
  • Клиентское приложение также может выполнить выход пользователя из сервера CAS — это приведет к выходу пользователя из всех других клиентских приложений, подключенных к тому же серверу CAS.

Сначала мы реализуем выход из клиентского приложения, а затем расширим его до единого выхода на сервере CAS.

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

@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 сначала истечет срок действия билета пользователя, а затем отправит асинхронный запрос всем зарегистрированным клиентским приложениям. Каждое клиентское приложение, получившее этот сигнал, выполнит локальный выход. Таким образом, достижение цели выхода из системы один раз приведет к выходу из системы везде.

Сказав это, давайте добавим некоторые конфигурации компонентов в наше клиентское приложение. В частности, в CasSecuredApplication :

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

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

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

logoutFilter будет перехватывать запросы к /logout/cas и перенаправлять приложение на CAS-сервер. SingleSignOutFilter будет перехватывать запросы, поступающие от сервера CAS, и выполнять локальный выход из системы.

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

Мы можем настроить сервер CAS для чтения учетных данных из базы данных MySQL. Мы будем использовать тестовую базу данных сервера MySQL, работающего на локальном компьютере. Обновим cas-server/src/main/resources/etc/cas/config/cas.properties :

cas.authn.accept.users=

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

Мы устанавливаем cas.authn.accept.users пустым. Это деактивирует использование статических пользовательских репозиториев сервером CAS.

Согласно приведенному выше SQL, учетные данные пользователей хранятся в таблице пользователей . Столбец электронной почты — это то, что представляет участника пользователя ( имя пользователя ).

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

Обратите внимание, что субъект в базе данных сервера CAS должен быть таким же, как и у клиентского приложения.

Давайте обновим CasAuthenticationProvider , чтобы он имел то же имя пользователя, что и сервер CAS:

@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties(serviceProperties());
provider.setTicketValidator(ticketValidator());
provider.setUserDetailsService(
s -> new User("test@test.com", "Mellon", true, true, true, true,
AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
provider.setKey("CAS_PROVIDER_LOCALHOST_8900");
return provider;
}

CasAuthenticationProvider не использует пароль для аутентификации. Тем не менее, его имя пользователя должно совпадать с именем сервера CAS для успешной аутентификации. Сервер CAS требует, чтобы сервер MySQL работал на локальном хосте с портом 3306 . Имя пользователя и пароль должны быть root .

Перезапустите сервер CAS и приложение Spring Boot еще раз. Затем используйте новые учетные данные для аутентификации.

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

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

Эти и другие есть в документации . Исходный код сервера CAS и приложения Spring Boot доступен на GitHub.