SAGA mikroservis uygulaması - 4

Bundan önceki yazılarda aldimbilet.com uygulamamız için tasarladığımız mikroservis yapısını neden ve nasıl tasarladığımızı gördük. Daha sonra backbone dediğimiz omurga yapısını kurguladık. Bu yazının tam olarak anlaşılabilmesi için öncekileri okumanızı tavsiye ediyorum. Tasarımımızda sıra geldi mikro olan servisleri yazmaya ve veritabanlarına erişmeye. Yazacağımız servisler birbirlerine çok benzer olacağı için öncelikle userservice yani kullanıcı işlemleri servisini detaylı yapacağız. Diğerleri benzer mantıkla (isterseniz farklı programlama dilleri ile) restful web service projesi olacak. Mimarimiz her zaman olduğu gibi burada dursun.

Userservices projesi ve pom dosyası

Uygulamamızda kullanıcı kaydı ve girişi olacağını söylemiştik. Amacımız minik servislerimizi küçük işlemlerle tanımlamak olduğu için kullanıcı kaydı, giriş ve sonrasında kullanıcı bilgileri getir servislerimiz ayrı ayrı olacak. Bu şu demek: Bir kütüphane yazıyorsunuz ve her metod separation of concerns 'ü göz önüne alarak minimum seviyede karmaşa içeriyor. Çünkü yukarıda MVC uygulamamız zaten bu metodları orkestrasyon ile yönetecek. Business gereksinimlerimiz minik servislerimizde yer almayacak. Bu servisler sadece belli bir veri üzerinde çalışacak ve içeriğindeki metodun sağlıklı işleyip işlemediği ile ilgili bilgileri döndürecek. Kodları yazınca biraz daha anlaşılır olacaktır.

Bir de şunu belirteyim. Ben kendi lokalimde bu servisleri tek bir veritabanına bağladım ama idealde ayrı ayrı veritabanlarına hatta belki de farklı teknolojilere bağlanmaları daha mantıklı olacaktır. Hatta bunların da cache 'lenmesi veya reactif çalışması gibi çözümler de mevcut. Ben lokalimde farklı adreslere bağlanmaya üşendiğim için tek veritabanına bağladım. Siz uygularken tamamen farklı veritabanlarına veya şemalara bağlayabilirsiniz.

