Login Olunmayan Mobile Bir Projede, Token İle Güvenlik
Selamlar,
Bu makalede, authentication yapısı olmayan bir mobile projede, servisler üzerinden dataya erişimin, nasıl gelişi güzel yapılmaması gerektiğini tartışacağız. Aslında bu konu, tüm platformlar için geçerlidir. Örneğin herhangi bir haber portalına giderseniz, çoğunlukla çekilen haber servislerinin korumasız olduğunu görebilirsiniz. Aşağıda, Chrome üzerinde F12 ile Network dinlenerek, gidilen bir haber detay sayfanın servisi görülmektedir. Siz de postman üzerinden aynı servise request atarsanız, ilgili content’e kolaylıkla erişebilirsiniz.
Örnek Postman: Aşağıda görüldüğü gibi yakalanan servis her yerden, kolaylıkla çağrılabilmektedir.
Bu güvenlik sorununun bir nebze olsun giderilebilmesi için, farklı platformlarda çalışan, bir doğrulama metodolojisinin oluşturulması gerekmektedir. Öncelikle login sistemi olmadığı için, ilgili Token’ın bu senaryoda Mobile tarafından üretilmesi ve, yine bu Token’ın geçerliliğinin aynı yöntem ile web servisi tarafında yani backend’den, sağlanması gerekmektedir.
Şimdi gelin örnek amaçlı, .Net Core tarafında ilgili Token’ı oluşturan ve kontrol eden methodları yazalım.
.Net Core bir projede, ilgili methodlar projenin birçok yerinde kullanılabileceği için, Core katmanında Security altında aşağıdaki gibi Encryption class’ında tanımlanmıştır.
JWT TOKEN İŞLEMLERİ:
Core/Security/Encryption.cs / GetJwtToken(): Aşağıda görüldüğü gibi, Jwt kütüphanesi kullanılarak static bir SecurityKey ile, token oluşturulmuştur. Eğer aynı static key, diğer token üretilecek platformlarda da kullanılır ise, backend tarafında Jwt üzerinden validate işlemi global olarak kolaylıkla yapılabilmektedir.
- “var tokenHandler = new JwtSecurityTokenHandler()” : tokenHandler, kendisine verilen parametrelere göre token üreten Jwt nesnesidir.
- “var key = Encoding.ASCII.GetBytes(_Config.Value.PrivateKey)” : Tüm platformlarda, hem token üretimi hem de var olan token’ın test edilmesi amacı ile kullanılan ortak static keydir. Bu projede, Config dosyasından çekilmektedir.
- “var tokenDescriptor = new SecurityTokenDescriptor” : Yaratılacak token’ın, parametrelerinin tanımlandığı kısımdır.
- “new Claim(ClaimTypes.Name, deviceID)” : İsim olarak, mobileden gönderilen unique deviceID değeri kullanılmaktadır.
- “Expires = DateTime.UtcNow.AddHours(1)” : Oluşturulacak token 30 dakka süre ile geçerlidir. Yani biri bu token’ı ele geçirse, en fazla 30 dakka boyunca ilgili servislere erişebilecektir. Bu süre daha da kısaltılabilir.
- “SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)” : İlgili token, Sha256 algoritması ve belirlenen static secret key ile oluşturulur.
- “var token = tokenHandler.CreateToken(tokenDescriptor)” : Tanımlanan parametrelere göre, Token oluşturulur.
- “return tokenHandler.WriteToken(token)” : Oluşturulan token, string olarak geri dönülür.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public string GetJwtToken(string deviceID) { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_Config.Value.PrivateKey); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, deviceID) }), Expires = DateTime.UtcNow.AddMinutes(30), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } |
Core/Configuration/Config.cs: Config’den çekilen, örnek amaçlı kullanılan SecurityKey’dir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
namespace Core.Configuration { public class Config { #region Props . . public string PrivateKey { get; set; } #endregion public Config() { PrivateKey = "2909012565820034"; } } } |
Core/Security/Encryption.cs / ValidateJwtToken(): Backend tarafında kullanılan, token doğrulama amaçlı bir methodur. Amaç, login olunmadan mobile veya diğer platformlardan yaratılan tokenların, request anında middleware’de araya girilip kontrol edilmesi ve doğru olması durumunda action’a gidilmesidir. İlgili token’ın geçerli olmama durumunda, “401 Unauthorized“ dönülmektedir.
- “var tokenHandler = new JwtSecurityTokenHandler()” : Jwt kütüphanesinin vazgeçilmez sınıfı, “JwtSecurityTokenHandler” Token doğrulama ve token üretmek için kullanılan sınıftır.
- “var key = Encoding.ASCII.GetBytes(_vbtConfig.Value.PrivateKey)” : Doğrulama için kullanılan Secret Key, üretilme anında kullanılan key ile aynı olmak zorundadır. Validate için, “byte[ ]” dizisine çevrilir.
- “tokenHandler.ValidateToken(mobileToken, new TokenValidationParameters” : Sorgulanacak token, secret key ile doğrulanır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public bool ValidateJwtToken(string mobileToken) { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_vbtConfig.Value.PrivateKey); try { tokenHandler.ValidateToken(mobileToken, new TokenValidationParameters { ValidateIssuerSigningKey = true, ValidateIssuer = false, ValidateAudience = false, IssuerSigningKey = new SymmetricSecurityKey(key), }, out SecurityToken validatedToken); } catch { return false; } return true; } |
CONTROLLER:
Şimdi sıra geldi, Mobile için servis verilecek methodların işaretlenmesine.
Infrastructure/MobileTokenAttribute: Aşağıda görüldüğü gibi, mobileden çağrılacak methodlar “MobileTokenAttribute“‘ü ile işaretlenmişlerdir. Böylece işaretli Actionlar, yukarıda tanımlanan Token kontrolüne sokulur.
1 2 3 4 5 6 7 8 9 |
using System; namespace TemplateProject.Infrastructure { [AttributeUsage(AttributeTargets.Method)] public class MobileTokenAttribute: Attribute { } } |
HomeCotroller/GetMobileCustomer(): GetMobileCustomer Action’ı, aşağıda görüldüğü gibi “MobileTokenAttribute” attribute’ü ile işaretlendiği için, JWT ile Token kontrolüne dahil olur.
- “[ServiceFilter(typeof(LoginFilter))]” : Makalenin devamında anlatılacak olan, “GetMobileCustomer()” Action’ına girmeden önce, MiddleWare’de çağrılan sınıftır.
- “public ServiceResponse<CustomerListModel> GetMobileCustomer()” : Mobile için, müşteri listesinin çekildiği methoddur.
- “var response = new ServiceResponse<CustomerListModel>(HttpContext)” : Katmanlı mimaride dönüş tipi, makalenin devamında anlatılacak olan ServiceResponse’dur. Ayrıca “CustomerListModel“, makalenin devamında anlatılacaktır.
- “response.List = _customerService.SearchCustomer(“”, 0, 10).List.ToList()” : Yine makalenin devamında anlatılacak olan, customerService’den ilk 10 müşteri kaydı alınır. Geri dönüş tipi bir liste olduğu için, ServiceResponse modelinin, “List” property’sine atanır.
- “response.Count = response.List.Count“: Paging için geri dönülen toplam kayıt sayısı, Count property’sine atanmıştır.
- “public IActionResult GetMobileToken(string deviceID)” : Örnek amaçlı, gerçek hayatta yayımlanmaması gereken bir methoddur. Bu örnekde, mobile yerine test amaçlı swagger’dan Jwt Token yaratılması için oluşturulmuş bir methoddur.
- “[Infrastructure.IgnoreAttribute]” : GetMobileCustomer Action’ının işaretlenmesi, LoginFilter Action Filter’a takılmaması, yani ignore edilmesi için tanımlanmıştır.
- “var jwtToken = _loginService.GetMobileToken(_deviceID).Entity” : Yukarıda Encryption class’ında tanımlanan “GetJwtToken()” methodunda, loginService’i çağrılarak oluşturulan token, geri dönülür.
- “public IActionResult ValidateJwtToken(string mobileToken)” : Yine dışarı açılmayacak, sadece swagger’da test amacı ile kullanılacak, JwtToken’ın geçerli olup olmadığını belirleyen bir servisdir.
- “var jwtToken = _loginService.ValidateJwtToken(mobileToken).Entity” : Yukarıda Encryption class’ında tanımlanan “ValidateJwtToken()” methodunda ,yine loginService’i çağrılarak ilgili Token’ın geçerliliği doğrulanır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
[ServiceFilter(typeof(LoginFilter))] [ApiController] [Route("[controller]")] public class HomeController : ControllerBase { private readonly ICustomerService _customerService; private readonly ILoginService _loginService; public HomeController(ICustomerService customerService,ILoginService loginService) { _customerService = customerService; _loginService = loginService; } [Infrastructure.MobileTokenAttribute] [HttpGet("GetMobileCustomer")] public ServiceResponse<CustomerListModel> GetMobileCustomer() { var response = new ServiceResponse<CustomerListModel>(HttpContext); response.List = _customerService.SearchCustomer("", 0, 10).List.ToList(); response.Count = response.List.Count; return response; } [Infrastructure.IgnoreAttribute] //LoginLogFilter'a takılmaz. [Route("GetMobileToken/{deviceID}")] [HttpPost] public IActionResult GetMobileToken(string deviceID) { //emp - unqDeviceId:95C9AF20-7474-45E1-89DF-C59164C6E774 string _deviceID = deviceID; var jwtToken = _loginService.GetMobileToken(_deviceID).Entity; return new ObjectResult(jwtToken); } [Infrastructure.IgnoreAttribute] //LoginLogFilter'a takılmaz. [Route("ValidateJwtToken/{mobileToken}")] [HttpPost] public IActionResult ValidateJwtToken(string mobileToken) { var jwtToken = _loginService.ValidateJwtToken(mobileToken).Entity; return new ObjectResult(jwtToken); } } |
MODELLER:
Core / ServiceResponse : Tüm servislerden dönen global model, ServiceResponse’dur. Makale amacı ile alanları, gerçek hayata göre kısıtlı tutulmuştur.
- “_customerService = customerService” : Customer Service, “Dependency Injection” ile sisteme dahil edilmiştir.
- “public bool HasExceptionError { get; set; }” : Serviste bir hata oluştuğunda, ilgili alan atanır.
- “public class ServiceResponse<T> : IServiceResponse<T>” : Generic Tipde, kendisine gönderilen tüm modelleri kapsamaktadır.
- “public IList<T> List { get; set; }” : Geriye dönüş tipi bir liste ise, bu property atanır.
- “public T Entity { get; set; }” : Geriye dönüş tipi tek bir model ise, bu property atanır.
- “public int Count { get; set; }” : Paging için toplam kayıt sayısı, burada tanımlanır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; namespace Core.ApiResponse { [Serializable] public class ServiceResponse<T> : IServiceResponse<T> { public bool HasExceptionError { get; set; } public IList<T> List { get; set; } [JsonProperty] public T Entity { get; set; } public int Count { get; set; } public ServiceResponse(HttpContext context) { List = new List<T>(); } } } |
Core/Models/Customer/CustomerListModel: İstenen müşteri raporu için gerekli olan alanlar, aşağıdaki gibi bir ViewModel’de tanımlanmıştır. Bu, aslında bir çeşit adaptor’dır. Yani database’den dönen tüm alanlara ihtiyaç duymayan sistemin, sadece kendi ihtiyacı olanları aldığı bir yapıdır. Not: AutoMapper kütüphanesi, ilgili alanların otomatik eşleşmesi için kullanılabilecek güzel bir kütüphanedir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
namespace Core.Models { public class CustomerListModel : BaseModel { public string CustomerID { get; set; } public string ProductName { get; set; } public string ShipCountry { get; set; } public decimal UnitPrice { get; set; } public int Quantity { get; set; } public decimal Total { get; set; } public bool IsDeleted { get; set; } public CustomerListModel() { } } } |
SERVİSLER:
Services/Customers/ICustomerService: GetMobileCustomer() Action’ından çağrılacak servisin interface’i aşağıdaki gibidir. Makale amaçlı “SearchCustomer()” adında, tek bir methodu bulunmaktadır.
1 2 3 4 5 6 7 |
namespace Services.Customers { public interface ICustomerService { ServiceResponse<CustomerListModel> SearchCustomer(string name, int pageNo, int pageSize); } } |
Services/Customers/CustomerService: Aranan isme göre , paging yapılarak bulunan müşterilerin kaydının döndürüldüğü “SearchCustomer()” methodu, aşağıdaki gibi tanımlanmıştır.
- “private readonly IRepository<DB.Entities.VwCustomerProducts> _customerProductRepository” : Bu projede, Entity Framework kullanılmıştır. DB tarafında “VwCustomerProducts” adında bir view yaratılmış ve linq query, bu view üzerinden yapılmıştır. Makalenin devamında anlatılacak “Repository” katmanı ile ilgili view sisteme dahil edilmiştir.
- “_customerProductRepository = customerProductRepository”: İlgili View, Dependency Injection ile sayfaya dahil edilmiştir.
- “ServiceResponse<CustomerListModel> SearchCustomer(string name, int pageNo, int pageSize)” Bu servis de geriye, “ServiceResponse<CustomerListModel>” dönmektedir. Parametre olarak, aranacak müşteri ismi, başlangıç sayfası ve çekilecek adet beklemektedir.
- “_customerProductRepository.Table” : Repository katmanındaki Table == DB’deki View’a karşılık gelmektedir. Linq Query, bu view üzerinden çekilmektedir.
- “Where(k => EF.Functions.Like(k.CustomerId ?? string.Empty, $”%{name}%”))” : Performans açışından EF.Functions.Like ve Contains performans anlamında artık, aynıdır. Kulağınıza küpe olsun :) Aranacak isme göre ilgili müşteri kaydı, “Like” ile bakılır.
- “.OrderBy(c => c.CustomerId) .Skip(pageNo * pageSize) .Take(pageSize)” : Müşteri numarasına göre sıralama(OrderBy) ve paging işlemleri Skip() ile atlanacak kayıt sayısı, Take() ile de alınacak toplam kayıt sayısı tanımlanır.
- “var models = query.Select(s => new CustomerListModel()” : Çeklen data, “List<CustomerListModel>“‘e doldurulur. Bu işlem AutoMapper kullanılarak daha pratik bir şekilde yapılabilirdi.
- “response.List = models” : Geri dönülecek data tipi list olduğu için,==> ServiceResponse modelin “List” porperty’sine ilgili model atanmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
namespace Services.Customers { public class CustomerService : ICustomerService { private readonly IRepository<DB.Entities.VwCustomerProducts> _customerProductRepository; public CustomerService(IRepository<DB.Entities.VwCustomerProducts> customerProductRepository) { _customerProductRepository = customerProductRepository; } public ServiceResponse<CustomerListModel> SearchCustomer(string name, int pageNo, int pageSize) { name = string.IsNullOrWhiteSpace(name) ? string.Empty : name.ToLower(CultureInfo.CurrentCulture); var query = _customerProductRepository.Table .Where(k => EF.Functions.Like(k.CustomerId ?? string.Empty, $"%{name}%")) .OrderBy(c => c.CustomerId) .Skip(pageNo * pageSize) .Take(pageSize) .ToList(); var response = new ServiceResponse<CustomerListModel>(null); var models = query.Select(s => new CustomerListModel() { CustomerID = s.CustomerId, ProductName = s.ProductName, Quantity = s.Quantity, ShipCountry = s.ShipCountry, Total = (decimal)s.Total, UnitPrice = s.UnitPrice }).ToList(); response.List = models; return response; } } |
Login/ILoginService: Test amaçlı, token yaratma ve onaylama işleminin yapıldığı servisin interface’i dir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using Core.ApiResponse; using Core.Models.Users; using System; using System.Collections.Generic; using System.Text; namespace Services.Login { public interface ILoginService { ServiceResponse<string> GetMobileToken(string deviceID); ServiceResponse<bool> ValidateJwtToken(string mobileToken); } } |
Login/LoginService: Aşağıda, HomeController’dan çağrılan LoginService üzerindeki GetMobileToken() ile, aslında mobile tarafında yaratılması gereken Token’ın, swagger’dan test amaçlı oluşturulması için yazılmış methodu görülmektedir. Ayrıca yine HomeController’dan çağrılan ValidateJwtToken(), middleware’de mobile tarafından gelen tüm requestlerde gönderilen token’ın geçerliliğini kontrol etmektedir. İlgili token’ın geçerli olması için, backend ve mobile tarafında secretKey’in aynı olması gerekmektedir. Geçerli bir token gelmemesi durumunda, geriye 401 Unauthorized dönülecektir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
namespace Services.Login { public class LoginService : ILoginService { private readonly IEncryption _encryptionService; public LoginService(IEncryption encryptionService) { _encryptionService = encryptionService; } public ServiceResponse<string> GetMobileToken(string deviceID) { string mobileTokenEncrpt = _encryptionService.GetJwtToken(deviceID); var response = new ServiceResponse<string>(null); response.Entity = mobileTokenEncrpt; return response; } public ServiceResponse<bool> ValidateJwtToken(string mobileToken) { bool isValidate = _encryptionService.ValidateJwtToken(mobileToken); var response = new ServiceResponse<bool>(null); response.Entity = isValidate; return response; } } } |
REPOSİTORY:
IRepository: Entity Framework üzerindeki Context’e, Table attribute’ü ile ulaşılmaktadır. Makale için kısaltılmıştır.
1 2 3 4 5 6 7 8 9 10 11 |
namespace Repository { public interface IRepository<T> { . . IQueryable<T> Table { get; } . . } } |
Repository: DBContext üzerinde Insert, Update ve Delete gibi genel işlemlerin yapıldığı bir sınıftır. Bu örnek de sadece DBContext’e erişmek amacı ile, Table property’si tanımlanmıştır. Kısaca Repository katmanı bir projede, Servis ile DB katmanı arasında genel Crud işlemlerinin yapıldığı, tüm temel DB işlemlerini kapsamaktadır. Bu genel işlemler, Servis katmanında ayrıca yazılmamalıdır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
namespace Repository { public class GeneralRepository<T> : IRepository<T> { private readonly DBContext _context; private DbSet<T> _entities; public GeneralRepository(DBtContext context) { _context = context; _entities = context.Set<T>(); } public virtual IQueryable<T> Table => Entities; protected virtual DbSet<T> Entities => _entities ?? (_entities = _context.Set<T>()); } } |
ActionFilter:
Geldik LoginFilter ile .Net Core üzerinde yazılan Custom ActionFilter’a:
Infrastructure/ LoginFilter: Controller veya Action üzerine konan “[ServiceFilter(typeof(LoginFilter))]” işaretlemesi ile ilgili filter’a girilir. Controller üzerine konur ise, altındaki tüm Actionlar için ilgili Filter çalıştırılır. Eğer sadece bir Action üzerine konur ise, sadece ilgili Action için LoginFilter devreye girer.
- “_encryption = encryption” : Dependency Injection ile “GetJwtToken()” ve “ValidateJwtToken()” methodları, ilgili servis üzerinden çağrılır.
- “public void OnActionExecuting(ActionExecutingContext context)” : Bir Action’a girmeden önce çalıştırılan methoddur. Kısaca, kapıdan duran bir bekçidir :)
- if (HasMobileTokenAttribute(context)) : Üzerinde “HasMobileTokenAttribute” işaretlemesi olan Actionlarda, MobileToken kontrolü yapılır.
- “string authHeader = context.HttpContext.Request.Headers[“Authorization”]” : Mobilde oluşturulan JWT Token, Header’dan ==> “Authorization” keyi ile okunur.
- “if (authHeader != null && authHeader.StartsWith(“Bearer”))” : Eğer token yok ise veya “Bearer” kelimesi içinde geçmiyor ise, geriye ==> “401 Unauthorized” sonucu dönülür.
- “var mobileToken = authHeader.Substring(“Bearer “.Length).TrimStart()” : Header’dan gelen JWT Token içinden, “Bearer” kelimesi çıkarılır.
- “if (_encryption.ValidateJwtToken(mobileToken))” : Gönderilen JWT Token’ın geçerli olup olmadığna bakılır.
- “context.Result = new UnauthorizedResult()” : Geçerli değil ise ==> “401 Unauthorized” sonucu dönülür.
- “public bool HasMobileTokenAttribute(FilterContext context)” : Mobile üzerindeki tüm işaretlemeler yani Attributeler gezilerek ==> “MobileTokenAttribute” ile işaretli mi diye bakılır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
public class LoginFilter : IActionFilter { private readonly IEncryption _encryption; public LoginFilter(IEncryption encryption) { _encryption = encryption; } public void OnActionExecuting(ActionExecutingContext context) { try { if (HasMobileTokenAttribute(context)) { string authHeader = context.HttpContext.Request.Headers["Authorization"]; if (authHeader != null && authHeader.StartsWith("Bearer")) { var mobileToken = authHeader.Substring("Bearer ".Length).TrimStart(); if (_encryption.ValidateJwtToken(mobileToken)) { return; } else { context.Result = new UnauthorizedResult(); return; } } else { context.Result = new UnauthorizedResult(); return; } } } catch (InvalidTokenException ex) { context.Result = new UnauthorizedResult(); return; } } public void OnActionExecuted(ActionExecutedContext context) { return; } public bool HasMobileTokenAttribute(FilterContext context) { return ((ControllerActionDescriptor)context.ActionDescriptor) .MethodInfo.CustomAttributes.Any(filterDescriptors => filterDescriptors.AttributeType == typeof(MobileTokenAttribute)); } } |
Bu makalede Authorization işlemleri olmayan yapılarda, kısıtlı da olsa güvenliği nasıl arttırabileceğimizi hep beraber tartıştık. Jwt kullanan projelerde istendiği zaman logout olunamamasından dolayı, benim pek de tercih ettiğim bir kütüphane değildir. Gerçi şimdi Jwt kullanan arkadaşlar, ilgili token’ın Local Storage’dan silinmesi durumunda işin çözüldüğünü düşüneceklerdir ama, sözüm ona silinen bu token’ı önceden ele geçiren bir kişi, Jwt tarafında Expire süresi dolana kadar kullanabilmektedir. Çünkü Jwt, bir token’ın expire süresi dolmadan onu emekli edemez! Ama JWT kütüphanesinin özellikle token üretiminde, farklı platformların birbirleri ile anlaşması durumunda, gayet güzel çalıştığını rahatlıkla söyliyebilirim.
Şimdi makalenin başında da verdiğim örnek de olduğu gibi, aklınıza peki bunu web ortamında nasıl yapabilirdik diye bir soru gelebilir. Yani bir haber portalından, istenen bir habere tıklandığında, javascript tarafında ilgili Jwt Token nasıl üretebilir ? Onu da isterseniz başka bir makalede tartışalım ?
Geldik bir makalenin daha sonuna. Yeni bir makalede görüşmek üzere hepinize hoşçakalın. Sağlıcakla kalın.
Source :
Peki mobile uygulamamızı decompile ederek içerisinden bu secret keyi alamazlar mı? Bu kısmı çok anlayamadım.
Benim bildiğim bir yol yok.
Ama olamaz demek de mümkün değil :)
Hocam Merhaba, Makaledeki uygulamanın kodlarının tamamını Github’a yükleyebilir misiniz?
Selamlar,
Bulabileceğimi sanmıyorum …