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 для аутентификации.
Таким образом, процесс аутентификации выглядит следующим образом:
- Пользователь пытается получить доступ к защищенной странице, что вызывает исключение аутентификации.
- Исключение вызывает
AuthenticationEntryPoint
. В ответAuthenticationEntryPoint
перенаправит пользователя на страницу входа на сервер CAS —https://localhost:8443/login.
- При успешной аутентификации сервер перенаправляет обратно клиенту с билетом
CasAuthenticationFilter
подхватит перенаправление и вызоветCasAuthenticationProvider.
CasAuthenticationProvider
будет использоватьTicketValidator
для подтверждения представленного билета на сервере CAS.- Если билет действителен, пользователь получит перенаправление на запрошенный безопасный 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.