Öncelikle şöyle bir manifestomuz olsun. Yeni eklenen her servis kendisini Eureka 'ya kaydedecek. Sonra config bilgilerini config server 'dan (dolayısı ile github 'dan) alacak. Ayrıca kullanıcı girişi gereken işlemler için JWT altyapısı kullanacak, merak etmeyin bunu da anlatacağım. Sisteme ne kadar servis eklerseniz ekleyin bu işlevleri kaçınılmaz olarak yerine getirecek. Şimdi userservice projesi için kürkçü dükkanımız olan Spring Initializr 'a giriyoruz ve aşağıdaki gibi bir proje oluşturuyoruz.

Ayrıca JWT altyapısı için de bir bağımlılığımız var. Maven repository 'de aşağıdaki adreste bulabilirsiniz güncel versiyonunu. Veya direkt olarak pom 'a ekleyebilirsiniz.

			
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
	<groupId>com.auth0</groupId>
	<artifactId>java-jwt</artifactId>
	<version>3.11.0</version>
</dependency>
			
		

"ab-util" isimli yardımcı proje de bulunuyor userservice yanında. Bu projede bazı sabit değerleri ve veriyapılarını yazdım. Bu şekilde gereksiz kod tekrarını ve kalabalığını azaltmış oldum. Bu projeyi github 'dan indirip ide 'nizde açtıktan sonra userservice pom dosyasına aşağıdaki ifadeyi eklemeniz gerekecek.

			
<dependency>
	<groupId>com.aldimbilet</groupId>
	<artifactId>util</artifactId>
	<version>0.0.1-SNAPSHOT</version>
</dependency>
			
		

Bağımlılıkların üzerinden kısaca geçeyim. İlk sırada spring web değil web services kullandık sadece restful metodları dışarıya vereceğimiz için. İkinci sıradaki JPA ise veritabanına bağlantı için kullanılacak api 'miz oluyor. Security ile JWT bilgisine göre metodlara erişime izin veren yapıyı kurgulayacağız. Config client sayesinde config server 'dan bilgileri alabileceğiz. Eureka client ile bu servisin durumunu Eureka 'ya kaydedeceğiz. Bootstrap sayesinde config bilgilerini bootstrap sırasında okuyacağımız bootstrap.properties dosyasından alabiliyoruz.

Lombok sayesinde pojo 'larımızın getter ve setter metodları kendiliğinden oluşuyor ve kodlarımızı kısaltmış oluyoruz. MySql driver ise JPA 'nın kullanacağı yani üzerinde operasyonlar yaparak verilere erişeceği sürücüyü belirtiyor. Mikroservisler açısından kritik olanlar "web services, Eureka client, config client ve bootstrap". Bunlar olmadan mikroservis olmayacaktır. Tabi POM xml 'de spring cloud versyionuna da dikkat. 2020... ve sonrası olmalı. Hoxton veya Greenwich değil.

Property dosyaları, JWT ve Security

O zaman öncelikle property 'lerden başlayalım. Application.properties içerisinde bir tane özellik var. Rastgele bir port alması için port numarası 0. Sabit vermenin ne dezavantajı vardı? Sistemde birden çok servis yazıldığında yanlışlıkla aynı portun verilmesi ihtimali azalsın düşüncesi ile böyle yaptım. Bu da kişisel bir tercih tabi. Daha önemlisi ise şu. Eureka 'ya bu servisten aynı anda 2-3 adet çalıştırıp kaydedebiliriz. Bu şekilde aralarında load balance yaptırabiliriz. Rastgele port verdiğimiz için aynı projeyi run ettiğimizde çakışma yaşamayacağız.

			
server.port = 0
			
		

Bootstrap properties dosyasında ise config server bağlantısı, servis adı ve profil bilgisi olacak.

			
spring.profiles.active=local
spring.application.name=ab-userservice

spring.cloud.config.discovery.service-id=ab-config-server
spring.cloud.config.fail-fast=true
spring.cloud.config.username=aldimbilet
spring.cloud.config.password=config

eureka.instance.instance-id=${spring.application.name}:${random.int(1,10000)}
			
		

Buradaki "profile.active" bilgisi github 'da ab-userserice-local.properties dosyasından property 'leri al ve üzerine ekle anlamı taşıyor. Config client olmasaydı bu profilin pek bir anlamı yoktu. Ama kodlarınız içerisinde Bean 'lerinizde (profile = "local") gibi bir tanım yaparak bu bean 'lerin belli bir profilde çalışmasını sağlayabiliyorsunuz yanlış bilmiyorsam. Ayrıca uygulamayı ayağa kaldırırken "-profile local" gibi bir parametre ekleyerek ayağa kalkması sırasında "application-local.properties" dosyasından property okumasını da sağlayabiliyorsunuz. Fakat bu iki özellik de şimdilik kapsamımız dışında ve biraz da alakasız.

Sonrakiler ise Eureka 'da görünecek isim ve config server bilgileri. Fail-fast ile "konfigürasyonu alamazsan hata verip dur" emrini vermiş olduk. Config server 'a erişmek için gereken kullanıcı adı ve şifreyi ve config server 'ın Eureka 'daki adını da yazmış oldum. Bu ayarları bir önceki yazıda da görmüştük. Peki oradaki eureka.instance.instance-id de neyin nesi?

Mikroservisler olmadan uygulamamız yoğunluk yaşadığında yeni bir server ekleme ihtiyacı olacağından bahsetmiştim. Bir jar veya war dosyasını sunucuya atıp ayağa kaldırdığımız için çözümü yeni fiziksel işlem gücü eklemede arıyorduk. Mikroservisler ile sistemi küçük ve bağımsız parçalara böldük ve Eureka 'da "lb://" şeklinde başlayan adreslerle load balance işlemi yaptırarak yükü dağıttık. Peki nereye dağıttık? Şu anda yazacağımız kullanıcı işlemleri servisi çok rağbet gören bir servis olması durumunda tek başına işlerin altından kalkamamaya başlayabilir. Yaptığı işlemler ağır işlemler de olabilir veya çökebilir. Bunun bir yedeğinin yani kopyasının ayakta tutulması ve ikisine sıra ile erişilmesi en mantıklısı olacaktır.

Bunu nasıl yapacağız? Çok basit aslında, uygulamayı 2 kere start edeceğiz. Rastgele bir port numarası verdiğimiz için çakışma yaşamayacağız zaten. Bu servisin 2 örneği ayağa kalkacak ve kendilerini Eureka 'ya kayıt edecekler. Bu sayede Eureka bunların arasında sıra ile request 'leri dağıtabilecek. Bu sırada Eureka 'nın web konsolunda ayağa kalkan servis örneklerine erişebilmek için de ayrıca bir id veriyoruz. Çünkü isimleri aynı olduğu için Eureka konsolu sadece (2) gibi instance sayısını yazacaktır. ID vererek istediğimiz servis instance 'ına tıklayarak gidebileceğiz. Aşağıdaki Eureka ekran görüntüsünde bulabilirsiniz bu id 'nin sonucunu.

Ayrıca uygulamanın diğer konfigürasyonları config repo 'da yani github 'da. Config repo da bir önceki yazıda incelenmişti. Oradaki repository 'de ab-userservice-local.properties isimli dosyada Eureka server ve DB bilgilerini yazmalısınız. Eureka bilgilerini yazmayı unutursanız Eureka client dependency 'si eklediğimiz için 8671 'de Eureka arayacak ve "could not connect" hatası verecektir. Bu dosyada DB ayarları da yapmalısınız. Eğer benim gibi MySql kullanırsanız Bilgisayarınızda MySql kurmanız ve 3306 default portunda bir schema oluşturmanız gerekecektir. Kullanıcı default olarak "root" olabilir. Şifreyi de kendinize göre belirleyebilirsiniz. Repo 'da bulunması gereken ayarlar aşağıdaki gibi.

			
eureka.client.service-url.defaultZone=http://aldimbilet:eureka@localhost:4442/eureka

spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/<schema>?user=root
spring.datasource.username=root
spring.datasource.password=<password>
spring.jpa.properties.hibernate.current_session_context_class = org.springframework.orm.hibernate5.SpringSessionContext
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect
spring.datasource.initialization-mode = never
			
		

Aslında bu şekilde mikroservisin minik servisi de hazır oluyor. Veritabanınız hazırsa bu servis ayağa kalkabilir ve kendini Eureka 'ya kaydedebilir. Gerekli ayarları da config server 'dan alabilir. Fakat bu servisten beklenenler bu kadar değil tabi. Buna bir de güvenlik katmanı ekleyelim. Spring security dependency 'sini eklediğimiz için zaten bütün request 'ler şu anda kullanıcı adı şifre isteyecek. Fakat biz bu servise erişilirken JWT bilgisi gönderilmesi istiyoruz. Neden? Çünkü servisin kendisi çok önemli bir iş yapmıyor, sadece içindeki belli metodlar kullanıcı girişi yapılmış olmasını gerektiriyor. Örneğin login işlemi herkese açık bir işlemdir fakat kullanıcı bilgilerini alıp getirmek giriş yapıldıktan sonra yapılabilmelidir.

Peki JWT ne demek? Json Web Tokens isimli şifrelenmiş bir veri ile session tutan bir veri yapısıdır diyebilirim kısaca. Token size bir merkez tarafından (bizim örnekte userservice) verilir ve belli bir geçerlilik süresi vardır. O süre içerisinde istediğiniz kadar giriş yapabilirsiniz. Güvenlik işlerini pek sevmediğim için fazla detaylarına da bakmadım ama yetki verme ve yekti kontrolü işlemleri yapılması için kullanılıyor diye biliyorum. Bu JWT bilgisi http isteğinin header 'ında "Authorization" anahtarı ile gider genelde. İçerisinde de "Bearer asdasdqwe123" gibi bir veri olur. JWT oluşturmak ve kontrol etmek için 3 adet class 'ımız var. Bu class 'ların işleyişini spring security bizim için hallediyor olacak.

Önce güvenlik konfigürasyonunu yazayım. Kodların aralarında yorumlarla açıklayayım parça parça takibi zorlaşmasın. Github 'da bu servisin kodları da var tabi ki.

			
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import com.aldimbilet.userservice.service.UserService;

@Configuration
// Sınıfın güvenlikten sorumlu sınıf olduğunu belirtir
@EnableWebSecurity
public class SeConfig extends WebSecurityConfigurerAdapter
{
	@Autowired
	// Bu sınıf user işlemlerini kolaylaştırıyor
	UserService userDetailsService;

	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
	{
		// Kullanıcı servisi içerisinde kullanıcıların şifreleri şifrelenir
		// Uygulama ayağa kalkarken Spring 'in kendi kullandığı AuthenticationManagerBuilder 'a şifre şifreleyicimizi verebiliriz
		auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
	}

	@Bean
	public PasswordEncoder passwordEncoder()
	{
		// BCryptPasswordEncoder yeterli güvenlikli bir şifreleyicidir
		return new BCryptPasswordEncoder();
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception
	{
		// JWT header bilgisinin iletilebilmesi için CSRF kaldırılır
		http.csrf().disable();
		// cors nedir bilemiyorum :)
		http.cors();
		// "/user" endpointi bu servisteki bütün endpoint 'lerde kullanılacak
		// gateway 'de routing işlemini de bu path 'e göre düzenlemiştik
		// login ve register işlemlerine sorgusuz geçiş izni verdik
		http.authorizeRequests().antMatchers("/user/login/**").permitAll();
		http.authorizeRequests().antMatchers("/user/register/**").permitAll();
		http.authorizeRequests().anyRequest().authenticated();
		// Filter kavramı gelen isteği filtrelemek içindir. Güvenlik filtrelerine kendi filtremizi ekliyoruz
		// Normalde spring security kendi filtrelerinden geçirir istekleri fakat biz JWT bilgisini kontrol ederek geçireceğiz
		// Bunun için özelleştirme yoluna gidiyoruz, JWTAuthenticationFilter ve JWTAuthorizationFilter bizim sınıflarımız
		http.addFilter(new JWTAuthenticationFilter(authenticationManager()));
		http.addFilter(new JWTAuthorizationFilter(authenticationManager()));
		// servisin session tutmasını engellemek istiyoruz çünkü JWT token bilgisi zaten servisi çağıran tarafta tutulacak
		http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
	}

	// Bu kodu sadece kopyala yapıştır ile aldım :)
	// Güvenlik konusu daha derin olduğu için fazla üzerinde düşünmedim
	@Bean
	CorsConfigurationSource corsConfigurationSource()
	{
		final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
		source.registerCorsConfiguration("/**", corsConfiguration);
		return source;
	}
}
			
		

Bu sınıf spring security için yazılmış security adaptörü idi. Şimdi JWT bilgisini üreten ve alıp kontrol eden sınıfları yazmamız lazım. Açıklamalar yine kodun içinde.

			
import java.io.IOException;
import java.util.Date;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.aldimbilet.userservice.model.ABUser;
import com.aldimbilet.util.JWTUtils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.fasterxml.jackson.databind.ObjectMapper;

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter
{
	// Security config 'den set edildi
	private AuthenticationManager authenticationManager;

	public JWTAuthenticationFilter(AuthenticationManager authenticationManager)
	{
		this.authenticationManager = authenticationManager;
		// Burası özellikle önemli, çünkü spring security otomatik olarak /login endpointini kullanır
		// Ama biz router 'da /user isteklerini buraya yönlendirdik
		// Bu yüzden bu ayarı eziyoruz
		setFilterProcessesUrl("/user/login");
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException
	{
		// Burası bize gelen kullanıcı adı ve şifre bilgisini JWT 'den çıkarıp kullanıcı sınıfına döndürdüğümüz yer
		// Spring security bu kullanıcı sınıfımızı kullanarak DB 'den gidip user tablosundan otomatik kontrol edecek
		try
		{
			// Request içerisinde gelecek olan veriyi ABUser sınıfımıza çevirebiliyoruz
			// Bu sınıfı kendimiz yazacağız ve içerisindeki username ve password alanları sayesinde gelen JWT bilgisi bu sınıfa map 'lenebilecek
			// Spring security 'nin getirdiği default 'lara uymanın böyle bir faydası oluyor
			// Aksi takdirde elimize spring security User sınıfı gelirdi ve onu kullanarak map işlemini kendimiz yapmak zorunda kalırdık
			ABUser creds = new ObjectMapper().readValue(req.getInputStream(), ABUser.class);
			// Burada bir gariplik var çünkü farklı hatalar da dönebiliyor
			// Bu yüzden MVC uygulamasında kullanıcı adı şifre girdiğinizde gelen hatalar için farklı farklı catch blokları yazmak zorunda kalabiliyorsunuz
			// Belki exception fırlatmak yerine response içerisinde status kodu döndürmek mantıklı olabilir
			return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(creds.getUsername(), creds.getPassword(), creds.getRoles()));
		}
		catch (IOException e)
		{
			throw new RuntimeException(e);
		}
	}

	@Override
	protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException
	{
		// Girilen kullanıcı adı ve şifre JWT ile gelir, açılır, Spring security tarafından DB 'den kontrol edilir
		// Başarısız ise buraya düşer ve response status 'ü UNAUTHORIZED olarak döner
		// Some mvc app or other app will get the UNAUTHORIZED (401) status as an indicator
		response.setStatus(HttpStatus.UNAUTHORIZED.value());
		response.getWriter().flush();
	}

	@Override
	protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException
	{
		// Girilen kullanıcı adı ve şifre JWT ile gelir, açılır, Spring security tarafından DB 'den kontrol edilir
		// Başarılı ise buraya düşer
		// Bu User sınıfı spring security user sınıfıdır, kendi ABUser sınıfımız değil
		// Yukarıdaki AuthenticationManager sınıfı spring security içerisindeki User sınıfını beklemektedir
		// JWT token 'ı ise bu kullanıcının kullanıcı adı ile oluşturuluyor çünkü unique bir bilgi, belki id gibi bir veri de denenebilirdi
		String token = JWT.create().withSubject(((User) auth.getPrincipal()).getUsername()).withExpiresAt(new Date(System.currentTimeMillis() + 900000)).sign(Algorithm.HMAC512(JWTUtils.SECRET_KEY.getBytes()));
		// Dönüşte response verisine kullanıcı adı ve token bilgisini dönebilirsiniz, istediğiniz veri yapısında tabi
		// Benim örneğin "(numan) asdasdqwe123" gibi bir veri döndürüyor, idealde bir json verisi döndürmek doğru olacaktır belki de
		String body = "(" + ((User) auth.getPrincipal()).getUsername() + ") " + token;
		// Response içerisindeki body 'ye yazılır
		res.getWriter().write(body);
		res.getWriter().flush();
	}
}
			
		

JWTAuthenticationFilter sınıfı JWT header 'ını oluşturan ve başarılı / başarısız işlem durumunda harekete geçen sınıftı. Ayrıca bir de JWTAuthorizationFilter sınıfımız olacak. Bu sınıf JWT token verisinin içindeki veriyi alıp user bilgisi şekline getirecek. Bu token 'ı yetki kontrolüne gönderecek ve filtrenin spring security 'ye eklenmesini sağlayacak.

			
import java.io.IOException;
import java.util.ArrayList;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import com.aldimbilet.util.Constants;
import com.aldimbilet.util.JWTUtils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;

public class JWTAuthorizationFilter extends BasicAuthenticationFilter
{
	public JWTAuthorizationFilter(AuthenticationManager authManager)
	{
		// Security config içerisinden auth manager göndermiştik, bunu üst class 'a bildiriyoruz
		super(authManager);
	}

	@Override
	protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException
	{
		// Burası sisteme eklediğimiz BasicAuthenticationFilter için filtrelemenin yapıldığı yer
		// Ekstra filtreyi JWT header 'ını kontrol etmek için ekledik, normalde olmayan bir işlem çünkü
		// Header 'daki "Authorization" ifadesini alıyoruz, Constants sınıfı "ab - util" isimli projede yazıyor
		String header = req.getHeader(Constants.HEADER_STRING);
		// Bu header "Bearer " ifadesi ile başlamalı örneğin "Bearer asdasdqweqwe123123"
		if (header == null || !header.startsWith(Constants.TOKEN_PREFIX))
		{
			// Header yoksa diğer filtrelerine devam et
			chain.doFilter(req, res);
			return;
		}
		// header varsa spring security 'de kullanılmak üzere context 'e Authentication metodunu bildir
		// getAuthentication metodu gerekli veriyi header 'dan alır, metodumuz aşağıda
		UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
		// Spring security 'nin okuyabilmesi için spring security context 'ine atar
		SecurityContextHolder.getContext().setAuthentication(authentication);
		// Sıradaki filtreden devam edilir, muhtemelen spring security 'nin kendi filtrelerinden birisdir
		chain.doFilter(req, res);
	}

	private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request)
	{
		// Basit bir işlem yapan bu metod, request header 'ından "Authorization" olanı alır ve Bearer 'ı çıkarır
		String token = request.getHeader(Constants.HEADER_STRING);
		if (token != null)
		{
			// parse eder, JWTUtils sınıfı ab-util projesinde sabit değerlerde
			// Burada replace yaptık çünkü header içerisinde "Bearer asdasdqwe123" gibi bir değer geliyor
			// Bu şekilde olmak zorunda değildi, best practice böyle olduğu için bu yapıyı kullandık
			String user = JWT.require(Algorithm.HMAC512(JWTUtils.SECRET_KEY.getBytes())).build().verify(token.replace(Constants.TOKEN_PREFIX, "")).getSubject();
			if (user != null)
			{
				return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<GrantedAuthority>());
			}
			return null;
		}
		return null;
	}
}
			
		

