Poprzedni wpis dotyczący Keycloaka zakończyliśmy na jego budowie. Skoro już znamy budowę Keycloaka, wiemy jak go zainstalować pora na integrację z backendem naszej aplikacji. Słowo „Prosta” oznaczania najprostsza formę integracji Keycloaka ze Springiem, zaawansowana pojawi się niebawem.
Pierwszą rzeczą jaką musimy zrobić to przygotować serwer Keycloak, korzystając z docker-compose który znajdziemy na poprzednim wpisie. Następnie wchodzimy na stronę http://localhost:8080/auth/admin/ logując się wcześniej ustawionymi danymi poprzez zmienne środowiskowe.
Fragment docker-compose.yml:
environment:
- "KEYCLOAK_USER=admin"
- "KEYCLOAK_PASSWORD=admin"
Wpisujemy dowlona nazwe reprezentująca zbiór naszych aplikacji (nazwe projektu)
Oczywiśnie możemy importować konfiguracje z już istniejącego realmu i na bazie niego tworzyc. Klikamy Create
Poprawne stworzenie realma powinno sie zakończyć stroną administracjna jego
Kolejną rzeczą jest dodanie obiektu client, który reprezentuje naszą aplikacje (webowa, mobilna itd), w tym celu musimy przejść do zakładki Clients.
Nastepnie w prawym rogu, klikamy create i wpisujemy nazwe naszej aplikacji.
zostawiamy client-protocol „openid-connect” i klimamy save . Po poprawnym stworzeniu pojawi się na strona administracjna tego klienta
Ustawiamy Access Type na Confidential co daje nam to ze aby appka springowa mogła korzystać z tego clienta musi posiadać specjany secret.
Service Accounts Enabled na ON co pozwala appce springowej na pobranie access tokenu dla swoich celów.
Zapisujemy ustawienia i przechodzimy do zakładki Credentials, kopjujemy Secret gdyż go ustawimy w aplikacji Springowej.
Kolejnym krokiem będzie utworzenie ról, definiujących posiadanie danych funkcjonalności tzn dostępów do konkretnych endpointów.Na potrzeby bloga stworze 2 role admin i user.
Przechodzimy do sekcji Roles we wcześniej zdefiniowanym kliencie -> Add Role i dodajemy Role
Następnie tworzymy role globalne w stworzonym realmie. Da nam to tą własność że będziemy mogli przypisać jedna role do usera, która będzie się różnie zachowywać w zależności od klienta – Rola globalna jakby agreguje specyficzne role klientów (aplikacji)
Teraz jak już wcześniej wspomniałem wiążemy 2 globalne role z rola danego klienta(aplikacji)
Globalna rola Spring-app-test-admin -> lokalna rola klienta – admin, Globalna rola Spring-app-test-user-> user. Tworzymy 2 uzytkownikow testowych (user-test,user-admin) i ustawiamy im hasla
Na koniec przypisujemy do naszych użytkowników role globalne
Kolejnym krokiem będzie konfiguracja aplikacji Spring Boota z Keycloakiem. Pierwszym krokiem będzie wygenerowanie szkieletu aplikacji poprzez stronę start.spring.io
Istotnym elemnetem jest dodanie 2 zależnosci Spring Security i Spring Web a nastepnie generujemy projekt i otwieramy w naszym ulubionym IDE.
Pierwszym krokiem będzie dodanie zależności dla Keycloaka, w pliku pom.xml dodajemy odpowiednie wpisy
<dependencies>
...
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>10.0.2</version>
</dependency>
...
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-adapter-bom</artifactId>
<version>10.0.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Kolejno edytujemy dane dostępowe do Keycloaka dla aplikacji w application.properties. Secret bedzie różnie generowany, wiec warto sprawdzić.
server.port = 8000
keycloak.realm = Projekt-testowy-sso
keycloak.auth-server-url = http://localhost:8080/auth
keycloak.ssl-required = external
keycloak.resource = spring-app-test
keycloak.credentials.secret = 01822bf2-1177-49fe-a1cf-454b540a65a0
keycloak.use-resource-role-mappings = true
keycloak.bearer-only = true
Kolejnym krokiem jest utworzenie konfiguracji spring security w oparciu o KeycloakWebSecurityConfigurerAdapter rozszerzajajcy klase bazowa WebSecurityConfigurer, dzięki czemu mozemy przeciazyc odpowiednie metody.
package pl.devopsi.springkeycloaktest.configuration;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
@Bean
public KeycloakConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.anyRequest()
.permitAll();
http.csrf().disable();
}
}
sessionAuthenticationStrategy() – definiuje session auth strategie
keycloakConfigResolver() – pozwala na uzywanie konfiguracji keycloaka poprzez properties
configure(AuthenticationManagerBuilder auth) -rejestruje KeycloakAuthenticationProvider w auth menadżerze
configure(HttpSecurity http) wymuaszmy aby wszystykie requesty wymagaly autoryzacji
Gdy juz mamy nasz konfiguracje Spring Security czas na testowe endpointy, które zweryfikują czy dany user ma odp role.
package pl.devopsi.springkeycloaktest.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.security.RolesAllowed;
@RestController
public class TestController {
@RolesAllowed("user")
@GetMapping("/user")
public String getAdmin(){
return "admin";
}
@RolesAllowed("admin")
@GetMapping("/admin")
public String getUser(){
return "user";
}
}
Tworzymy klasyczne kontrollery Spring MVC poprzez adnotacje @RestController i @GetMapping oraz nadajemy odpowiednie dostepy do endpointow poprzez @RolesAllowed, więc czas na testy.
Zacznijmy od pobrania access tokenu z keycloaka dla danego usera, podajac odpowiednie dane, ustawiane wcześniej
curl --location --request POST 'http://localhost:8080/auth/realms/Projekt-testowy-sso/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Cookie: JSESSIONID=6B7B9E9100D31DB815B2CE345AF86446.63bcd1903719; JSESSIONID=6B7B9E9100D31DB815B2CE345AF86446' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=spring-app-test' \
--data-urlencode 'client_secret=01822bf2-1177-49fe-a1cf-454b540a65a0' \
--data-urlencode 'username=user-admin' \
--data-urlencode 'password=test'
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJodlZhTlRGYllIU0JHWHhWSEYtcUJ3bTRyVWxwQVB5a19uc0RySEhRWlQ0In0.eyJleHAiOjE2MTU0NjM4MjksImlhdCI6MTYxNTQ2MzUyOSwianRpIjoiOGIwMDZlNTUtMzY0NC00MzgwLTg4OTAtZDdlNWJkNDNhMjQ2IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL1Byb2pla3QtdGVzdG93eS1zc28iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiOTcyOTNkOGItZjdlNi00NmVjLThhMmItZThmNmUyNjMxNmQ1IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic3ByaW5nLWFwcC10ZXN0Iiwic2Vzc2lvbl9zdGF0ZSI6Ijc4M2FlMzc5LWRkZDMtNGQ1Yy1hOWI5LTFjZDc5YjY4NGRhNyIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsic3ByaW5nLWFwcC10ZXN0LWFkbWluIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7InNwcmluZy1hcHAtdGVzdCI6eyJyb2xlcyI6WyJhZG1pbiJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ1c2VyLWFkbWluIn0.gHZxSfUwyWVxF3uOEIjx505zVe2LW_7PXpSRmNr2UrYkllq2WkR1WxBUpOsxjjzO0iGdCJbJAdYoCMnd1MY-MmhwseKEenfV4v1DxFNaTO1jSppt1oXAV_GMlcFI4_hE9jBD-d7pDR69oQmjYJr3QDbNGyDnKzPMrvDWyl_jatD6ek5Js8ogvFGRhDW7W64x8zaDBvbg47xNE-gbzIGfnglz2GkDbzyw1cE7PvxnncUhMhkNcsCriBeBiztJa4jQQLAL05YvtN1bXC05hab6JIEKYFLqI_a8R2krfFNhD0IGYySq_Jlifh9-JgkB8H25d4AtYUtaS1KcNQWuhbKJ_Q",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiNmQ5MWQxYi1kNzllLTQ0MzEtYjRmNS0xMjEwOWZhMTBlZDcifQ.eyJleHAiOjE2MTU0NjUzMjksImlhdCI6MTYxNTQ2MzUyOSwianRpIjoiYTcxNTYzYjItNGE2Zi00ZTVmLTk5ZTYtNDE3ZDA0ZWQ5ZjFjIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL1Byb2pla3QtdGVzdG93eS1zc28iLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvUHJvamVrdC10ZXN0b3d5LXNzbyIsInN1YiI6Ijk3MjkzZDhiLWY3ZTYtNDZlYy04YTJiLWU4ZjZlMjYzMTZkNSIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJzcHJpbmctYXBwLXRlc3QiLCJzZXNzaW9uX3N0YXRlIjoiNzgzYWUzNzktZGRkMy00ZDVjLWE5YjktMWNkNzliNjg0ZGE3Iiwic2NvcGUiOiJlbWFpbCBwcm9maWxlIn0.hNWAl6xpzzi0ja-qvRVN_a2V0YyNjZONUE_ohX5EEXM",
"token_type": "Bearer",
"not-before-policy": 0,
"session_state": "783ae379-ddd3-4d5c-a9b9-1cd79b684da7",
"scope": "email profile"
}
curl --location --request GET 'http://localhost:8000/admin' \
--header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJodlZhTlRGYllIU0JHWHhWSEYtcUJ3bTRyVWxwQVB5a19uc0RySEhRWlQ0In0.eyJleHAiOjE2MTU0NjM4MjksImlhdCI6MTYxNTQ2MzUyOSwianRpIjoiOGIwMDZlNTUtMzY0NC00MzgwLTg4OTAtZDdlNWJkNDNhMjQ2IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL1Byb2pla3QtdGVzdG93eS1zc28iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiOTcyOTNkOGItZjdlNi00NmVjLThhMmItZThmNmUyNjMxNmQ1IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic3ByaW5nLWFwcC10ZXN0Iiwic2Vzc2lvbl9zdGF0ZSI6Ijc4M2FlMzc5LWRkZDMtNGQ1Yy1hOWI5LTFjZDc5YjY4NGRhNyIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsic3ByaW5nLWFwcC10ZXN0LWFkbWluIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7InNwcmluZy1hcHAtdGVzdCI6eyJyb2xlcyI6WyJhZG1pbiJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ1c2VyLWFkbWluIn0.gHZxSfUwyWVxF3uOEIjx505zVe2LW_7PXpSRmNr2UrYkllq2WkR1WxBUpOsxjjzO0iGdCJbJAdYoCMnd1MY-MmhwseKEenfV4v1DxFNaTO1jSppt1oXAV_GMlcFI4_hE9jBD-d7pDR69oQmjYJr3QDbNGyDnKzPMrvDWyl_jatD6ek5Js8ogvFGRhDW7W64x8zaDBvbg47xNE-gbzIGfnglz2GkDbzyw1cE7PvxnncUhMhkNcsCriBeBiztJa4jQQLAL05YvtN1bXC05hab6JIEKYFLqI_a8R2krfFNhD0IGYySq_Jlifh9-JgkB8H25d4AtYUtaS1KcNQWuhbKJ_Q' \
--header 'Cookie: JSESSIONID=7D7D4C5CA8A7ACEC93276BDE350DCF19'
Caly kodzik znajdziemy na naszym Githubie