Web uygulamalarında güvenlik, özellikle kimlik doğrulama ve yetkilendirme süreçleri son derece kritik bir rol oynamaktadır. Bu yazıda, Spring Boot uygulamanızda JWT (JSON Web Token) kullanarak kullanıcı kimlik doğrulaması nasıl yapılır, detaylı bir şekilde anlatacağım.
1. Gerekli Bağımlılıkların Eklenmesi #
Spring Boot ile JWT tabanlı kimlik doğrulama ve yetkilendirme işlemleri gerçekleştirebilmek için bazı temel bağımlılıkları projenize dahil etmeniz gerekmektedir. Bu bağımlılıklar, güvenlik ve JWT işlemleri için gereklidir. Aşağıda, Spring Boot projenize bu bağımlılıkların nasıl ekleneceği açıklanmıştır.
Maven Kullanıyorsanız:
pom.xml
dosyanıza aşağıdaki bağımlılıkları ekleyerek gerekli kütüphaneleri projenize dahil edebilirsiniz.
<dependencies>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JJWT (Java JWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
Gradle Kullanıyorsanız:
Eğer Gradle kullanıyorsanız, aşağıdaki bağımlılıkları build.gradle
dosyanıza ekleyebilirsiniz.
dependencies {
// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// JJWT (Java JWT) API, Implementation ve Jackson desteği
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
2. Kullanıcı Bilgilerinin Güvenli Bir Şekilde Saklanması: AuthDto
#
İlk adım olarak, kullanıcı bilgilerini saklayacağımız bir DTO (Data Transfer Object) sınıfı oluşturmalıyız. Bu sınıf, JWT token içinde yer alacak bilgileri temsil edecektir. Örneğin, kullanıcı id’si, email adresi ve rollerini içerecek bir sınıf:
@Builder
@Getter
@Setter
public class AuthDto {
private UUID id;
private String email;
private List<GrantedAuthority> roles;
}
Bu DTO, kullanıcı bilgilerini taşıyacak ve güvenli bir şekilde SecurityContext içinde kullanılacaktır.
3. JWT Oluşturma ve Doğrulama: TokenService
#
JWT’yi oluşturup doğrulayacak olan servis sınıfını yazmamız gerekiyor. Bu sınıf, token’ın üretilmesinden, doğrulama ve güvenlik bağlamını (authentication) oluşturma sürecine kadar olan tüm işlemleri yönetecek.
@RequiredArgsConstructor
@Service
public class TokenService implements ITokenService {
private final IUserRepository userRepository;
private final int MILLISECOND_IN_MINUTE = 1000 * 60; // Zaman hesaplamalarında kullanmak için milisaniye cinsinden dakika çevirisi
private final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256; // Kullanılacak imzalama algoritması
@Value("${security.token.access.secret}")
private String accessTokenSecret; // Access token için gizli anahtar
@Value("${security.token.access.expires}")
private int accessTokenExpires; // Access token'in geçerlilik süresi (dakika cinsinden)
// Kullanıcı objesinden JWT'nin oluşturulması
@Override
public String generateAccessToken(User user) {
Date now = new Date(); // Şu anki zaman
int expiresIn = accessTokenExpires * MILLISECOND_IN_MINUTE; // Token'ın geçerlilik süresi
// JWT token'ı oluşturuluyor
return Jwts.builder()
.setSubject(user.getEmail()) // Kullanıcının email adresi
.claim("id", user.getId()) // Kullanıcının id'si
.claim("roles", rolesToString(user.getRoles())) // Kullanıcının rollerini string olarak ekliyoruz
.setIssuedAt(now) // Token'ın oluşturulma zamanı
.setExpiration(new Date(now.getTime() + expiresIn)) // Token'ın son kullanma tarihi
.signWith(jwtKeyGenerator(accessTokenSecret), SIGNATURE_ALGORITHM) // JWT imzalaması
.compact(); // Token'ı oluştur
}
// JWT token'ından kullanıcı kimlik doğrulaması (Authentication) oluşturulması
@Override
public Authentication getAuthenticationFromAccessToken(String accessToken) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(jwtKeyGenerator(accessTokenSecret)) // JWT'nin imzasını doğrulamak için gizli anahtar
.build().parseClaimsJws(accessToken).getBody(); // JWT token'ını çöz
// Token'dan roller bilgisini alıyoruz ve GrantedAuthority objelerine dönüştürüyoruz
List<GrantedAuthority> roles = stringToRoles(claims.get("roles", String.class));
// Authentication objesini oluşturuyoruz
AuthDto authDto = AuthDto.builder()
.id(UUID.fromString(claims.get("id", String.class))) // Kullanıcının UUID'sini al
.email(claims.getSubject()) // Kullanıcının e-posta adresini al
.roles(roles) // Kullanıcının rollerini ekle
.build();
// Kullanıcı bilgisiyle birlikte authentication objesini döndür
return new UsernamePasswordAuthenticationToken(authDto, null, roles);
}
// Kullanıcının rollerini string formatına dönüştüren yardımcı metot
private String rolesToString(Collection<Role> roles) {
return roles.stream()
.map(Role::name) // Rollerin isimlerini al
.collect(Collectors.joining(",")); // Virgülle ayırarak tek bir string'e çevir
}
// String formatındaki rollerin GrantedAuthority listesine dönüştürülmesi
private List<GrantedAuthority> stringToRoles(String rolesString) {
if (rolesString == null || rolesString.isEmpty()) {
return Collections.emptyList(); // Eğer roller boşsa, boş bir liste döndür
}
// String içindeki rollerden her birini GrantedAuthority'ye dönüştür
return Arrays.stream(rolesString.split(",\\s*"))
.map(role -> Role.valueOf(role)) // Her rolü Role enum'ına dönüştür
.filter(Objects::nonNull) // Null olmayanları filtrele
.map(role -> new SimpleGrantedAuthority(role.name())) // GrantedAuthority nesnelerine dönüştür
.collect(Collectors.toList());
}
// JWT için gizli anahtarın oluşturulması
private Key jwtKeyGenerator(String secret) {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret)); // BASE64 ile decode edilmiş anahtarı kullanıyoruz
}
}
4. JWT Kontrolü İçin AuthTokenFilter
#
Her gelen istekte, JWT’nin doğruluğunu kontrol etmek için bir filtre kullanıyoruz. Bu filtre, JWT’nin geçerli olup olmadığını kontrol eder ve geçerli ise kullanıcıyı doğrular.
@RequiredArgsConstructor
@Component
public class AuthTokenFilter extends OncePerRequestFilter {
private final ITokenService tokenService; // TokenService, JWT doğrulama işlemleri için kullanılır
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String url = request.getRequestURI(); // Gelen isteğin URL'sini al
// Eğer URL '/api/auth/*' ile başlıyorsa, JWT kontrolü yapma
if (url.matches("/api/auth/*")) {
filterChain.doFilter(request, response);
return;
}
// Cookie içinde access token'ı alıyoruz
Cookie accessTokenCookie = WebUtils.getCookie(request, "accessToken");
// Eğer token yoksa, hata mesajı ekleyip, filtreyi geçiyoruz
if (accessTokenCookie == null) {
attachError(request, ErrorCode.ACCESS_TOKEN_DOES_NOT_EXIST); // Hata mesajı ekle
filterChain.doFilter(request, response); // Filtreyi geç
return;
}
try {
// Token'ı doğrulayıp Authentication objesini alıyoruz
Authentication authentication = tokenService.getAuthenticationFromAccessToken(accessTokenCookie.getValue());
// SecurityContext'e doğrulama bilgilerini yerleştiriyoruz
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (ExpiredJwtException e) {
// Token süresi dolmuşsa, hata mesajı ekleyip, SecurityContext'i temizliyoruz
attachErrorAndClearAuthContext(request, ErrorCode.ACCESS_TOKEN_EXPIRED);
} catch (MalformedJwtException e) {
// Token bozulmuşsa, hata mesajı ekleyip, SecurityContext'i temizliyoruz
attachErrorAndClearAuthContext(request, ErrorCode.ACCESS_TOKEN_MALFORMED);
} catch (RuntimeException e) {
// Diğer hata durumlarında, hata mesajı ekleyip, SecurityContext'i temizliyoruz
attachErrorAndClearAuthContext(request, ErrorCode.INTERNAL_SERVER);
}
filterChain.doFilter(request, response); // Filtreyi geç
}
private void attachError(HttpServletRequest request, ErrorCode errorCode) {
// Hata mesajlarını request'e ekliyoruz
request.setAttribute("exceptionStatus", errorCode.getHttpStatusCode());
request.setAttribute("exceptionCode", errorCode.getCode());
request.setAttribute("exceptionMessage", errorCode.getMessage());
}
private void attachErrorAndClearAuthContext(HttpServletRequest request, ErrorCode errorCode) {
// Hata mesajlarını ekleyip, SecurityContext'i temizliyoruz
attachError(request, errorCode);
SecurityContextHolder.clearContext(); // Authentication bilgilerini temizle
}
}
5. JWT Hatalarını Yönetmek İçin AuthExceptionHandler
#
JWT doğrulama sırasında oluşabilecek hataları yönetmek için kullanılan AuthenticationEntryPoint
, kullanıcılara doğru hata mesajları göndermek için yapılandırılır.
@Slf4j
@Component
public class AuthExceptionHandler implements AuthenticationEntryPoint {
final ObjectMapper MAPPER = new ObjectMapper(); // JSON yanıtları için ObjectMapper
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.error("Unauthorized error: {}", authException.getMessage()); // Hata mesajını logluyoruz
response.setContentType(MediaType.APPLICATION_JSON_VALUE); // Yanıt türünü JSON olarak belirliyoruz
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // HTTP 401 (Unauthorized) durum kodu
final Map<String, Object> body = new HashMap<>(); // Yanıt için hata bilgilerini içeren bir map
// Eğer hata bilgileri request içinde varsa, bunları alıp JSON yanıt olarak gönderiyoruz
if (request.getAttribute("exceptionCode") != null &&
request.getAttribute("exceptionMessage") != null &&
request.getAttribute("exceptionStatus") != null) {
body.put("code", request.getAttribute("exceptionCode").toString());
body.put("message", request.getAttribute("exceptionMessage").toString());
body.put("status", Integer.parseInt(request.getAttribute("exceptionStatus").toString()));
} else {
// Eğer hata bilgileri yoksa, genel bir yetkilendirme hatası mesajı gönderiyoruz
body.put("code", ErrorCode.AUTHENTICATION_REQUIRED.getCode());
body.put("message", ErrorCode.AUTHENTICATION_REQUIRED.getMessage());
body.put("status", ErrorCode.AUTHENTICATION_REQUIRED.getHttpStatusCode());
}
// JSON yanıtı gönder
MAPPER.writeValue(response.getOutputStream(), body);
}
}
6. Spring Security Yapılandırması: SecurityConfig
#
Son olarak, JWT doğrulama filtremizi ve hata yönetimi sınıfımızı Spring Security yapılandırmasına dahil ediyoruz. Bu yapılandırma, tüm uygulama genelinde güvenliği sağlayacak şekilde filtre ve doğrulama işlemleri yapar.
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final AuthTokenFilter authTokenFilter; // AuthTokenFilter, her gelen istekte JWT doğrulamasını yapacak
private final AuthExceptionHandler authExceptionHandler; // AuthExceptionHandler, hata mesajlarını yönetir
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.exceptionHandling(customizer -> customizer.authenticationEntryPoint(authExceptionHandler)) // Hata yönetimi sınıfını kullan
.addFilterBefore(authTokenFilter, BasicAuthenticationFilter.class) // JWT doğrulama filtresini yerleştir
.csrf(AbstractHttpConfigurer::disable) // CSRF korumasını devre dışı bırak
.sessionManagement(customizer -> customizer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Stateless oturum yönetimi
.authorizeHttpRequests(requests -> requests
.requestMatchers("/api/auth/*").permitAll() // Kimlik doğrulaması gerektirmeyen yollar
.anyRequest().authenticated()) // Diğer tüm isteklerde kimlik doğrulaması gerektir
.build(); // Yapılandırmayı uygula
}
}
Sonuç #
Bu yazıda, Spring Boot uygulamalarında JWT tabanlı kimlik doğrulama ve yetkilendirme işlemleri nasıl yapılır, bunu inceledik. JWT’nin oluşturulması, doğrulanması, güvenlik filtreleri ve hata yönetimi gibi temel adımları olabildiğince yalın ve detaylı anlatmaya çalıştım. Sorularınız için benimle iletişime geçebilirsiniz.