Endpoint 'ler ve kullanıcı işlemleri

Güvenlik kısmı tamam. Bu servis artık rest endpointlerine http request 'leri ile ulaşmak istediğimizde bizden JWT bilgisi bekleyecek header 'da. "Authorization" key 'i ile "Bearer asdasdqwe123" gibi. Bu tokenı da alabilmemiz için json verisi formatında kullanıcı adı ve şifre göndereceğiz. Bu kısım ileride MVC uygulamasında olacak. Güvenlik kısmını da hallettiğimize göre endpoint 'ler ekleyebiliriz. Açıklamalar yine kodlarda olacak fakat kullanıcı için gerekli olan sınıflardan başka yardımcı repository veya model sınıflarını yazmayacağım. O kısmı kendi istediğiniz şekilde gerçekleştirebilir veya Github 'daki kodlardan alabilirsiniz. Konumuz dışında kalıyorlar biraz.

			
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.aldimbilet.pojos.CardInfoPojo;
import com.aldimbilet.pojos.UserInfoPojo;
import com.aldimbilet.pojos.UserRegisterPojo;
import com.aldimbilet.userservice.model.ABUser;
import com.aldimbilet.userservice.model.CardInfo;
import com.aldimbilet.userservice.repo.CardRepository;
import com.aldimbilet.userservice.service.UserService;
import com.aldimbilet.userservice.util.MapperUtils;
import com.aldimbilet.util.JacksonUtils;

