Anmeldung in eine Spring-Boot-App hinzufügen
Dieser Schnellstart fügt einer Spring-Boot-4.1-App die KeenAuth-Anmeldung mit dem OAuth-2.0-Client von Spring Security hinzu, dem Standard-OpenID-Connect-Client des Spring-Ökosystems. KeenAuth hat noch kein Java-SDK, und ein solches ist auch nicht nötig: KeenAuth ist ein Standard-OpenID-Connect-Provider. Ihre App ist der OAuth-Client und bewahrt jedes Token in der serverseitigen Sitzung auf; der Browser erhält nur ein Sitzungs-ID-Cookie. Sie konfigurieren die Anmeldung, schützen eine Seite, rufen eine geschützte API mit dem Access Token des Benutzers auf, melden sich ab und testen die App offline.
Die App hat diese Routen:
| Route | Was sie tut |
|---|---|
GET / |
Öffentliche Startseite: ein Anmeldelink oder der Name des Benutzers und eine Schaltfläche zum Abmelden |
GET /dashboard |
Benötigt eine Sitzung. Zeigt den Benutzer und die Nachrichten, die Ihre API zurückgibt |
GET /oauth2/authorization/keenauth |
Von Spring Security bereitgestellt: startet die Anmeldung |
GET /login/oauth2/code/keenauth |
Von Spring Security bereitgestellt: der Callback |
POST /logout |
Von Spring Security bereitgestellt: beendet die Sitzung hier und bei KeenAuth. Benötigt das CSRF-Token |
Bevor Sie beginnen
- Eine KeenAuth-Umgebung. Ihr Issuer sieht etwa so aus:
https://your-environment.eu.keenauth.app. - Eine Webanwendung in der Umgebung (Anwendung anlegen) mit einem Client Secret (
client_secret_basic). Registrieren Sie in einerdevelopment-Umgebung den Redirect-URIhttp://localhost/login/oauth2/code/keenauthund den Post-Logout-Redirect-URIhttp://localhost/; ein Loopback-URI wird ohne Port registriert und passt auf jeden Port, etwa8080. - Eine API, die in der Umgebung registriert ist (API anlegen) mit dem Scope
messages:read. Ihridentifier, zum Beispielhttps://api.example.com, ist die Audience ihrer Access Tokens. Der Express-Schnellstart erstellt eine API, die diese Tokens akzeptiert. - JDK 21 und Maven 3.9.
1. Abhängigkeiten hinzufügen
Das Spring-Boot-Parent (spring-boot-starter-parent 4.1.2) legt jede Version fest. Fügen Sie die Starter für Web, OAuth-2.0-Client und Thymeleaf hinzu:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
<exclusions>
<!-- Logback is EPL-1.0 OR LGPL-2.1, outside the licence allow list: Log4j 2 (Apache-2.0) logs instead. -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
<!-- EPL-2.0 OR GPL-2.0 with Classpath Exception, outside the allow list; this app uses none of its annotations. -->
<exclusion>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- Spring Security's OAuth 2.0 client: OpenID Connect sign-in with discovery, PKCE and ID token checks. -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
Das Beispiel ersetzt zwei Abhängigkeiten, deren Lizenzen nicht auf der Lizenz-Allowlist unserer Beispiele stehen: Logback (EPL-1.0 oder LGPL-2.1) durch Log4j 2 und jakarta.annotation-api (EPL-2.0 oder GPL-2.0 mit Classpath Exception), die die App nicht verwendet. Aus demselben Grund verwenden die Tests TestNG statt JUnit (EPL-2.0). Ihre eigene App muss das nicht tun.
2. Konfigurieren
Die App liest ihre Konfiguration ausschließlich aus Umgebungsvariablen. Committen Sie deren Werte niemals.
| Variable | Erforderlich | Bedeutung |
|---|---|---|
KEENAUTH_ISSUER |
ja | Der Issuer Ihrer Umgebung, https |
KEENAUTH_CLIENT_ID |
ja | Die client_id der Webanwendung |
KEENAUTH_CLIENT_SECRET |
ja | Ihr Client Secret |
APP_BASE_URL |
ja | Die öffentliche URL der App. Der Redirect-URI wird daraus gebildet. http nur für localhost, 127.0.0.1 oder [::1] |
API_URL |
ja | Die API, die die App aufruft, zum Beispiel http://localhost:3001 |
KEENAUTH_API_AUDIENCE |
ja | Der Identifier dieser API in KeenAuth |
SESSION_SECURE_COOKIE |
nein | false nur für einfaches http://localhost |
KeenAuthSettings.from liest diese Werte beim Start ein und prüft sie. Fehlt ein Wert oder ist er unbrauchbar, stoppt die App, und keine Fehlermeldung enthält einen Wert.
public static KeenAuthSettings from(Function<String, String> env) {
return new KeenAuthSettings(url(need(env, "KEENAUTH_ISSUER"), "KEENAUTH_ISSUER", true, false),
need(env, "KEENAUTH_CLIENT_ID"), need(env, "KEENAUTH_CLIENT_SECRET"),
url(need(env, "APP_BASE_URL"), "APP_BASE_URL", false, false),
url(need(env, "API_URL"), "API_URL", false, false),
// RFC 8707: the access token is requested for your API, which then accepts it as its audience.
url(need(env, "KEENAUTH_API_AUDIENCE"), "KEENAUTH_API_AUDIENCE", false, true));
}
3. Client registrieren
Die Client-Registrierung stammt aus Discovery (<issuer>/.well-known/openid-configuration), das Spring Security beim Start abruft. Das Beispiel setzt die Client-Zugangsdaten, den Redirect-URI und die Scopes und aktiviert PKCE für diesen vertraulichen Client:
/** The endpoints and the signing keys come from discovery: <issuer>/.well-known/openid-configuration. */
@Bean
ClientRegistrationRepository clientRegistrationRepository(KeenAuthSettings settings) {
return new InMemoryClientRegistrationRepository(
registration(ClientRegistrations.fromIssuerLocation(settings.issuer()), settings));
}
static ClientRegistration registration(ClientRegistration.Builder discovered, KeenAuthSettings settings) {
return discovered.registrationId("keenauth")
.clientId(settings.clientId())
.clientSecret(settings.clientSecret())
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri(settings.redirectUri())
// messages:read is a scope of your API; the example API requires it on /api/messages.
.scope("openid", "profile", "email", "messages:read")
// PKCE (RFC 7636) with S256 for this confidential client too; state and nonce are always on.
.clientSettings(ClientRegistration.ClientSettings.builder().requireProofKey(true).build())
.build();
}
4. Anmeldung, API-Audience und Abmeldung konfigurieren
Der Resolver für die Autorisierungsanfrage fügt den Identifier Ihrer API als resource hinzu (RFC 8707). Ohne ihn ist das Access Token nicht für Ihre API bestimmt, und die API weist es zurück. Die Abmeldung ist das POST /logout von Spring Security mit dem CSRF-Token, gefolgt von OpenID Connect RP-Initiated Logout mit id_token_hint und post_logout_redirect_uri:
DefaultOAuth2AuthorizationRequestResolver resolver = new DefaultOAuth2AuthorizationRequestResolver(clients,
"/oauth2/authorization");
// RFC 8707: an access token for your API.
resolver.setAuthorizationRequestCustomizer(
(request) -> request.additionalParameters((params) -> params.put("resource", settings.apiAudience())));
OidcClientInitiatedLogoutSuccessHandler logout = new OidcClientInitiatedLogoutSuccessHandler(clients);
logout.setPostLogoutRedirectUri(settings.postLogoutRedirectUri());
http.authorizeHttpRequests((authorize) -> authorize.requestMatchers("/", "/error").permitAll()
.anyRequest()
.authenticated())
.oauth2Login((login) -> login.authorizationEndpoint((endpoint) -> endpoint.authorizationRequestResolver(resolver)))
// POST /logout with the CSRF token; then OpenID Connect RP-Initiated Logout with id_token_hint.
.logout((out) -> out.logoutSuccessHandler(logout));
return http.build();
Spring Security prüft state beim Callback, tauscht den Code mit dem PKCE-Verifier ein und prüft das ID Token (Signatur mit den Schlüsseln Ihrer Umgebung, RS256, iss, aud, exp, iat und nonce). Nach der Anmeldung vergibt es dem Benutzer eine neue Sitzungs-ID. Die Sitzung lebt auf dem Server; bei mehreren Servern verwenden Sie Spring Session mit einem gemeinsamen Speicher.
Die Startseite verlinkt zur Anmeldung und sendet ein Formular an die Abmeldung; th:action fügt das CSRF-Token hinzu:
<!-- th:action adds the CSRF token. -->
<form th:action="@{/logout}" method="post"><button type="submit">Sign out</button></form>
</div>
<p th:unless="${user}"><a href="/oauth2/authorization/keenauth">Sign in</a></p>
5. Ihre API aufrufen
Das Dashboard benötigt eine Sitzung. Es nimmt das Access Token aus dem autorisierten Client, den Spring Security in der Sitzung hält, und ruft damit Ihre API auf. Das Token bleibt auf dem Server; der Browser erhält nur das HTML.
/** Needs a session (SecurityConfig). The access token stays on the server; the browser receives only the HTML. */
@GetMapping("/dashboard")
public String dashboard(@AuthenticationPrincipal OidcUser user,
@RegisteredOAuth2AuthorizedClient("keenauth") OAuth2AuthorizedClient client, Model model) {
ResponseEntity<String> response = this.messages.fetch(client.getAccessToken().getTokenValue());
model.addAttribute("user", user);
model.addAttribute("status", response.getStatusCode().value());
model.addAttribute("messages", response.getStatusCode().is2xxSuccessful() ? response.getBody() : null);
return "dashboard";
}
public ResponseEntity<String> fetch(String accessToken) {
return this.rest.get()
.uri("/api/messages")
.headers((headers) -> headers.setBearerAuth(accessToken))
.retrieve()
.onStatus((status) -> true, (request, response) -> {
})
.toEntity(String.class);
}
Das Beispiel hat kein Refresh Token: Läuft das Access Token ab, meldet sich der Benutzer erneut an.
6. Die App ausführen
Starten Sie die API aus dem Express-Schnellstart (oder eine andere API, die die Tokens akzeptiert) und führen Sie die App mit exportierten Variablen aus:
mvn spring-boot:run
Öffnen Sie http://localhost:8080, wählen Sie Anmelden, melden Sie sich bei KeenAuth an und öffnen Sie das Dashboard.
7. Ohne Netzwerk testen
Die Tests benötigen weder Netzwerk noch ein KeenAuth-Konto. Ein kleiner HTTP-Server auf einem Loopback-Port übernimmt die Rolle Ihrer Umgebung (Discovery, ein für den Testlauf erzeugter Schlüsselsatz, der Token-Endpunkt und Userinfo) sowie die der API. Der Test ersetzt die Client-Registrierung durch eine, die von diesem Server aus Discovery bezieht, und zwar über dieselbe Methode registration:
/** The registration of SecurityConfig, discovered from the fake environment instead of the issuer above. */
@TestConfiguration
static class FakeDiscovery {
@Bean
ClientRegistrationRepository clientRegistrationRepository(KeenAuthSettings settings) {
return new InMemoryClientRegistrationRepository(
SecurityConfig.registration(ClientRegistrations.fromIssuerLocation(KEENAUTH.issuer()), settings));
}
}
@Test
public void callbackWithAnUnknownStateIsRefusedBeforeAnyTokenRequest() throws Exception {
MockHttpSession session = new MockHttpSession();
startSignIn(session);
MvcResult result = returnFromKeenAuth(session, "not-the-state");
assertEquals(result.getResponse().getRedirectedUrl(), "/login?error");
assertTrue(KEENAUTH.requestsTo("/oauth2/token").isEmpty());
mvn dependency:list -DoutputFile=target/dependencies.txt
python3 ../scripts/maven_deps.py target/dependencies.txt # SHA-256 lock and licences
mvn test
Die Tests prüfen, dass die Konfiguration beim Start validiert wird (und dass keine Fehlermeldung ein Geheimnis enthält); dass die Anmeldung PKCE S256, state, nonce und resource sendet; dass der Code mit dem passenden Verifier und mit Client-Authentifizierung eingetauscht wird; dass das Access Token im Header Authorization an die API geht und nie in ein Cookie gelangt; dass ein unbekannter State, ein ID Token für einen anderen Client und ein ID Token mit einem anderen Nonce ohne Sitzung abgelehnt werden; dass eine Seite ohne Sitzung die Anmeldung startet; und dass die Abmeldung das CSRF-Token benötigt und mit id_token_hint zum Logout-Endpunkt führt.
Die vollständige Anmeldung mit einem echten Browser benötigt eine KeenAuth-Umgebung. Sie ist nicht Teil der Offline-Tests.
Hinter einem Proxy oder Load Balancer
Setzen Sie APP_BASE_URL auf die öffentliche URL der App; der Redirect-URI wird daraus gebildet. Wenn ein Proxy TLS beendet, setzen Sie server.forward-headers-strategy=framework nur, wenn der Proxy der einzige Weg zur App ist.
Aktuelle Grenzen
- Die Anmeldung startet mit einem
GETauf/oauth2/authorization/keenauth, wie es Spring Security standardmäßig tut. Die Prüfung vonstateund PKCE schützen den Callback. - Spring Security prüft den Autorisierungs-Antwortparameter
issnicht (RFC 9207). - Spring Security akzeptiert ID Tokens mit RS256 standardmäßig. Signiert Ihre Umgebung mit einem anderen Algorithmus, konfigurieren Sie eine Bean
OidcIdTokenDecoderFactorymit diesem Algorithmus. - Kein Refresh Token und keine stille Anmeldung.
Nächste Schritte
- Eine Express-API schützen: die API, die diese App aufruft.
- Tokens prüfen: der Verifier unter der Middleware Ihrer API.