Spring Boot GraphQL JWE Kimlik Doğrulaması, GraphQL’in esnekliğini, stateless şifreli JWT’leri (JWE) ve JPA'yı birleştirerek güvenli ve ölçeklendirilebilir bir API sunar.
Yazan
Şuayb Şimşek
Spring Boot, güvenlik, mikroservis ve cloud-native mimari konularında pratik teknik notlar paylaşan backend odaklı fullstack geliştirici.
Bu bölümde, Spring Boot uygulamamızı H2/PostgreSQL, JPA, Liquibase changelog’ları, veri yüklemeleri, JWE anahtar özellikleri, GraphQL şema tanımları ve GraalVM native-image reflection tanımları ile yapılandırmak için gereken tüm uygulama ve veritabanı düzeyindeki yapılandırma dosyalarını tanımlıyoruz.
application.yml
Spring datasource, H2 konsolu, JPA/Hibernate, Liquibase changelog path’i, GraphQL subscriptions pathi ve tüm JWT/JWE anahtarları, issuer ve geçerlilik süresi (expiration) ayarlarını içerir.
liquibase/master.xml
Liquibase master changelog dosyası; H2 ve PostgreSQL için DBMS özel özellikler ve alt changelog tanımlarını barındırır.
liquibase/changelog/changelog-user.xmluser_identity, authority ve user_authority_mapping tabloları, indeksler, yabancı anahtarlar (FK) ve ilk <loadData> adımlarını tanımlayan temel şema değişiklik changelogu.
liquibase/data/user.csv
Kullanıcı kayıtları (UUID, kullanıcı adı, bcrypt ile şifrelenmiş parola, e-posta, enabled flagi, zaman damgaları ve oluşturan kullanıcı bilgisi).
liquibase/data/authority.csv
Yetki kayıtları (UUID, ad, açıklama, zaman damgaları ve oluşturan kullanıcı bilgisi).
liquibase/data/user_authority_mapping.csv
Kullanıcılar ile yetkiler arasındaki mappingler (composite birincil anahtar, zaman damgaları ve oluşturan kullanıcı bilgisi).
graphql/schema.graphqls
API için özel scalar tipler, query, subscription ve mutation tanımları ile DTO şema tanımlamalarını içeren GraphQL şema dosyası.
META-INF/native-image/liquibase/reflect-config.json
GraalVM native-image oluştururken Liquibase sınıfları için gerekli reflection kurallarını tanımlar.
🛠️ Adım 3: GraphQL & Güvenlik & Veritabanı Yapılandırması
Bu bölümde JWE tabanlı kimlik doğrulama, GraphQL entegrasyonu, GraalVM native-image çalışma zamanı ipuçları ve özel scalars için gerekli bean'leri ve ayarları tanımlıyoruz:
JwtProperties: JWT düzenleyicisi (issuer), geçerlilik süresi ve imzalama/şifreleme anahtar çiftlerini yapılandırır.
SecurityJwtConfig: RSA JWK nesnelerini oluşturur, JWT encoder/decoder’ı, authentication converter ve token resolver ve WebSocket interceptor bean’lerini yapılandırır.
SecurityConfig: DomainUserDetailsService ile entegre olur, kimlik doğrulama yöneticisi (authentication manager), password encoder ve JWE desteği ile stateless güvenlik filtre zincirini yapılandırır.
DatabaseConfig: JPA repository'lerini, auditing ve transaction yönetimini etkinleştirir.
GraphQLConfig: GraphQL için özel scalarları (Long, Date, Instant) tanımlar.
NativeConfig: GraalVM native-image derlemesi için çalışma zamanı ipuçlarını (reflection ve kaynak desenleri) kaydeder.
SecurityJwtConfig
SecurityConfig
JwtProperties
DatabaseConfig
GraphQLConfig
InstantScalar
NativeConfig
Main
🛠️ Adım 4: JPA Entegrasyonu
Bu bölümde, kullanıcıları, rollerleri ve bunların eşlemelerini temsil eden JPA entity’lerini ve kullanıcıları yetkileriyle birlikte yüklemek için Spring Data JPA deposunu tanımlıyoruz.
BaseEntity: Audit alanları (createdAt, createdBy, updatedAt, updatedBy) sağlayan soyut üst sınıf.
Authority: Rol verilerini saklayan authority tablosu entity’si.
User: Kullanıcı kimlik bilgileri ve profili saklayan user_identity tablosu entity’si.
UserAuthorityMapping: Kullanıcıları ve rolleri birbirine bağlayan user_authority_mapping ilişki tablosunun entity’si.
UserAuthorityMappingId: UserAuthorityMapping için composite anahtar sınıfı.
UserRepository: Entity graph kullanarak kullanıcı ve yetkilerini getiren Spring Data JPA repository.
BaseEntity
Authority
User
UserAuthorityMapping
UserAuthorityMappingId
UserRepository
🛠️ Adım 5: Güvenli JWE Token Yardımcı Sınıfları Oluşturun
Bu bölümde, Spring Boot uygulamanızda JSON Web Encryption (JWE) tokenları oluşturmak, şifrelemek ve çözmek için gerekli temel yardımcı sınıfları ve sabitleri tanımlıyoruz. Ayrıca auditing entegrasyonu ve JPA tabanlı UserDetailsService de ekliyoruz:
AuthoritiesConstants: ROLE_ ön ekiyle rol isimlerini merkezileştirir.
CookieBearerTokenResolver: Bearer token’ları yetkilendirme başlıklarından veya HTTP çerezlerinden çözer.
CookieUtils: Yeni veya süresi dolmuş token’ler için HTTP-only, secure ResponseCookie oluşturur ve HttpHeaders içinden accessToken değerini çıkartır.
JweUtil: Nimbus kütüphanesi ile RSA anahtarları kullanarak JWT’leri imzalar (JWS) ve şifreler (JWE).
KeyUtils: PEM formatındaki anahtar çiftinden RSA JWK’leri oluşturur.
SecurityUtils: SecurityContext oturum açan kullanıcının bilgisini sunar.
SpringSecurityAuditorAware: Auditing için oturum açan kullanıcıyı sağlayan AuditorAware implementasyonu.
DomainUserDetailsService: JPA tabanlı UserDetailsService, kullanıcı kimlik bilgilerini ve yetkilerini getirir.
GraphQlTokenCookieInterceptor: GraphQL yanıtlarını yakalar ve GraphQLContext içindeki accessToken veya clearAccessToken flaglerine göre erişim çerezini oluşturur veya siler.
CookieAuthenticationWebSocketInterceptor: Headerdan veya çerezlerden alınan JWE token’larla WebSocket bağlantılarını doğrular.
Bu yardımcılar, Spring Security ile durumsuz (stateless) JWE tabanlı bir kimlik doğrulama akışının temelini oluşturur.
AuthoritiesConstants
CookieBearerTokenResolver
CookieUtils
JweUtil
KeyUtils
SecurityUtils
DomainUserDetailsService
SpringSecurityAuditorAware
GraphQlTokenCookieInterceptor
CookieAuthenticationWebSocketInterceptor
🧪 Adım 6: Kimlik Doğrulama ve Güvenli Endpointler
Bu bölümde, aşağıdaki GraphQL controller ve DTO’ları tanımlıyoruz:
AuthController: Kullanıcıları doğrular, JWE token'ı GraphQL mutationu ile oluşturur ve GraphQLContext içine accessToken veya clearAccessToken flaglerini ekler.
HelloController: Kimliği doğrulanmış kullanıcılar ve yalnızca adminin erişebileceği GraphQL query, subscription ve mutationları sunar.
LoginInput: Login işlemi için (kullanıcı adı/şifre) GraphQL input tipi.
GreetInput & GreetDTO: greet mutationu için GraphQL input ve yanıt tipi.
TokenDTO: Token, tür ve geçerlilik süresini içeren JWE token yanıt modelidir.
Bu bileşenler, JWE token’ları ve JPA yı kullanarak GraphQL API’de stateless kimlik doğrulama akışını tamamlar.
AuthController
HelloController
LoginInput
GreetInput
GreetDTO
TokenDTO
▶️ Uygulamayı Çalıştır
BASH
./mvnw spring-boot:run
# or
gradle bootRun
GraalVM 22.3+ yüklüyse, native profili ile native-image derlemek için şu komutu çalıştır:
BASH
./mvnw native:compile -Pnative
Native-image derlemesi başarıyla tamamlandıktan sonra, çalıştırılabilir dosya target/ altında oluşturulur (örneğin: target/spring-boot-graphql-jwe-auth-demo). Doğrudan şu komut ile çalıştırın:
BASH
./target/spring-boot-graphql-jwe-auth-demo
Opsiyonel: UPX yüklüyse, yerel çalıştırılabilir dosyayı daha küçük boyut için sıkıştırabilirsiniz:
▶️ düğmesine tıklayın. Sadece ROLE_ADMIN yetkisine sahip token’lar veri alacak; diğerleri yetki hatası görecektir.
🏁 Sonuç
Artık Spring Boot GraphQL JWE Kimlik Doğrulama için üretim odaklı bir Spring Boot temeliniz var. Sonraki adımda ayarları kendi domainine uyarlayıp test ve gözlemlenebilirlik katmanını ekleyerek gerçek trafik altında doğrulayın.
JAVASecurityJwtConfig.java
package io.github.susimsek.springbootgraphqljwedemo.config;
import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.JWEDecryptionKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import io.github.susimsek.springbootgraphqljwedemo.security.CookieAuthenticationWebSocketInterceptor;
import io.github.susimsek.springbootgraphqljwedemo.security.CookieBearerTokenResolver;
import io.github.susimsek.springbootgraphqljwedemo.security.KeyUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.server.WebSocketGraphQlInterceptor;
import org.springframework.graphql.server.support.BearerTokenAuthenticationExtractor;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import java.util.List;
import static io.github.susimsek.springbootgraphqljwedemo.security.SecurityUtils.AUTHORITIES_KEY;
@Configuration
public class SecurityJwtConfig {
private final JwtProperties props;
public SecurityJwtConfig(JwtProperties props) {
this.props = props;
}
@Bean
public RSAKey signingKey() throws Exception {
return KeyUtils.buildRsaKey(
props.getSigning().getPublicKey(),
props.getSigning().getPrivateKey(),
props.getSigning().getKeyId(),
true
);
}
@Bean
public RSAKey encryptionKey() throws Exception {
return KeyUtils.buildRsaKey(
props.getEncryption().getPublicKey(),
props.getEncryption().getPrivateKey(),
props.getEncryption().getKeyId(),
false
);
}
@Bean
public JWKSource<SecurityContext> jwkSource(RSAKey signingKey, RSAKey encryptionKey) {
JWKSet jwkSet = new JWKSet(List.of(signingKey, encryptionKey));
return (jwkSelector, context) -> jwkSelector.select(jwkSet);
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWEKeySelector(new JWEDecryptionKeySelector<>(
JWEAlgorithm.RSA_OAEP_256,
EncryptionMethod.A128GCM,
jwkSource
));
jwtProcessor.setJWSKeySelector(new JWSVerificationKeySelector<>(
JWSAlgorithm.RS256,
jwkSource
));
jwtProcessor.setJWTClaimsSetVerifier((claims, ctx) -> {});
return new NimbusJwtDecoder(jwtProcessor);
}
@Bean
public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
return new NimbusJwtEncoder(jwkSource);
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
converter.setAuthorityPrefix("");
converter.setAuthoritiesClaimName(AUTHORITIES_KEY);
JwtAuthenticationConverter authConverter = new JwtAuthenticationConverter();
authConverter.setJwtGrantedAuthoritiesConverter(converter);
return authConverter;
}
@Bean
public BearerTokenResolver bearerTokenResolver() {
CookieBearerTokenResolver resolver = new CookieBearerTokenResolver();
resolver.setAllowUriQueryParameter(false);
resolver.setAllowFormEncodedBodyParameter(false);
resolver.setAllowCookie(true);
return resolver;
}
@Bean
public WebSocketGraphQlInterceptor authenticationInterceptor(JwtDecoder jwtDecoder) {
return new CookieAuthenticationWebSocketInterceptor(
new BearerTokenAuthenticationExtractor(),
new ProviderManager(new JwtAuthenticationProvider(jwtDecoder))
);
}
}
package io.github.susimsek.springbootgraphqljwedemo.security;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AuthoritiesConstants {
public static final String ADMIN = "ROLE_ADMIN";
public static final String USER = "ROLE_USER";
public static final String ANONYMOUS = "ROLE_ANONYMOUS";
}
KOTLINAuthoritiesConstants.kt
package io.github.susimsek.springbootgraphqljwedemo.security
object AuthoritiesConstants {
const val ADMIN = "ROLE_ADMIN"
const val USER = "ROLE_USER"
const val ANONYMOUS = "ROLE_ANONYMOUS"
}
package io.github.susimsek.springbootgraphqljwedemo.controller
import io.github.susimsek.springbootgraphqljwedemo.dto.GreetDTO
import io.github.susimsek.springbootgraphqljwedemo.dto.GreetInput
import org.springframework.graphql.data.method.annotation.Argument
import org.springframework.graphql.data.method.annotation.MutationMapping
import org.springframework.graphql.data.method.annotation.QueryMapping
import org.springframework.graphql.data.method.annotation.SubscriptionMapping
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.stereotype.Controller
import reactor.core.publisher.Flux
import java.time.Duration
import java.time.Instant
@Controller
@RequiredArgsConstructor
@PreAuthorize("isAuthenticated()")
class HelloController {
@QueryMapping
fun helloAll(@AuthenticationPrincipal jwt: Jwt): String {
val roles = jwt.getClaimAsStringList(SecurityUtils.AUTHORITIES_KEY)
return "Hello, ${jwt.subject}! Your roles: $roles"
}
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@QueryMapping
fun helloAdmin(@AuthenticationPrincipal jwt: Jwt): String =
"Hello Admin, ${jwt.subject}!"
@MutationMapping
fun greet(
@Argument input: GreetInput,
@AuthenticationPrincipal jwt: Jwt
): GreetDTO {
val msg = "Hello ${jwt.subject}, you said: ${input.message}"
return GreetDTO(msg, Instant.now())
}
@SubscriptionMapping("greetStream")
fun greetStream(
@Argument input: GreetInput,
@AuthenticationPrincipal jwt: Jwt
): Flux<GreetDTO> =
Flux.interval(Duration.ofSeconds(1))
.map { i ->
val msg = "Hello ${jwt.subject}, you said: ${input.message} (event ${i + 1})"
GreetDTO(msg, Instant.now())
}
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@SubscriptionMapping("greetStreamAdmin")
fun greetStreamAdmin(
@Argument input: GreetInput,
@AuthenticationPrincipal jwt: Jwt
): Flux<GreetDTO> =
Flux.interval(Duration.ofSeconds(2))
.map { i ->
val msg = "Hello Admin ${jwt.subject}, you said: ${input.message} (admin event ${i + 1})"
GreetDTO(msg, Instant.now())
}
}
JAVALoginInput.java
package io.github.susimsek.springbootgraphqljwedemo.dto;
public record LoginInput(
String username,
String password
) {}
KOTLINLoginInput.kt
package io.github.susimsek.springbootgraphqljwedemo.dto
data class LoginInput(
val username: String,
val password: String
)
JAVAGreetInput.java
package io.github.susimsek.springbootgraphqljwedemo.dto;
public record GreetInput(
String message
) {}
KOTLINGreetInput.kt
package io.github.susimsek.springbootgraphqljwedemo.dto
data class GreetInput(
val message: String
)
JAVAGreetDTO.java
package io.github.susimsek.springbootgraphqljwedemo.dto;
import java.time.Instant;
public record GreetDTO(
String greeting,
Instant timestamp
) {}
KOTLINGreetDTO.kt
package io.github.susimsek.springbootgraphqljwedemo.dto
import java.time.Instant
data class GreetDTO(
val greeting: String,
val timestamp: Instant
)
JAVATokenDTO.java
package io.github.susimsek.springbootgraphqljwedemo.dto;
public record TokenDTO(
String accessToken,
String tokenType,
long accessTokenExpiresIn
) {}
KOTLINTokenDTO.kt
package io.github.susimsek.springbootgraphqljwedemo.dto
data class TokenDTO(
val accessToken: String,
val tokenType: String,
val accessTokenExpiresIn: Long
)