@RestController
// Bu servisin bütün path 'leri "/user" ile başlar ve gateway bu endpointe yönlendirme yapar
// Routelocator 'lara bakabilirsiniz
@RequestMapping(path = "/user")
public class UserController
{
	@Autowired
	Environment environment;

	@Autowired
	UserService userService;

	@Autowired
	CardRepository cardRepo;

	@GetMapping(path = "hello")
	public ResponseEntity<String> hello()
	{
		// /user/hello endpoint 'inde bu servisten birden fazla ayağa kaldırarak cevabın sıra ile döndüğünü görmek için port bilgisini döndürüyoruz, rastgele verilmişti çünkü
		ResponseEntity<String> entity = new ResponseEntity<>("body " + environment.getProperty("local.server.port"), HttpStatus.OK);
		return entity;
	}

	@PostMapping(path = "register")
	// @PostMapping işlemlerde parametre olarak @RequestBody eklemek zorundasınız
	// Bu body 'nin türü istediğiniz bir class olabilir
	public ResponseEntity<String> register(@RequestBody UserRegisterPojo userInfo)
	{
		// antmatcher 'lara bakarak bu endpoint 'in herkese açık olduğunu görebilirsiniz
		// UserRegisterPojo sınıfı "util" projesinden geliyor
		ABUser newUser = MapperUtils.convertUserRegisterPojoToABUser(userInfo);
		ResponseEntity<String> entity;
		// Burada validasyon yapılmamalı, bu sayede bu servis mikro olarak kalabilir yoksa makroya dönüşür
		// Burası sadece db hatasını veya data hatasını bildirmeli
		if (userService.save(newUser))
		{
			// 200
			entity = new ResponseEntity<>(HttpStatus.OK);
		}
		else
		{
			// 500
			entity = new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
		}
		return entity;
	}
	
