Merhabalar arkadaşlar.

InMemory User ve DataBase’de Tanımlı Kullanıcılar yazılarımızda Spring Boot ve Spring Security ile bir Rest Web Servis’i Basic Authentication ile korumayı görmüştük.

Bu yazıda ise token bazlı doğrulama ve yetkilendirmeyi de görmüş olacağız ve bunu JSON Web Token kullanarak yapacağız. Yazıdaki örneğimiz bir önceki yazıdaki örneğimizin üstüne eklemeler yapılmış hali olacak, bu nedenle yazının kapsamı içinde olmayan konuları es geçeceğim.

JSON Web Token hakkında bilgi edinmek ya da bilgi tazelemek için Rahman Hocamın şuradaki yazısını okuyabilirsiniz.

WebSecurityConfig.java

WebSecurityConfig sınıfımız içerisinde sadece configure(HttpSecurity http) metodunda değişiklik yapacağız arkadaşlar.

@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.cors().configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues()).and().csrf()
				.disable().authorizeRequests()
				.antMatchers("/memberList").access("hasRole('ROLE_ADMIN')")
				.antMatchers(HttpMethod.POST, "/login").permitAll().anyRequest().authenticated().and()
				.addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
						UsernamePasswordAuthenticationFilter.class)
				.addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
	}

Metodumuz içerisinde şu değişiklikleri yaptık:

  • antMatchers(HttpMethod.POST, “/login”).permitAll() ifadesi ile /login path’ine sadece POST isteklerinin yapılabileceğini ve herkese açık olduğunu söyledik.

  • addFilterBefore(new JWTLoginFilter(“/login”, authenticationManager()),UsernamePasswordAuthenticationFilter.class) ifadesi ile /login path’ine yapılacak tüm isteklerin JWTLoginFilter’dan geçmesini sağlıyoruz. JWTLoginFilter bize doğrulanmış kullanıcı için token bilgisi dönecek.

  • addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) satırı ile de login dışında kalan path’lere yapılacak isteklerin JWTAuthenticationFilter’dan geçmesini sağlıyoruz. JWTAuthenticationFilter, gelen istekler içerisinde token olup olmadığını kontrol edecek ve token’dan ilgili bilgileri alıp Spring Security’e aktaracak.

JWTLoginFilter.java

package com.ilkaygunel.filter;

import java.io.IOException;
import java.util.Collections;

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.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.ilkaygunel.pojo.AccountCredentials;
import com.ilkaygunel.security.TokenAuthenticationService;

public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {

	public JWTLoginFilter(String url, AuthenticationManager manager) {
		super(new AntPathRequestMatcher(url));
		setAuthenticationManager(manager);
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest arg0, HttpServletResponse arg1)
			throws AuthenticationException, IOException, ServletException {
		AccountCredentials creds = new ObjectMapper().readValue(arg0.getInputStream(), AccountCredentials.class);
		return getAuthenticationManager()
				.authenticate(new UsernamePasswordAuthenticationToken(creds.getUsername(), creds.getPassword()));
	}

	@Override
	protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
			Authentication auth) throws IOException, ServletException {
		TokenAuthenticationService.addAuthentication(res, auth);
	}
}

JWTLoginFilter sınıfı WebSecurityConfig sınıfında tanımladığımız üzere /login path’ine gelen istekleri önce attemptAuthentication() metodu ile gelen username&password’ü vererek Spring Security vasıtası ile doğrulatacak ve gelen user geçerli bir user ise successfulAuthentication() metodu içerisinde TokenAuthenticationService sınıfındaki addAuthentication() metodunu kullanarak dönülecek response’a bir token eklemesi yapacak.

JWTAuthenticationFilter.java

package com.ilkaygunel.filter;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import com.ilkaygunel.security.TokenAuthenticationService;

public class JWTAuthenticationFilter extends GenericFilterBean {

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
			throws IOException, ServletException {
		Authentication authentication = TokenAuthenticationService.getAuthentication((HttpServletRequest) request);

		SecurityContextHolder.getContext().setAuthentication(authentication);
		filterChain.doFilter(request, response);
	}
}

JWTAuthenticationFilter sınıfı WebSecurityConfig sınıfında da belirtildiği gibi /login path’i dışında kalmış olan tüm path’lere gelen istekleri denetleyecek olan sınıftır. doFilter() metodu TokenAuthenticationService sınıfındaki getAuthentication metodu ile bir adet Authentication nesnesi alır ve SecurityContextHolder.getContext().setAuthentication(authentication) kod satırındaki setAuthentication() metoduna bu Authentication nesnesini parametre olarak geçirir. setAuthentication() metodu kendisine gelen Authentication nesnesi ile gelen bilgilere sahip user’ın gerçekten olup olmadığı ve yetkisinin ne olduğu durumlarına bakar. Akabinde JWTAuthenticationFilter filterChain.doFilter() ile red ya da kabul işlemini yaparak görevini tamamlar.

TokenAuthenticationService.java

package com.ilkaygunel.security;

import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.ilkaygunel.pojo.AccountCredentials;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import static java.util.Collections.emptyList;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

public class TokenAuthenticationService {

	static final long EXPIRATIONTIME = 864_000_000; // 10 days
	static final String SECRET = "ThisIsASecret";
	static final String TOKEN_PREFIX = "Bearer";
	static final String HEADER_STRING = "Authorization";

