Spring Boot JWT(JSON Web Tokens) Nedir?

Web projelerimizi geliştirirken kullanıcı kimliklendirme/yetkilendirme işlemi oldukça önemlidir. Uygulamamızı yetkisiz kişilerden korumak ve yalnızca yetkili kullanıcıların erişimi için çeşitli yöntemler kullanırız. Bu çözümlerden birisi de token kullanmaktır. İşte bu noktada çeşitli standartlar bulunmaktadır. Bu yazımızda da JWT ile Authorization işlemine bakacağız. Burada Authentication ile karıştırılmamalı. Authentication, kimlik doğrulamadır. Authorization ise sisteme giriş için yetki kontrolüdür. Authentication birkez yapıldıktan sonra, atılan her istekte bir authorization işlemi yapılır. Tabi bu durum geliştirdiğiniz uygulamaya göre değişebilir.

Kısa notlar:

1-)Jwt request responselarımız üzerinde server a giden isteklerin aynı kişiden gidip gitmediğini doğrulamak için bir yöntem.

2-)Client server a istekte bulunduğunda(username-pasword olabilir.) server(sunucu) bize client a token(şifre) oluşturuyor.Bu token sonucundada isteğin sonraki isteklerden hangisinin kimden geldiğinin anlayabilmesini sağlıyor.

3-) Header token ı oluştururken kullandığımız şifreleme algoritmasının ne olduğunu belirten bir kısım var

JWT (JSON Web Tokens) Nedir? 🖥

JWT(JSON Web Tokens), bir RFC7519 endüstri standartıdır. JWT, kullanıcının doğrulanması, web servis güvenliği, bilgi güvenliği gibi birçok konuda kullanılabilir. JWT oldukça popüler ve tercih edilen bir yöntemdir.

Senaryomuz

1- Client(İstemci), login sayfasında kullanıcı adı(veya mail) ve parola ile sunucu tarafına bir login isteği gönderir.

2- Sunucu gelen bilgiyi veritabanı sorgusu ile kontrol eder. Eğer geçersizse authentication error ‘401 Unauthorized’ döner, geçerliyse kullanıcı bilgileri ve önceden belirlenmiş bir gizli anahtar ile token oluşturur. Bu token client tarafına HTTP isteğinin ‘Header’ kısmında gönderir. (Ayrıca oluşturulan token veritabanında ilgili kullanıcı adına kaydedilebilir ama JWT’nin çalışmasını etkilemez.)

3- Client(İstemci) gelen token bilgisini local depolama(localStorage gibi) alanında tutar. Daha sonra yetki isteyen tüm isteklerde(get, post, put, delete…) Header bilgisi içerisinde bu token bilgisini gönderir. Gelen her istekte sunucu tokenı kontrol eder(verify) ve kullanıcının erişip erişemeyeceğini denetler. Erişim kabul edilirse normal şekilde veri akışı devam eder. Kabul etmezse client tarafına ‘403 Forbidden’ döner.

Authorization için senaryomuz özetle bu şekilde. Tabi burada tokenın oluşturulması, verify edilmesi, HTTP isteği içerisinde bunun gönderilmesi gibi ince detaylar da var.

JWT(JSON Web Tokens) Yapısı

JWT ile üretilen token Base64 ile kodlanmış 3 ana kısımdan oluşmaktadır. Bunlar Header(Başlık), Payload(Veri), Signature(İmza) kısımlarıdır. Bu kısımlara daha yakından bakalım. Aşağıdaki token örneğinde dikkat edecek olursak aaa.bbb.ccc şeklinde noktalarla ayrılmış 3 alan bulunmaktadır.

Örnek JWT Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0dXMiOiJ0ZWJyaWtsZXIhIDopIn0.sTLXY5iAs1IzJJ-8GVP_pMR65qqgCUpbMl-aSPcrQHc

Header(Başlık)

JWT’de kullanılacak bu kısım JSON formatında yazılmakta ve 2 alandan oluşmaktadır. Bunlar token tipi ve imzalama için kullanılacak algoritmanın adı. Örnek olarak:

{
  "alg": "HS256",
  "typ": "JWT"
}

Algoritma kısmında HS256, HMAC SHA256 ya da RSA gibi birçok farklı algoritma kullanılabilir. Type kısmında ise JWT yazmakta. Bu kısım Base64 ile encode edilir ve oluşturulacak tokenın ilk parçasını oluşturur.

Payload(Veri) 📁

Bu kısım ‘claim’leri içerir. Bu kısımda tutulan veriler ile token istemci ve sunucu arasında eşsiz olur. Bu tutulan claim bilgileri de bu eşsizliği sağlar. Bu kısımda 3 tip claim bulunmaktadır.

Registered(Kayıtlı) claims: JWT tarafından önceden reserve edilmiş 3 harf uzunluğunda claimlerdir. Yani bu ayarlanmış belli claim isimlerini diğer claimlerde kullanamazsınız. Bu bilgilerin kullanılması zorunlu değildir ama önerilmektedir. Bu claimlerden bazıları iss (issuer), exp (expiration time), sub (subject), aud(audience) ve diğerleri. Bunlardan en çok kullanılanı expiration time yani son geçerlilik tarihidir. Örneğin token bilginizin 3 saat sonra geçersiz olmasını isterseniz bu bilgiyi exp alanında gönderirsiniz. 3 saat ardından aynı token ile gelen isteklerde token geçersiz olarak değerlendirilir.

Public (Açık) claims: İsteğe bağlı, açık yayınlanan claimlerdir.

Private (Gizli) claims: Tarafların kendi aralarında bilgi taşımak için kullandığı gizli claimlerdir.

Örnek bir payload alanı:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