	@GetMapping(path = "getUserInfo")
	public ResponseEntity<UserInfoPojo> getUserInfo(@RequestParam String username)
	{
		ABUser user = userService.findByUsername(username);
		UserInfoPojo pojo = MapperUtils.convertABUserToUserInfoPojo(user);
		ResponseEntity<UserInfoPojo> entity;
		// Kullanıcı bilgisini olduğu gibi döndürmektense bir aracı pojo sınıfı tanımlayıp hem bu serviste hem de MVC uygulamasında kullanmış oldum
		entity = new ResponseEntity<>(pojo, HttpStatus.OK);
		return entity;
	}
	
	@GetMapping(path = "getUserCard")
	public ResponseEntity<CardInfoPojo> getUserCard(@RequestParam Long userId)
	{
		// getUserCard bilgisi kullanıcının kredi kartı bilgisi (DB 'de kayıtlı olması lazım)
		CardInfo info = cardRepo.findByUserId(userId);
		CardInfoPojo pojo = MapperUtils.convertCardInfoToCardInfoPojo(info);
		ResponseEntity<CardInfoPojo> entity;
		// Burası business açısından bir fault tolerance oluyor
		// Kart bilgisini alamazsam hata olarak döndürüyorum ve MVC uygulamasında hata oluştuğunu anlıyorum
		// Bunun yerine status yine OK döndürülüp farklı bir veri döndürülebilirdi ve muhtemelen biraz daha best practice 'e yakınsamış olurdu
		// Fakat buradaki kararlarınız MVC uygulamasındaki FeignClient yapılarını da etkileyecektir
		if (pojo != null)
		{
			entity = new ResponseEntity<>(pojo, HttpStatus.OK);
		}
		else
		{
			entity = new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
		}
		return entity;
	}
}
			
		

