Spring Boot uygulamasına giriş ekleme

Bu hızlı başlangıç, Spring ekosisteminin standart OpenID Connect istemcisi olan Spring Security'nin OAuth 2.0 istemcisiyle bir Spring Boot 4.1 uygulamasına KeenAuth girişi ekler. KeenAuth'un henüz bir Java SDK'sı yoktur ve buna ihtiyaç da yoktur: KeenAuth standart bir OpenID Connect sağlayıcısıdır. Uygulamanız OAuth istemcisidir ve tüm belirteçleri sunucu tarafındaki oturumda tutar; tarayıcıya yalnızca bir oturum kimliği çerezi gider. Girişi yapılandırır, bir sayfayı korur, kullanıcının erişim belirteciyle korumalı bir API'yi çağırır, çıkış yapar ve uygulamayı çevrimdışı test edersiniz.

Uygulamanın rotaları şunlardır:

Rota Ne yapar
GET / Herkese açık ana sayfa: bir giriş bağlantısı ya da kullanıcının adı ve bir çıkış düğmesi
GET /dashboard Oturum gerektirir. Kullanıcıyı ve API'nizin döndürdüğü iletileri gösterir
GET /oauth2/authorization/keenauth Spring Security tarafından sunulur: girişi başlatır
GET /login/oauth2/code/keenauth Spring Security tarafından sunulur: geri çağırma adresi
POST /logout Spring Security tarafından sunulur: oturumu burada ve KeenAuth'ta sonlandırır. CSRF belirtecini gerektirir

Başlamadan önce

1. Bağımlılıkları ekleyin

Spring Boot üst projesi (spring-boot-starter-parent 4.1.2) tüm sürümleri sabitler. Web, OAuth 2.0 istemcisi ve Thymeleaf starter'larını ekleyin:

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

Bu örnek, lisansları örneklerimizin lisans izin listesinin dışında kalan iki bağımlılığı değiştirir: Logback (EPL-1.0 veya LGPL-2.1) yerine Log4j 2 ve uygulamanın kullanmadığı jakarta.annotation-api (EPL-2.0 veya Classpath Exception ile GPL-2.0). Aynı nedenle testleri JUnit (EPL-2.0) yerine TestNG ile yazar. Kendi uygulamanızın bunları yapmasına gerek yoktur.

2. Yapılandırın

Uygulama yapılandırmasını yalnızca ortam değişkenlerinden okur. Değerleri asla kaydetmeyin.

Değişken Zorunlu Anlamı
KEENAUTH_ISSUER evet Ortamınızın veren kuruluşu (issuer), https
KEENAUTH_CLIENT_ID evet Web uygulamasının client_id değeri
KEENAUTH_CLIENT_SECRET evet Web uygulamasının istemci gizli anahtarı
APP_BASE_URL evet Uygulamanın herkese açık adresi. Yönlendirme adresi bundan oluşturulur. http yalnızca localhost, 127.0.0.1 veya [::1] için kullanılabilir
API_URL evet Uygulamanın çağırdığı API, örneğin http://localhost:3001
KEENAUTH_API_AUDIENCE evet Bu API'nin KeenAuth'taki tanımlayıcısı
SESSION_SECURE_COOKIE hayır Yalnızca düz http://localhost için false

KeenAuthSettings.from, bu değerleri başlangıçta okur ve denetler. Eksik ya da kullanılamayan bir değer uygulamayı durdurur ve hiçbir hata iletisi bir değer içermez.

	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. İstemciyi kaydedin

İstemci kaydı, Spring Security'nin başlangıçta aldığı keşiften (<issuer>/.well-known/openid-configuration) gelir. Örnek, istemci kimlik bilgilerini, yönlendirme adresini ve kapsamları ayarlar ve bu gizli istemci için PKCE'yi açar:

	/** 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. Giriş, API hedef kitlesi ve çıkışı yapılandırın

Yetkilendirme isteği çözümleyicisi, API'nizin tanımlayıcısını resource olarak ekler (RFC 8707). Bu olmadan erişim belirteci API'niz için düzenlenmez ve API onu reddeder. Çıkış, CSRF belirteciyle birlikte Spring Security'nin POST /logout isteğidir; ardından id_token_hint ve post_logout_redirect_uri ile OpenID Connect RP-Initiated Logout gelir:

		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, geri çağırmada state değerini denetler, kodu PKCE doğrulayıcısıyla takas eder ve kimlik belirtecini denetler (ortamınızın anahtarlarıyla imza, RS256, iss, aud, exp, iat ve nonce). Girişten sonra kullanıcıya yeni bir oturum kimliği verir. Oturum sunucuda tutulur; birden fazla sunucu kullanıyorsanız ortak bir depoyla Spring Session kullanın.

Ana sayfa giriş bağlantısını gösterir ve çıkış için bir form gönderir; th:action CSRF belirtecini ekler:

      <!-- 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. API'nizi çağırın

Pano sayfası bir oturum gerektirir. Erişim belirtecini, Spring Security'nin oturumda tuttuğu yetkili istemciden alır ve API'nizi onunla çağırır. Belirteç sunucuda kalır; tarayıcı yalnızca HTML alır.

	/** 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);
	}

Örnekte yenileme belirteci yoktur: erişim belirteci sona erdiğinde kullanıcı yeniden giriş yapar.

6. Uygulamayı çalıştırın

Express hızlı başlangıcındaki API'yi (ya da bu belirteçleri kabul eden herhangi bir API'yi) başlatın ve uygulamayı değişkenler dışa aktarılmış olarak çalıştırın:

mvn spring-boot:run

http://localhost:8080 adresini açın, Giriş yap seçeneğini seçin, KeenAuth'ta giriş yapın ve pano sayfasını açın.

7. Ağ olmadan test edin

Testler ağ ya da KeenAuth hesabı gerektirmez. Döngü geri bağlantı noktasında çalışan küçük bir HTTP sunucusu, ortamınızı (keşif, test çalıştırması için oluşturulmuş bir anahtar kümesi, belirteç uç noktası ve kullanıcı bilgisi) ve API'yi canlandırır. Test, aynı registration metoduyla istemci kaydını, bu sunucudan keşfedilen bir kayıtla değiştirir:

	/** 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

Testler şunları denetler: yapılandırmanın başlangıçta doğrulanması (ve hiçbir hata iletisinin gizli anahtar içermemesi); girişin PKCE S256, state, nonce ve resource gönderdiği; kodun eşleşen doğrulayıcı ve istemci kimlik doğrulamasıyla takas edildiği; erişim belirtecinin Authorization başlığında API'ye gittiği ve asla bir çereze yazılmadığı; bilinmeyen bir state, başka bir istemci için kimlik belirteci ve farklı bir nonce içeren kimlik belirteci için oturum açılmadan reddedildiği; oturumu olmayan bir sayfanın girişi başlattığı; çıkışın CSRF belirtecini gerektirdiği ve id_token_hint ile çıkış uç noktasına gittiği.

Gerçek bir tarayıcıyla tam giriş bir KeenAuth ortamı gerektirir. Bu, çevrimdışı testlerin parçası değildir.

Bir proxy veya yük dengeleyici arkasında

APP_BASE_URL değerini uygulamanın herkese açık adresine ayarlayın; yönlendirme adresi bundan oluşturulur. Bir proxy TLS'i sonlandırıyorsa, server.forward-headers-strategy=framework ayarını yalnızca uygulamaya erişmenin tek yolu proxy ise kullanın.

Bugünkü sınırlar

Sonraki adımlar