Sem Göksu
Sem Göksu
Yazılım · Yolculuk · Fenerbahçe
React Native

Fullstack Mobil — Bölüm 3: JWT ile Authentication

API'mize JWT tabanlı kimlik doğrulama ekleyip RN tarafında tam auth akışını kuruyoruz. BCrypt şifre hash, AuthController, Axios interceptor ile token enjeksiyonu, Zustand store ile auth state — production seviyesinde güvenlik.

21 Nisan 2026 6 dk okuma 18 0
Add Scaffold > Identity - login/register sayfalarini override etmek icin isaretliyoruz.
Add Scaffold > Identity - login/register sayfalarini override etmek icin isaretliyoruz.

IDE tarafinda: Visual Studio tarafinda projeye sag tiklayip Add > New Scaffolded Item > Identity diyerek bu dialog acilir. Ozellestirmek istediginiz sayfalari isaretleyip ApplicationDbContext'i seciyoruz.

Merhaba arkadaşlar, serinin üçüncü bölümüne hoş geldiniz. Geçen yazıda CRUD API'mizi yazdık ama her şey açıktaydı — kim isterse herkes DELETE yapabilirdi. Bu bölümde bunu düzeltiyoruz: JWT tabanlı authentication kuruyoruz, kullanıcı kayıt/giriş akışını kodluyoruz, RN tarafında token'ı güvenli saklayıp her istekle API'ye göndereceğiz.

Akışı Görselleştirelim
Login akışı şöyle:

1. Kullanıcı RN app'te e-posta + şifre girer
2. RN → POST /api/auth/login — API bilgileri doğrular
3. API doğruysa JWT token üretip döndürür
4. RN bu token'ı AsyncStorage'a kaydeder
5. Bundan sonraki her istekte Authorization: Bearer <token> header'ı eklenir
6. API her istekte token'ı doğrular, [Authorize] attribute'u olan endpoint'leri korur

Backend: Paketleri Ekleyelim
cd api
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package BCrypt.Net-Next
BCrypt şifre hash'leme için. Düz metin şifre asla saklanmaz.

Auth Servisi
// Services/AuthService.cs
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;

public interface IAuthService
{
    Task<string?> LoginAsync(string email, string sifre);
    Task<User> RegisterAsync(string kullaniciAdi, string email, string sifre);
}

public class AuthService(AppDb db, IConfiguration config) : IAuthService
{
    public async Task<User> RegisterAsync(string kullaniciAdi, string email, string sifre)
    {
        var existing = await db.Users
            .FirstOrDefaultAsync(u => u.Email == email || u.KullaniciAdi == kullaniciAdi);
        if (existing is not null)
            throw new InvalidOperationException("Bu e-posta veya kullanıcı adı zaten kayıtlı.");

        var user = new User
        {
            KullaniciAdi = kullaniciAdi,
            Email = email,
            SifreHash = BCrypt.Net.BCrypt.HashPassword(sifre),
            Rol = "User"
        };
        db.Users.Add(user);
        await db.SaveChangesAsync();
        return user;
    }

    public async Task<string?> LoginAsync(string email, string sifre)
    {
        var user = await db.Users.FirstOrDefaultAsync(u => u.Email == email);
        if (user is null) return null;
        if (!BCrypt.Net.BCrypt.Verify(sifre, user.SifreHash)) return null;
        return TokenUret(user);
    }

    private string TokenUret(User user)
    {
        var key = config["Jwt:Key"]!;
        var issuer = config["Jwt:Issuer"]!;
        var audience = config["Jwt:Audience"]!;
        var expireMin = int.Parse(config["Jwt:ExpireMinutes"] ?? "60");

        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
            new Claim(JwtRegisteredClaimNames.UniqueName, user.KullaniciAdi),
            new Claim(JwtRegisteredClaimNames.Email, user.Email),
            new Claim(ClaimTypes.Role, user.Rol),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
        };

        var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
        var creds = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: issuer,
            audience: audience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(expireMin),
            signingCredentials: creds);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}
AuthController
// Controllers/AuthController.cs
public record RegisterDto([Required] string KullaniciAdi, [Required, EmailAddress] string Email, [Required, MinLength(6)] string Sifre);
public record LoginDto([Required] string Email, [Required] string Sifre);
public record AuthResponseDto(string Token, string KullaniciAdi, string Rol);

[ApiController]
[Route("api/[controller]")]
public class AuthController(IAuthService auth) : ControllerBase
{
    [HttpPost("register")]
    public async Task<IActionResult> Register([FromBody] RegisterDto dto)
    {
        try
        {
            var user = await auth.RegisterAsync(dto.KullaniciAdi, dto.Email, dto.Sifre);
            return Ok(new { user.Id, user.KullaniciAdi, user.Email });
        }
        catch (InvalidOperationException ex)
        {
            return Conflict(new { error = ex.Message });
        }
    }

    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginDto dto)
    {
        var token = await auth.LoginAsync(dto.Email, dto.Sifre);
        if (token is null) return Unauthorized(new { error = "Geçersiz bilgiler" });
        // Rolü tekrar çekmek yerine token'dan parse edip döneriz — pratikte servisten dönsün daha iyi
        return Ok(new AuthResponseDto(token, dto.Email, "User"));
    }
}
Program.cs'te Auth Pipeline
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

builder.Services.AddScoped<IAuthService, AuthService>();

var jwtKey = builder.Configuration["Jwt:Key"]!;
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(o =>
    {
        o.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)),
            ClockSkew = TimeSpan.Zero
        };
    });

builder.Services.AddAuthorization();

// ...
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
Sıra geldi endpoint'leri korumaya:

