CAS SSO Dengan Keamanan Musim Semi

1. Ikhtisar

Dalam tutorial ini, kita akan melihat Apereo Central Authentication Service (CAS) dan kita akan melihat bagaimana layanan Spring Boot dapat menggunakannya untuk otentikasi. CAS adalah solusi Single Sign-On (SSO) perusahaan yang juga open source.

Apa itu SSO? Saat Anda masuk ke YouTube, Gmail, dan Maps dengan kredensial yang sama, itulah Sistem Masuk Tunggal. Kami akan mendemonstrasikan ini dengan menyiapkan server CAS dan aplikasi Spring Boot. Aplikasi Spring Boot akan menggunakan CAS untuk otentikasi.

2. Pengaturan CAS Server

2.1. Instalasi dan Dependensi CAS

Server menggunakan gaya Maven (Gradle) War Overlay untuk memudahkan penyiapan dan penerapan:

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

Perintah ini akan mengkloning template cas-overlay ke direktori cas-server .

Beberapa aspek yang akan kami bahas termasuk pendaftaran layanan JSON dan koneksi database JDBC. Jadi, kami akan menambahkan modul mereka ke dependensi bagian dari build.gradle berkas:

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

Mari pastikan untuk memeriksa versi terbaru dari casServer.

2.2. Konfigurasi CAS Server

Sebelum kita dapat memulai CAS server, kita perlu menambahkan beberapa konfigurasi dasar. Mari kita mulai dengan membuat folder cas-server / src / main / resources dan di folder ini. Ini akan diikuti dengan pembuatan application.properties di folder tersebut juga:

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

Mari lanjutkan dengan pembuatan file key-store yang direferensikan dalam konfigurasi di atas. Pertama, kita perlu membuat folder / etc / cas dan / etc / cas / config di cas-server / src / main / resources .

Kemudian, kita perlu mengubah direktori menjadi cas-server / src / main / resources / etc / cas dan menjalankan perintah untuk menghasilkan keystore :

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

Agar kita tidak mengalami kesalahan handshake SSL, kita harus menggunakan localhost sebagai nilai nama depan dan belakang. Kita harus menggunakan yang sama untuk nama dan unit organisasi juga. Selanjutnya, kita perlu mengimpor keystore ke JDK / JRE yang akan kita gunakan untuk menjalankan aplikasi klien kita:

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

Kata sandi untuk keystore sumber dan tujuan adalah changeit . Pada sistem Unix, kita mungkin harus menjalankan perintah ini dengan hak istimewa admin ( sudo ). Setelah mengimpor, kita harus memulai ulang semua contoh Java yang menjalankan atau memulai ulang sistem.

Kami menggunakan JDK11 karena diperlukan oleh CAS versi 6.1.x. Juga, kami mendefinisikan variabel lingkungan $ JAVA11_HOME yang mengarah ke direktori utamanya. Kami sekarang dapat memulai server CAS:

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

Saat aplikasi dimulai, kita akan melihat "SIAP" tercetak di terminal dan server akan tersedia di // localhost: 8443 .

2.3. Konfigurasi Pengguna CAS Server

Kami belum dapat masuk karena kami belum mengkonfigurasi pengguna mana pun. CAS memiliki metode berbeda dalam mengelola konfigurasi, termasuk mode mandiri. Mari buat folder config cas-server / src / main / resources / etc / cas / config di mana kita akan membuat file properti cas.properties . Sekarang, kita dapat mendefinisikan pengguna statis di file properti:

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

Kami harus mengkomunikasikan lokasi folder config ke CAS server agar pengaturan diterapkan. Mari perbarui task.gradle sehingga kita bisa meneruskan lokasi sebagai argumen JVM dari baris perintah:

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}" } } }

Kami kemudian menyimpan file dan menjalankan:

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

Harap perhatikan bahwa nilai cas.standalone.configurationDirectory adalah jalur absolut . Sekarang kita dapat pergi ke // localhost: 8443 dan masuk dengan nama pengguna casuser dan kata sandi Mellon .

3. Setup Klien CAS

Kami akan menggunakan Spring Initializr untuk membuat aplikasi klien Spring Boot. Ini akan memiliki dependensi Web , Keamanan , Freemarker dan DevTools . Selain itu, kami juga akan menambahkan dependensi untuk modul Spring Security CAS ke pom.xml -nya :

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

Terakhir, mari tambahkan properti Spring Boot berikut untuk mengonfigurasi aplikasi:

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

4. Registrasi Layanan CAS Server

Aplikasi klien harus mendaftar dengan server CAS sebelum otentikasi apa pun . Server CAS mendukung penggunaan registri klien YAML, JSON, MongoDB dan LDAP.

Dalam tutorial ini, kita akan menggunakan metode JSON Service Registry. Mari buat folder lain cas-server / src / main / resources / etc / cas / services . Folder inilah yang akan menampung file JSON registri layanan.

Kami akan membuat file JSON yang berisi definisi aplikasi klien kami. Nama file, casSecuredApp-8900.json, mengikuti pola 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" }

The ServiceID atribut mendefinisikan pola URL regex untuk aplikasi klien. Pola tersebut harus cocok dengan URL aplikasi klien.

The id atribut harus unik. Dengan kata lain, seharusnya tidak ada dua atau lebih layanan dengan id yang sama yang terdaftar di server CAS yang sama. Memiliki id duplikat akan menyebabkan konflik dan menimpa konfigurasi.

Kita juga mengkonfigurasi tipe logout menjadi BACK_CHANNEL dan URL-nya menjadi // localhost: 8900 / exit / cas sehingga kita dapat melakukan satu logout nanti. Sebelum CAS server dapat menggunakan file konfigurasi JSON kita, kita harus mengaktifkan registry JSON di cas.properties kita :
cas.serviceRegistry.initFromJson=true cas.serviceRegistry.json.location=classpath:/etc/cas/services

5. Konfigurasi Sistem Masuk Tunggal CAS Client

Langkah selanjutnya bagi kami adalah mengonfigurasi Spring Security agar berfungsi dengan server CAS. Kita juga harus memeriksa aliran penuh interaksi, yang disebut urutan 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 tidak menggunakan sandi untuk otentikasi. Meskipun demikian, nama penggunanya harus sama dengan yang ada di CAS server agar otentikasi berhasil. Server CAS membutuhkan server MySQL untuk dijalankan di localhost di port 3306 . Nama pengguna dan kata sandi harus root .

Mulai ulang server CAS dan aplikasi Spring Boot sekali lagi. Kemudian gunakan kredensial baru untuk otentikasi.

8. Kesimpulan

Kami telah melihat bagaimana menggunakan CAS SSO dengan Spring Security dan banyak file konfigurasi yang terlibat. Ada banyak aspek lain dari CAS SSO yang dapat dikonfigurasi. Mulai dari tema dan tipe protokol hingga kebijakan otentikasi.

Ini dan lainnya ada di dokumen. Kode sumber untuk server CAS dan aplikasi Spring Boot tersedia di GitHub.