Burada dikkatinizi çekmiştir, UserService isimli bir sınıfımız var. Spring security 'nin kullanıcı kayıt ve giriş işlemleri için sağladığı hazır bir şablon var. Kendi repository metodlarınızı geliştirin ve bir servis aracılığı ile kullanın diye yönlendiriyorlar geliştiricileri aslında. Bu sınıftaki önemli noktaları yazayım.

			
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User.UserBuilder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.aldimbilet.userservice.model.ABUser;
import com.aldimbilet.userservice.repo.UserRepository;

@Service
// Spring security tarafından sağlanan UserDetailsService şablonunu kullanıyoruz
public class UserService implements UserDetailsService
{
	@Autowired
	UserRepository userRepository;

	@Autowired
	// PasswordEncoder bean 'i security config sınıfımızda oluşturulmuştu
	PasswordEncoder bCryptPasswordEncoder;

	@Override
	@Transactional(readOnly = true)
	public UserDetails loadUserByUsername(String username)
	{
		// ABUser kendi user tanımımız
		ABUser user = userRepository.findByUsername(username);
		// Bu kullanıcı bilgileri ile spring security 'nin UserBuilder sınıfına çeviriyoruz
		UserBuilder builder = org.springframework.security.core.userdetails.User.withUsername(user.getUsername());
		// Kullanıcının şifresini ve rollerini de bildiriyoruz
		builder.password(user.getPassword());
		builder.authorities(user.getRoles());
		return builder.build();
	}
	