Bu kısım Base64 ile encode edilir ve oluşturulacak tokenın ikinci parçasını oluşturur.

Signature(İmza) 🔐

Bu kısım tokenın son kısmıdır. Bu kısmın oluşturulabilmesi için header, payload ve gizli anahtar(secret) gereklidir. İmza kısmı ile veri bütünlüğü garanti altına alınır. Burada kullandığımız gizli anahtar Header kısmında belirttiğimiz algoritma için kullanılır. Header ve Payload kısımları bu gizli anahtar ile imzalanır

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-256-bit-secret
)

JWT Verify(Doğrulama) 🔓

Doğrulama işlemi üstteki senaryoda da belirttiğimiz gibi client tarafından token geldikten sonra kullanıcının yetkisini kontrol etmek için kullanılır. Tokenın geçerli olup olmadığı JWT ile doğrulanır. JWT doğrulama işlemi oldukça basittir. Gelen tokenda Header(1. kısım) ve Payload(2. kısım) sunucumuzda bulunan gizli anahtar ile imzalanır ve 3. kısım hesaplanır. Daha sonra bu oluşturulan imza(3. kısım) client tarafından gelen imza ile karşılaştırılır. Eğer imzalar aynı ise token geçerli sayılır ve kullanıcıya erişim verilir.

JWT(JSON Web Tokens) Avantajları

1- Stateless çalışır. Yani kontrol edecek bir Session bulunmamaktadır. Bilgiler ve son geçerlilik tarihi ne sunucuda ne client tarafında tutulur. Token içerisinde gerekli bilgiler tutulur.

2- Portable çalışır. Birden çok backend ile çalışabilir, yalnızca 2 taraf arasında kullanılmak zorunda değildir. Bu durum hem web uygulamanız hem de mobil uygulamanız aynı web servisi kullandığında oldukça önemlidir.

3- JSON formatını kullanır.

4- Doğrulama işlemi diğer Authorization metodlarına göre daha hızlıdır. Doğrulama işlemi için veritabanı ile bağlantı kurmaya gerek kalmaz.

5- Cookie kullanmaya gerek yoktur. Mobil uygulamalar için de rahatlıkla kullanılabilir.

Token oluşturma generate etmek:

public String generateToken(String username){

   return Jwts.builder()
           .setSubject(username)
           .setIssuer("www.haydikodlayalim.com")//kim yarattı
           .setIssuedAt(new Date(System.currentTimeMillis()))//hangi tarihte yarattı-> oluşturulan an imzalandı,yaratıldı
           .setExpiration(new Date(System.currentTimeMillis()+validity))//ne zamana kadar geçerli
           .signWith(key)//imzalarken neler kullanabiliyoruz
           .compact();
   //token oluşturuldu token geri döndürülüyor
}
//token ın validate olup olmadığını doğru mu yanlıs mı dıye kontrol edicek

Token validate ve ilgili methodlar:

public boolean tokenValidate(String token){
     if(getUsernameToken(token)!= null && isExpired(token)){
         return true;
     }
     return false;
 }

 //string token alıcak geriye string username dönücek.
public String getUsernameToken(String token){
    Claims claims= getClaims(token);
    return claims.getSubject(); //username i token ın içerisinden çıkarmış oldum
}
 public boolean isExpired(String token){
     Claims claims = getClaims(token);
     return claims.getExpiration().after(new Date(System.currentTimeMillis()));
 }

 private Claims getClaims(String token) {
     return Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();//parseClaimJws dicez içerisine token ı vericez bize bütün claimleri vericek. claim den kastımız:setIssuer claimdir,setExpiration bir claimdir. parser() metodunada secretkey ile bu işi çöz dememiz lazım
 }

Örnek TokenFilter classı:

@Component
public class JwtTokenFilter extends OncePerRequestFilter {

    @Autowired
    private TokenManager tokenManager;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {

        /**
         * "Bearer 123hab2355"
         */
        final String authHeader = httpServletRequest.getHeader("Authorization");

        String username = null;
        String token = null;


        if (authHeader != null && authHeader.contains("Bearer")) {
            token = authHeader.substring(7);
            try {
                username = tokenManager.getUsernameToken(token);
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }

        if (username != null && token != null
                && SecurityContextHolder.getContext().getAuthentication() == null) {
            if (tokenManager.tokenValidate(token)) {
                UsernamePasswordAuthenticationToken upassToken =
                        new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
                upassToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                SecurityContextHolder.getContext().setAuthentication(upassToken);
            }
        }

        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

UserDetails Class ı:

@Service
public class UserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService {

    //veritabanına bağlamadığımız için liste olusturup username sifre verdik
    private Map<String,String> users= new HashMap<>();

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @PostConstruct
    public void init(){
        users.put("temelt",passwordEncoder.encode("123"));
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if(users.containsKey(username)){
            return new User(username,users.get(username),new ArrayList<>());
        }
        throw new UsernameNotFoundException(username);
    }
    //gelen users içerisinde bu username var ise return spring framework un User ını implement edicez(spring in anlayacağı bir user vermemiz gerekiyo geriye)
    //ya bulacak listeden nesneyi spring in anladığı bir user nesnesi olarak dönece yada hata fırlatıcak.
}

WebSecurityConfiguration class ı:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter
{
    @Autowired
    private JwtTokenFilter jwtTokenFilter;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    public void configurePasswordEncoder(AuthenticationManagerBuilder builder) throws Exception{
    builder.userDetailsService(userDetailsService).passwordEncoder(getBCryptPasswordEncoder());
    }
    @Bean
    public BCryptPasswordEncoder getBCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager getAuthenticationManager() throws Exception{
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests().antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

Last updated

Was this helpful?