[Authorize]
[HttpPost]
public async Task<IActionResult> Olustur([FromBody] PostCreateDto dto)
{
    var userId = int.Parse(User.FindFirstValue(JwtRegisteredClaimNames.Sub)!);
    var post = new Post
    {
        Baslik = dto.Baslik,
        Icerik = dto.Icerik,
        YazarId = userId,
        Yayinda = false
    };
    db.Posts.Add(post);
    await db.SaveChangesAsync();
    return CreatedAtAction(nameof(Getir), new { id = post.Id }, new { id = post.Id });
}

[Authorize(Roles = "Admin")]
[HttpDelete("{id:int}")]
public async Task<IActionResult> Sil(int id) { ... }
Frontend: Mobile Tarafında Auth
RN tarafında AsyncStorage kullanacağız:

cd mobile
npx expo install @@react-native-async-storage/async-storage
npm install axios zustand
Axios Instance + Interceptor
// src/api/client.ts
import axios from 'axios';
import AsyncStorage from '@@react-native-async-storage/async-storage';

export const api = axios.create({
  baseURL: __DEV__ ? 'http://192.168.1.42:5000' : 'https://api.blogapp.com',
  timeout: 15000
});

// Her istekte token'ı ekle
api.interceptors.request.use(async (config) => {
  const token = await AsyncStorage.getItem('authToken');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 401 gelirse token'ı temizle
api.interceptors.response.use(
  (r) => r,
  async (error) => {
    if (error.response?.status === 401) {
      await AsyncStorage.removeItem('authToken');
    }
    return Promise.reject(error);
  }
);
Auth Store — Zustand
// src/store/auth.ts
import { create } from 'zustand';
import AsyncStorage from '@@react-native-async-storage/async-storage';
import { api } from '../api/client';

type AuthState = {
  token: string | null;
  kullaniciAdi: string | null;
  rol: string | null;
  yuklendi: boolean;
  login: (email: string, sifre: string) => Promise<void>;
  logout: () => Promise<void>;
  baslat: () => Promise<void>;
};

export const useAuth = create<AuthState>((set) => ({
  token: null,
  kullaniciAdi: null,
  rol: null,
  yuklendi: false,

  baslat: async () => {
    const token = await AsyncStorage.getItem('authToken');
    const ka = await AsyncStorage.getItem('kullaniciAdi');
    const rol = await AsyncStorage.getItem('rol');
    set({ token, kullaniciAdi: ka, rol, yuklendi: true });
  },

  login: async (email, sifre) => {
    const res = await api.post('/api/auth/login', { email, sifre });
    await AsyncStorage.setItem('authToken', res.data.token);
    await AsyncStorage.setItem('kullaniciAdi', res.data.kullaniciAdi);
    await AsyncStorage.setItem('rol', res.data.rol);
    set({ token: res.data.token, kullaniciAdi: res.data.kullaniciAdi, rol: res.data.rol });
  },

  logout: async () => {
    await AsyncStorage.multiRemove(['authToken', 'kullaniciAdi', 'rol']);
    set({ token: null, kullaniciAdi: null, rol: null });
  }
}));
Login Ekranı
// src/screens/LoginScreen.tsx
import { useState } from 'react';
import { View, Text, TextInput, Button, Alert, StyleSheet } from 'react-native';
import { useAuth } from '../store/auth';

export function LoginScreen() {
  const [email, setEmail] = useState('');
  const [sifre, setSifre] = useState('');
  const [yukleniyor, setYukleniyor] = useState(false);
  const login = useAuth((s) => s.login);

  const handleLogin = async () => {
    setYukleniyor(true);
    try {
      await login(email, sifre);
    } catch (err: any) {
      Alert.alert('Hata', err.response?.data?.error ?? 'Giriş yapılamadı');
    } finally {
      setYukleniyor(false);
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.baslik}>Giriş Yap</Text>
      <TextInput
        placeholder="E-posta"
        value={email}
        onChangeText={setEmail}
        autoCapitalize="none"
        keyboardType="email-address"
        style={styles.input}
      />
      <TextInput
        placeholder="Şifre"
        value={sifre}
        onChangeText={setSifre}
        secureTextEntry
        style={styles.input}
      />
      <Button title={yukleniyor ? 'Giriş yapılıyor...' : 'Giriş'} onPress={handleLogin} disabled={yukleniyor} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', padding: 20, backgroundColor: '#0f172a' },
  baslik: { fontSize: 32, fontWeight: 'bold', color: '#fff', marginBottom: 30, textAlign: 'center' },
  input: { backgroundColor: '#1e293b', color: '#fff', padding: 14, borderRadius: 8, marginBottom: 12, fontSize: 16 }
});
Güvenlik Notları
- AsyncStorage şifreli değil: Jailbreak edilmiş telefonlarda okunabilir. Hassas veri için expo-secure-store kullanın.
- HTTPS zorunlu: Production'da asla HTTP kullanmayın, token çalınır.
- Token refresh: JWT'nin süresi dolunca refresh token pattern ile yenileme yapın.
- Logout temizliği: Sadece state'i değil storage'ı da temizleyin.

Özet
Bu bölümde full auth akışı kurduk: backend'de BCrypt + JWT, mobilde Axios interceptor + Zustand store + AsyncStorage. Artık korumalı endpoint'lerimiz ve girişli kullanıcı deneyimimiz var.

Bir sonraki bölümde UI tarafına yoğunlaşıyoruz: Navigation, ekran akışları, React Query ile data fetching, liste + detay + form ekranları. Başka bir makalede görüşmek üzere...
Etiketler: #ASP.NET #C#
Paylaş:

Yorumlar (0)

Henüz yorum yok. İlk yorumu sen yap!

Yorum bırak

* Yorumlar moderasyon sonrası yayınlanır. E-posta gizli tutulur.