	public boolean save(ABUser user)
	{
		// Kullanıcı kaydetmeden önce şifre şifrelemek :)
		user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
		return userRepository.save(user);
	}

	public ABUser findByUsername(String username)
	{
		return userRepository.findByUsername(username);
	}

	public ABUser findById(Long userId)
	{
		return userRepository.findById(userId);
	}
}
			
		

Bu şekilde userservice yani kullanıcı işlemleri servisinin endpoint 'lerini ve güvenlik önlemlerini bitirmiş oluyoruz. DB ayarlarınızı yapabildiyseniz ve ab-util projesini dahil ettiyseniz bu servisi de ayağa kaldırabileceğinizi düşünüyorum. Run as -> Spring Boot Application şeklinde ayağa kaldırabilirsiniz. Dilerseniz birden fazla kere ayağa da kaldırabilirsiniz. Eureka web konsolunda görebileceksiniz.

Failover servisi

Bir de bu servis ayakta değil iken cevap verecek ab-userservice-failover servisi yazmamız gerekecek. Sonuçta resilliency ve fault tolerance kavramını da göz önünde bulundurmamız gerekiyor. Bu servisi detaylı yazmayacağım. Önemli olan 1 adet @Controller sınıfı bulunuyor ve get ve post isteklerine karşılık veriyor. Userservice 'te get ve post işlemleri olduğu için bunlar gateway 'de forward edilince bu servise de get ve post olarak düşecek. Put metodumuz olursa onu da ileride eklememiz gerekir tabi.

Userservice-failover için dependency 'lerde web services, config client, eureka client ve bootstrap olması yetecektir. Yine application properties içerisinde port numarası 0 ve bootstrap.properties içerisinde application.name bilgisi, profile=local bilgisi ve config server bilgileri olması lazım. Bunlar userservice ile neredeyse birebir aynı zaten. Bir de github repository 'sinde "ab-userservice-failover-local.properties" dosyasında userservice 'de olduğu gibi Eureka bağlantısı yazmanız gerekecek. Bu da userservice 'dekinin aynısı. Bu şekilde hazırladığınız spring boot projesine bir controller ekleyebilirsiniz.

			
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserServiceController
{
	// Gateway kodlarını bir önceki yazıda yazmıştık, routing sırasında userservice 'e ulaşılamazsa "forward/user-failover" yapılıyordu
	// Response status ok döndürüp farklı bir veri döndürülebilirdi
	// <Object> kullanılarak userservice 'deki bütün request 'lere cevap verebilmesi amaçlandı
	@RequestMapping(path = "user-failover", method = RequestMethod.GET)
	public ResponseEntity<Object> userServiceFails()
	{
		ResponseEntity<Object> entity = new ResponseEntity<>("user service is down", HttpStatus.SERVICE_UNAVAILABLE);
		return entity;
	}

	@RequestMapping(path = "user-failover", method = RequestMethod.POST)
	public ResponseEntity<Object> userServiceFails(@RequestBody Object body)
	{
		ResponseEntity<Object> entity = new ResponseEntity<>("user service is down", HttpStatus.SERVICE_UNAVAILABLE);
		return entity;
	}
}
			
		