	public static void addAuthentication(HttpServletResponse res, Authentication auth) {

		String concattedRoles = "";
		for (GrantedAuthority ga : auth.getAuthorities()) {
			if (!"".equals(concattedRoles)) {
				concattedRoles += "," + ga.getAuthority();
			} else {
				concattedRoles += ga.getAuthority();
			}

		}

		String JWT = Jwts.builder().setSubject(auth.getName()).claim("roles", concattedRoles)
				.setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
				.signWith(SignatureAlgorithm.HS512, SECRET).compact();
		res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + JWT);
	}

	public static Authentication getAuthentication(HttpServletRequest request) {
		String token = request.getHeader(HEADER_STRING);
		if (token != null) {
			Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
					.getBody();
			String user = claims.getSubject();
			String roles = (String) claims.get("roles");
			List<String> roleList = Arrays.asList(roles.split("\\s*,\\s*"));
			List<GrantedAuthority> grantedAuths = new ArrayList<GrantedAuthority>();
			for (int i = 0; i < roleList.size(); i++) {
				SimpleGrantedAuthority abv = new SimpleGrantedAuthority(roleList.get(i));
				grantedAuths.add(abv);
			}
			return user != null ? new UsernamePasswordAuthenticationToken(user, null, grantedAuths) : null;
		}
		return null;
	}
}

TokenAuthenticationService sınıfı içerisinde addAuthentication(…) ve getAuthentication(…) metotları yer almakta. addAuthentication(…) metodu

  • Authentication nesnesi üzerindeki rolleri concattedRoles String’inde virgüllerle ayrılmış şekilde tutar. Bir sistem üzerindeki bir kullanıcının birden fazla rolü olabilir, bu nedenle kullanıcıya verilecek token içerisinde kullanıcının sahip olduğu tüm roller de olmalıdır.

  • JWT string’ini elde ettiğimiz satırda ise setSubject(…) metoduna kullanıcı adını geçiriyoruz.

  • claim(…) metoduna key-value çifti halinde parametre geçiriyoruz ve kullanıcının sahip olduğu rolleri tutan concattedRoles String’ini roles key’i ile kaydediyoruz. claim(…) metodunu daha çok token içerisinde gidip gelmesini istediğimiz alanlar için kullanıyoruz.

  • setExpiration(…) metodu ile token’ın ne kadar süre için geçerli olacağını söylüyoruz. Bizim örneğimizde 10 günlük bir expiration time var.

  • signWith(…) metoduna bir hash algoritması ve bir de hash sırasında kullanılacak gizli bir anahtar bilgisini geçirerek JWT tokenımızı imzalıyoruz.

  • Son olarak compact(…) metodu ile de String tiğindeki JWT token’ı elde ediyoruz.

  • Elde ettiğimiz toke’ı da res.addHeader(…) diyerek HEADER_STRING değişkeninin değeri ile response’a ekliyoruz.

getAuthentication(…) metodu ise,

  • request nesnesinin header’ından HEADER_STRING değişkenin değeri ile token’ı alıyor.

  • Eğer token gelmiş ise Claims yani diğer payload kısmından veriler almaya başlıyoruz. Daha önce subject’e koyduğumuz username’i ve roles bilgilerini alıyoruz. roles nesnesi birden fazla role var ise virgüllerle ayrılmış halde olacağı için öncelikle virgüllerle ayrılmış o string’den bir liste (roleList) elde ediyoruz ve bir for döngüsü ile GrantedAuthority tipinde veri tutan listeye bu rolleri ekliyoruz.

  • Son adımda ise username’ti tutan user değişkeni boş değil ise UsernamePasswordAuthenticationToken sınıfının yapılandırıcısına user, null ve rolleri tutan grantedAuths nesnelerini geçirerek UsernamePasswordAuthenticationToken nesnesini döndürüyoruz.

AccountCredentials.java

package com.ilkaygunel.pojo;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;

public class AccountCredentials {

	private String username;
	private String password;
	private Collection<GrantedAuthority> roles;

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public Collection<GrantedAuthority> getRoles() {
		return roles;
	}

	public void setRoles(Collection<GrantedAuthority> roles) {
		this.roles = roles;
	}

}

AccountCredentials sınıfı da gelen kullanıcı için bilgiler tutacağımız basit bir pojo sınıfı.

Ekran Görüntüleri

Şimdi Postman üzerinden http://localhost:8080/SpringSecurityWithSpringBoot/login adresine body kısmında raw halde aşağıdaki ekran görüntüsünde olan JSON’ı POST ediyorum ve cevap kısmında bana token dönüşü oluyor:

Bu token ile şimdi de http://localhost:8080/SpringSecurityWithSpringBoot/memberList adresine header içerisinde Authorization key’i ile edindiğim token’ı bulundurarak GET isteği gönderiyorum. Web servis bana ilgili verileri dönüyor.

Şimdi rolü ROLE_USER olan bir kullanıcı ile aynı işlemi deniyelim.

Gördüğümüz gibi Spring Security role yetkili olmadığı için ilgili kaynağa erişimimi reddediyor.

Bu yazıda anlatacaklarım bu kadar arkadaşlar. Başka bir yazıda görüşene kadar sağlıcakla kalın.

Selam ve Sevgilerimle