User Microservice - Baştan Kodlama
Aşağıda, tüm ayrıntılarıyla User microservice'i yeniden baştan kodlayacağız.
1. Proje Yapılandırması
1.1. pom.xml
xml<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>user-service</artifactId>
<version>1.0.0</version>
<properties>
<java.version>21</java.version>
<spring.boot.version>3.1.0</spring.boot.version>
<mapstruct.version>1.5.2.Final</mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>21</source>
<target>21</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
2. application.yml
server:
port: 8081
spring:
datasource:
url: jdbc:postgresql://localhost:5432/usersdb
username: postgres
password: password
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8080/oauth2/default
jwt:
secret: myJwtSecret
expiration: 86400000 # 1 day in milliseconds
3. Model, Repository, Service ve Controller Katmanları
3.1. User
Entity
package com.example.userservice.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Entity
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String username;
@NotBlank
private String password;
@Email
@NotBlank
private String email;
@Enumerated(EnumType.STRING)
private Role role;
public enum Role {
USER, ADMIN
}
}
3.2. UserRepository
package com.example.userservice.repository;
import com.example.userservice.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
3.3. DTO'lar ve Mapper
3.3.1. UserRequest
package com.example.userservice.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class UserRequest {
@NotBlank
private String username;
@NotBlank
private String password;
@Email
@NotBlank
private String email;
@NotBlank
private String role;
}
3.3.2. UserResponse
package com.example.userservice.dto;
import lombok.Data;
@Data
public class UserResponse {
private Long id;
private String username;
private String email;
private String role;
}
3.3.3. UserMapper
package com.example.userservice.mapper;
import com.example.userservice.dto.UserRequest;
import com.example.userservice.dto.UserResponse;
import com.example.userservice.model.User;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target = "role", expression =
"java(User.Role.valueOf(userRequest.getRole().toUpperCase()))")
User toEntity(UserRequest userRequest);
@Mapping(target = "role", source = "role")
UserResponse toResponse(User user);
}
3.4. UserService
package com.example.userservice.service;
import com.example.userservice.model.User;
import com.example.userservice.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public User registerUser(User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
return userRepository.save(user);
}
public User findByUsername(String username) {
return userRepository.findByUsername(username).orElse(null);
}
}
3.5. UserController
package com.example.userservice.controller;
import com.example.userservice.dto.UserRequest;
import com.example.userservice.dto.UserResponse;
import com.example.userservice.mapper.UserMapper;
import com.example.userservice.model.User;
import com.example.userservice.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
private final UserMapper userMapper;
@PostMapping("/register")
public ResponseEntity<UserResponse> registerUser(@Validated
@RequestBody UserRequest userRequest) {
User user = userService.registerUser(userMapper.toEntity(userRequest));
return ResponseEntity.ok(userMapper.toResponse(user));
}
@GetMapping("/{username}")
public ResponseEntity<UserResponse> getUser(@PathVariable String username) {
User user = userService.findByUsername(username);
if (user == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(userMapper.toResponse(user));
}
}
4. Güvenlik Yapılandırması
4.1. SecurityConfig
package com.example.userservice.config;
import com.example.userservice.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.
configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.
EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserService userService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.csrf().disable()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/users/register").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
return username -> userService.findByUsername(username);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager
(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
Sonuç
Bu yapılandırma ile User microservice'i baştan sona tamamladık. MapStruct
kullanarak DTO'lar arasında dönüşümleri kolayca gerçekleştirdik ve güvenlik yapılandırmasıyla kullanıcı doğrulama işlemlerini sağladık. Bu yapılandırmayı diğer microservisler için de benzer şekilde uygulayabilirsiniz.
1. CustomUserDetails
1.1. CustomUserDetails
package com.example.userservice.security;
import com.example.userservice.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Stream.of(new SimpleGrantedAuthority("ROLE_" +
user.getRole().name()))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
2. JwtTokenProvider
2.1. JwtTokenProvider
package com.example.userservice.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private Long jwtExpiration;
public String generateToken(Authentication authentication) {
CustomUserDetails userPrincipal = (CustomUserDetails)
authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpiration);
return Jwts.builder()
.setSubject(userPrincipal.getUsername())
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUserUsernameFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (Exception e) {
return false;
}
}
}
3. JwtAuthenticationFilter
3.1. JwtAuthenticationFilter
package com.example.userservice.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.
WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String jwt = getJwtFromRequest(request);
if (jwt != null && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUserUsernameFromJWT(jwt);
CustomUserDetails userDetails =
(CustomUserDetails) userDetailsService.loadUserByUsername(username);
JwtAuthenticationToken authentication =
new JwtAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource()
.buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
4. JwtAuthenticationToken
4.1. JwtAuthenticationToken
package com.example.userservice.security;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
public JwtAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
}
Tam Proje Kodu
Projenin tamamı aşağıda verilmiştir. Her bir bileşen yukarıda detaylandırılmıştır ve aşağıda proje dizin yapısında belirtilmiştir.
user-service
│
├── pom.xml
│
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── userservice
│ │ │ ├── UserServiceApplication.java
│ │ │ ├── controller
│ │ │ │ └── UserController.java
│ │ │ ├── dto
│ │ │ │ ├── UserRequest.java
│ │ │ │ └── UserResponse.java
│ │ │ ├── mapper
│ │ │ │ └── UserMapper.java
│ │ │ ├── model
│ │ │ │ └── User.java
│ │ │ ├── repository
│ │ │ │ └── UserRepository.java
│ │ │ ├── security
│ │ │ │ ├── CustomUserDetails.java
│ │ │ │ ├── JwtAuthenticationFilter.java
│ │ │ │ ├── JwtAuthenticationToken.java
│ │ │ │ └── JwtTokenProvider.java
│ │ │ ├── config
│ │ │ │ └── SecurityConfig.java
│ │ │ └── service
│ │ │ └── UserService.java
│ │ └── resources
│ │ ├── application.yml
│ │ └── data.sql
└── src
├── test
└── java
└── com
└── example
└── userservice
└── UserServiceApplicationTests.java
UserServiceApplication.java
package com.example.userservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
Bu yapılandırma ile User microservice'ini tamamlamış olduk. Her bileşen bağımsız olarak çalışır ve JWT ile güvenli bir kimlik doğrulama sağlar. MapStruct kullanarak DTO'lar arasındaki dönüşümleri de sağladık. Bu yapılandırmayı diğer microservisler için de benzer şekilde uygulayabilirsiniz.
Unit ve Entegrasyon Testleri
JUnit 5 ve Mockito kullanarak User microservice'i için birim ve entegrasyon testleri yazalım.
1. Birim Testleri
1.1. UserServiceTest
package com.example.userservice.service;
import com.example.userservice.model.User;
import com.example.userservice.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks
private UserService userService;
@Test
void testRegisterUser() {
User user = new User();
user.setUsername("testuser");
user.setPassword("password");
when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword");
when(userRepository.save(any(User.class))).thenReturn(user);
User savedUser = userService.registerUser(user);
assertNotNull(savedUser);
assertEquals("testuser", savedUser.getUsername());
verify(passwordEncoder, times(1)).encode("password");
verify(userRepository, times(1)).save(user);
}
@Test
void testFindByUsername() {
User user = new User();
user.setUsername("testuser");
when(userRepository.findByUsername("testuser")).
thenReturn(Optional.of(user));
User foundUser = userService.findByUsername("testuser");
assertNotNull(foundUser);
assertEquals("testuser", foundUser.getUsername());
verify(userRepository, times(1)).findByUsername("testuser");
}
}
2. Entegrasyon Testleri
2.1. UserControllerIntegrationTest
package com.example.userservice.controller;
import com.example.userservice.dto.UserRequest;
import com.example.userservice.dto.UserResponse;
import com.example.userservice.model.User;
import com.example.userservice.repository.UserRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.
AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Optional;
import static org.springframework.test.web.servlet.request.
MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.
MockMvcResultMatchers.*;
@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserRepository userRepository;
@Autowired
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
void testRegisterUser() throws Exception {
UserRequest userRequest = new UserRequest();
userRequest.setUsername("testuser");
userRequest.setPassword("password");
userRequest.setEmail("testuser@example.com");
userRequest.setRole("USER");
mockMvc.perform(post("/users/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(userRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("testuser"))
.andExpect(jsonPath("$.email").value("testuser@example.com"));
}
@Test
void testGetUser() throws Exception {
User user = new User();
user.setUsername("testuser");
user.setPassword("password");
user.setEmail("testuser@example.com");
user.setRole(User.Role.USER);
userRepository.save(user);
mockMvc.perform(get("/users/testuser")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("testuser"))
.andExpect(jsonPath("$.email").value("testuser@example.com"));
}
@Test
void testGetUserNotFound() throws Exception {
mockMvc.perform(get("/users/nonexistentuser")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound());
}
}
3. pom.xml
Güncellemesi
Test kütüphanelerini eklemek için pom.xml
dosyasına gerekli bağımlılıkları ekleyin.
<dependencies>
<!-- Other dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
4. Uygulama Testleri İçin application.yml
Test ortamı için gerekli yapılandırmaları da ekleyin.
spring:
datasource:
url: jdbc:postgresql://localhost:5432/usersdb_test
username: postgres
password: password
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
Sonuç
Yukarıdaki birim ve entegrasyon testleri, User microservice'in farklı katmanlarını kapsar. Bu testler, servis ve kontrolör katmanlarının beklenen şekilde çalıştığını doğrular. JUnit 5 ve Mockito kullanarak bu testleri yazmak, kodun doğruluğunu ve güvenilirliğini artırır. Entegrasyon testleri ise uygulamanın çeşitli bileşenlerinin birlikte nasıl çalıştığını test eder. Bu yapı, microservice'in sağlam ve hatasız olmasını sağlar.
- Identity Service (Kimlik Servisi): Kullanıcı kimlik bilgileri ve doğrulama süreçlerini yönetir.
- Auth Service (Kısaltılmış Kimlik Doğrulama Servisi): Kimlik doğrulama ve yetkilendirme işlemlerini kapsar.
- User Management Service (Kullanıcı Yönetim Servisi): Kullanıcı bilgilerini ve kimlik doğrulama işlemlerini kapsar.
Bu isimlendirmelerden herhangi biri, servisin amacını daha iyi yansıtabilir.