CAS SSO с пролетна сигурност

1. Общ преглед

В този урок ще разгледаме Apereo Central Authentication Service (CAS) и ще видим как услугата Spring Boot може да я използва за удостоверяване. CAS е корпоративно решение за единен вход (SSO), което също е с отворен код.

Какво е SSO? Когато влезете в YouTube, Gmail и Maps с едни и същи идентификационни данни, това е Single Sign-On. Ще демонстрираме това, като настроим CAS сървър и приложение Spring Spring. Приложението Spring Boot ще използва CAS за удостоверяване.

2. Настройка на CAS сървър

2.1. Инсталиране на CAS и зависимости

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

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

Тази команда ще клонира шаблона на cas-overlay в директорията на 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 и да изпълним командата, за да генерираме keykey :

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

За да нямаме SSL грешка при ръкостискане, трябва да използваме localhost като стойността на собственото и фамилното име. Трябва да използваме същото за името на организацията и звеното. Освен това трябва да импортираме thekeystore в JDK / JRE, който ще използваме за стартиране на нашето клиентско приложение:

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

Паролата за хранилището на ключове източник и местоназначение е changeit . В системите на Unix може да се наложи да изпълним тази команда с привилегия admin ( sudo ). След импортирането трябва да рестартираме всички екземпляри на Java, които работят или рестартираме системата.

Използваме JDK11, защото това се изисква от CAS версия 6.1.x. Също така дефинирахме променливата на средата $ JAVA11_HOME, която сочи към нейната начална директория. Вече можем да стартираме CAS сървъра:

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

Когато приложението стартира, ще видим „ГОТОВ“ на терминала и сървърът ще бъде достъпен на // localhost: 8443 .

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

Все още не можем да влезем, тъй като не сме конфигурирали нито един потребител. CAS има различни методи за управление на конфигурацията, включително самостоятелния режим. Нека създадем конфигурационна папка cas-server / src / main / resources / etc / cas / config, в която ще създадем файл със свойства cas.properties . Сега можем да дефинираме статичен потребител във файла със свойства:

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

Трябва да съобщим местоположението на конфигурационната папка на CAS сървъра, за да влязат в сила настройките. Нека актуализираме task.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 е абсолютен път . Вече можем да отидем на // localhost: 8443 и да влезем с потребителско име и парола Mellon .

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

Ще използваме Spring Initializr, за да генерираме клиентско приложение Spring Boot. Той ще трябва Web , сигурност , Freemarker и Инструментите за програмисти зависимости. Освен това ще добавим и зависимостта за CAS модула Spring Security към неговия pom.xml :

 org.springframework.security spring-security-cas 5.3.0.RELEASE 

И накрая, нека добавим следните свойства на 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" : "//localhost:8900/login/cas", "name" : "casSecuredApp", "id" : 8900, "logoutType" : "BACK_CHANNEL", "logoutUrl" : "//localhost:8900/exit/cas" }

В serviceId атрибут определя регулярен URL модел за заявката на клиента. Шаблонът трябва да съответства на URL адреса на клиентското приложение.

The ID атрибут трябва да бъде уникално. С други думи, не трябва да има две или повече услуги с един и същ идентификатор, регистрирани на един и същ CAS сървър. Наличието на дублирани идентификатори ще доведе до конфликти и отменяне на конфигурациите.

Също така конфигурираме типа излизане да бъде BACK_CHANNEL, а URL адресът да бъде // 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 последователност.

Let's add the following bean configurations to the CasSecuredApplication class of our Spring Boot app:

@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("//cas-client:8900/login/cas"); serviceProperties.setSendRenew(false); return serviceProperties; } @Bean public TicketValidator ticketValidator() { return new Cas30ServiceTicketValidator("//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("[email protected]", "Mellon", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_ADMIN"))); provider.setKey("CAS_PROVIDER_LOCALHOST_8900"); return provider; }

The ServiceProperties bean has the same URL as the serviceId in casSecuredApp-8900.json. This is important because it identifies this client to the CAS server.

The sendRenew property of ServiceProperties is set to false. This means a user only needs to present login credentials to the server once.