Sadece tek bir sınıfla failover uygulaması da ayağa kalkabilir ve Eureka 'da görünebilir. Pek gereği olmasa da dilerseniz bundan da birden fazlasını ayağa kaldırabilirsiniz. Bu noktada kendiniz geliştrmek adına activityservice ve payment servislerini ve bunların failover servislerini (neredeyse userservice ve failover ile birebir aynı) kendiniz yazıp ayağa kaldırabilirsiniz. Sisteme sıfırdan bir mikroservis eklemek için gereken kılavuzu da buraya ekleyeyim.

  • Aşağıdaki dependency 'lerle bir spring boot projesi oluşturun
    • spring-boot-starter-web-services
    • spring-boot-starter-security
    • spring-cloud-starter-config (client)
    • spring-cloud-starter-bootstrap
    • spring-cloud-starter-netflix-eureka-client
  • JWT için gerekli dependency 'yi ekleyin
  • "ab - util" projesini 'de pom 'a ekleyebilirsiniz
  • application.properties içerisinde server.port = 0 olmalı
  • bootstrap.properties içerisinde ise aşağıdakileri ekleyin
    • spring.application.name = <app_name>
    • eureka.instance.instance-id=${spring.application.name}:${random.int(1,10000)}
    • spring.cloud.config.discovery.service-id=ab-config-server
    • spring.cloud.config.fail-fast=true
    • spring.cloud.config.username=aldimbilet
    • spring.cloud.config.password=config
    • spring.profiles.active=<profile>
  • <app_name>-<profile>.properties şeklinde bir dosyayı github 'daki config reponuza ekleyin
  • Config repo 'da eureka bağlantı adresini yazın
  • BasicAuthenticationFilter sınıfından bir extend yapın veya direkt olarak kodları userservice 'den alın
  • UsernamePasswordAuthenticationFilter sınıfından extend yapın veya direkt olarak kodları userservice 'den alın
  • WebSecurityConfigurerAdapter sınıfı yazın veya kopyala yapıştır ile userservice 'den alın
  • @GetMapping, @PostMapping gibi http metodları ile @Controller sınıfı oluşturun
  • @PostMapping ise parametre olarak @RequestBody eklemeyi unutmayın
  • Return type olarak ResponseEntity<T> döndürün (T istediğiniz bir sınıf veya veri yapısı)
  • Daha sonra MVC uygulamasında kullanmak üzere mapping türünü, path 'ini, parametrelerini ve dönüş değerini not edin
  • Kullanıcı girişi gerekiyor mu sorusuna karar verin
  • Giriş gerekmiyorsa security config içerisindeki antmatcher 'lara permitall() olarak path 'i ekleyin
  • Kullanıcı girişi gereken metodlar için de antmatcher ekleyin
  • Gateway 'deki routeconfig sınıfında gerekli route bilgisini (örneğin "/<myservice>" gibi bir ifade ile) ekleyin
  • Failover servisi oluştursanız failover için de route eklemeyi unutmayın

Userservice ve failover hazır ve ayağa kalkabilmiş ise Eureka consolunda aşağıdaki gibi görünecektir. Userservice 2 kere ayağa kaldırıldığı için UP (2) ifadesini görebilirsiniz. Bu servisler için ID bilgisini rastgele vermiştik. Bu yüzden 2 adet link görebiliyoruz. Failover için id bilgisi vermediğimiz için yanında port numarası olan 0 'ı görüyoruz. Fakat aslında uygulama ayağa kalkarken server 'da rastgele bir port aldı. Biz bunu burada göremiyoruz.

Gerisi size kalmış

Bu şekilde kendi minik servislerinizi ve endpointlerini ekleyebilirsiniz umarım. Kullanıcı servisi ile ilgili en çok zorlandığım nokta JWT ile ilgili güncel (Aralık 2020) bir Java örneğini bulmaktı. Mikroservisler ile ilgili yazılarda genelde sistemin tool 'ları ve mimarisi üzerine çok konuşuluyor fakat JWT bilgisi nereden gelir nerede tutulur ve nasıl gönderilir gibi bir anlatım pek yoktu. Ayrıca kullanıcı servisindeki rest metodlarının parametrelerinde @RequestBody zorunlu imiş. Bunu bilmediğim için metod parametresi olduğu halde bu metoda neden MVC tarafından post edemiyorum diye çok düşünmüştüm.

Şu anda mikroservis tasarımımız ve kodlamamız çoğunlukla bitmiş oldu. Tebrik ediyorum beraber bu noktaya gelebildiyseniz :) Şimdi bu servisleri çalıştıran, daha doğrusu bu altyapıdan faydalanan ve orkestrasyon yapan MVC uygulamamıza sıra geldi. En başta söylediğimiz işlevleri hayata geçirmek için gerekli kodları servislere böldük ve şimdi sıra işletmesinde ve konuşturmasında.

Yine epeyce uzun bir yazı oldu fakat mikroservis dediğimiz şuraya şu kodu yazın çalışır şeklinde bir kavram değildir demiştim. Bir kullanıcı servisi JWT header 'ları kullanıyor ve basit read işlemleri yapıyorsa bile yazılan her kod bir kararın sonucu oluyor veya başka bir karara sebep oluyor. Getirisi ve götürüsü oluyor. Bu yüzden açıklama yaparak yazmak en kalıcı öğrenme biçimi oluyor. MVC uygulamasını yazacağımız bir sonraki yazıda görüşmek üzere :)


Bir yorum yazabilirsiniz