The AuthenticationEntryPoint bean will handle authentication exceptions. Thus, it'll redirect the user to the login URL of the CAS server for authentication.

In summary, the authentication flow goes:

  1. A user attempts to access a secure page, which triggers an authentication exception
  2. The exception triggers AuthenticationEntryPoint. In response, the AuthenticationEntryPoint will take the user to the CAS server login page – //localhost:8443/login
  3. On successful authentication, the server redirects back to the client with a ticket
  4. CasAuthenticationFilter will pick up the redirect and call CasAuthenticationProvider
  5. CasAuthenticationProvider will use TicketValidator to confirm the presented ticket on CAS server
  6. If the ticket is valid, the user will get a redirection to the requested secure URL

Finally, let's configure HttpSecurity to secure some routes in WebSecurityConfig. In the process, we'll also add the authentication entry point for exception handling:

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

6. CAS Client Single Logout Configuration

So far, we've dealt with single sign-on; let's now consider CAS single logout (SLO).

Applications that use CAS for managing user authentication can log out a user from two places:

  • The client application can logout a user from itself locally – this will not affect the user's login status in other applications using the same CAS server
  • The client application can also log out the user from the CAS server – this will cause the user to be logged out from all other client apps connected to the same CAS server.

We'll first put in place logout on the client application and then extend it to single logout on the CAS server.

In order to make obvious what goes on behind the scene, we'll create a logout() method to handle the local logout. On success, it'll redirect us to a page with a link for single 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"; }

In the single logout process, the CAS server will first expire the user's ticket and then send an async request to all registered client apps. Each client app that receives this signal will perform a local logout. Thereby accomplishing the goal of logout once, it will cause a log out everywhere.

Having said that, let's add some bean configurations to our client app. Specifically, in the CasSecuredApplicaiton:

@Bean public SecurityContextLogoutHandler securityContextLogoutHandler() { return new SecurityContextLogoutHandler(); } @Bean public LogoutFilter logoutFilter() { LogoutFilter logoutFilter = new LogoutFilter("//localhost:8443/logout", securityContextLogoutHandler()); logoutFilter.setFilterProcessesUrl("/logout/cas"); return logoutFilter; } @Bean public SingleSignOutFilter singleSignOutFilter() { SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter(); singleSignOutFilter.setCasServerUrlPrefix("//localhost:8443"); singleSignOutFilter.setLogoutCallbackPath("/exit/cas"); singleSignOutFilter.setIgnoreInitConfiguration(true); return singleSignOutFilter; }

The logoutFilter will intercept requests to /logout/cas and redirect the application to the CAS server. The SingleSignOutFilter will intercept requests coming from the CAS server and perform the local logout.

7. Connecting the CAS Server to a Database

We can configure the CAS server to read credentials from a MySQL database. We'll use the test database of a MySQL server that's running in a local machine. Let's update 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

We set the cas.authn.accept.users to blank. This will deactivate the use of static user repositories by the CAS server.

According to the SQL above, users' credentials are stored in the users table. The email column is what represents the users' principal (username).

Please make sure to check the list of supported databases, available drivers and dialects. We also set the password encoder type to NONE. Other encryption mechanisms and their peculiar properties are also available.

Note that the principal in the database of the CAS server must be the same as that of the client application.

Let's update CasAuthenticationProvider to have the same username as the CAS server:

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

CasAuthenticationProvider не използва паролата за удостоверяване. Независимо от това, потребителското му име трябва да съвпада с това на CAS сървъра, за да бъде успешно удостоверяването. CAS сървърът изисква MySQL сървър да работи на localhost на порт 3306 . Потребителското име и паролата трябва да са root .

Рестартирайте отново CAS сървъра и приложението Spring Boot. След това използвайте новите идентификационни данни за удостоверяване.

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

Разгледахме как да използваме CAS SSO с Spring Security и много от участващите конфигурационни файлове. Има много други аспекти на CAS SSO, който може да се конфигурира. В диапазона от теми и типове протоколи до политики за удостоверяване.

Тези и други са в документите. Изходният код за CAS сървъра и приложението Spring Boot е достъпен в GitHub.