ASP.NET Web API için JWT kimlik doğrulaması


264

Web API uygulamamda JWT taşıyıcı jetonunu (JSON Web Token) desteklemeye çalışıyorum ve kayboluyorum.

.NET Core ve OWIN uygulamaları için destek görüyorum.
Şu anda uygulamamı IIS'de barındırıyorum.

Bu kimlik doğrulama modülünü uygulamamda nasıl elde edebilirim? Kullanabileceğim bir yolu var mı <authentication>yönlü I kullanım şekilleri / Windows kimlik doğrulaması benzer yapılandırmayı?

Yanıtlar:


611

Bu soruyu yanıtladım: 4 yıl önce HMAC kullanarak bir ASP.NET Web API'sinin güvenliği .

Şimdi, güvenlikte çok şey değişti, özellikle JWT popüler hale geliyor. Burada, JWT'yi mümkün olan en basit ve temel şekilde nasıl kullanacağımı açıklamaya çalışacağım, bu yüzden OWIN, Oauth2, ASP.NET Identity ormanından kaybolmayacağız ... :).

JWT jetonunu bilmiyorsanız, biraz şunlara bakmanız gerekir:

https://tools.ietf.org/html/rfc7519

Temel olarak, bir JWT jetonu şuna benzer:

<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>

Misal:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ

Bir JWT belirtecinin üç bölümü vardır:

  1. Üstbilgi: Base64 olarak kodlanan JSON biçimi
  2. Talepler: Base64'te kodlanan JSON biçimi.
  3. İmza: Base64'te kodlanan Başlık ve Talepler temel alınarak oluşturulmuş ve imzalanmıştır.

Yukarıdaki jetonla jwt.io web sitesini kullanırsanız , jetonun kodunu çözebilir ve aşağıdaki gibi görebilirsiniz:

resim açıklamasını buraya girin

Teknik olarak JWT, başlıklarda imzalanan ve başlıklarda belirtilen güvenlik algoritmasıyla talep edilen imzayı kullanır (örnek: HMACSHA256). Bu nedenle, taleplerde hassas bilgiler depolarsanız JWT'nin HTTP'ler üzerinden aktarılması gerekir.

Şimdi, JWT kimlik doğrulamasını kullanmak için eski bir Web Api sisteminiz varsa gerçekten bir OWIN ara katman yazılımına ihtiyacınız yoktur. Basit konsept, JWT belirtecinin nasıl sağlanacağı ve istek geldiğinde belirtecin nasıl doğrulanacağıdır. Bu kadar.

Geri demo, JWT belirteç hafif tutmak için, sadece depolamak usernameve expiration timeJWT'de. Ancak bu şekilde, rol yetkilendirmesi yapmak istiyorsanız: roller .. gibi daha fazla bilgi eklemek için yeni yerel kimlik (temel) yeniden oluşturmanız gerekir. Ancak, JWT'ye daha fazla bilgi eklemek istiyorsanız, size kalmış: çok esnektir.

OWIN ara katman yazılımı kullanmak yerine, denetleyiciden gelen eylemi kullanarak bir JWT belirteç uç noktası sağlayabilirsiniz:

public class TokenController : ApiController
{
    // This is naive endpoint for demo, it should use Basic authentication
    // to provide token or POST request
    [AllowAnonymous]
    public string Get(string username, string password)
    {
        if (CheckUser(username, password))
        {
            return JwtManager.GenerateToken(username);
        }

        throw new HttpResponseException(HttpStatusCode.Unauthorized);
    }

    public bool CheckUser(string username, string password)
    {
        // should check in the database
        return true;
    }
}

Bu naif bir eylemdir; üretimde JWT belirteci sağlamak için bir POST isteği veya Temel Kimlik Doğrulama uç noktası kullanmalısınız.

Jeton nasıl oluşturulur username?

Jetonu System.IdentityModel.Tokens.Jwtoluşturmak için Microsoft'tan çağrılan NuGet paketini , hatta isterseniz başka bir paketi kullanabilirsiniz. Demoda aşağıdakilerle HMACSHA256birlikte kullanıyorum SymmetricKey:

/// <summary>
/// Use the below code to generate symmetric Secret Key
///     var hmac = new HMACSHA256();
///     var key = Convert.ToBase64String(hmac.Key);
/// </summary>
private const string Secret = "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==";

public static string GenerateToken(string username, int expireMinutes = 20)
{
    var symmetricKey = Convert.FromBase64String(Secret);
    var tokenHandler = new JwtSecurityTokenHandler();

    var now = DateTime.UtcNow;
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name, username)
        }),

        Expires = now.AddMinutes(Convert.ToInt32(expireMinutes)),

        SigningCredentials = new SigningCredentials(
            new SymmetricSecurityKey(symmetricKey), 
            SecurityAlgorithms.HmacSha256Signature)
    };

    var stoken = tokenHandler.CreateToken(tokenDescriptor);
    var token = tokenHandler.WriteToken(stoken);

    return token;
}

JWT belirtecini sağlamak için uç nokta yapılır. Şimdi, istek geldiğinde JWT nasıl doğrulanır? Demoda JwtAuthenticationAttributehangi miras aldım IAuthenticationFilter( burada kimlik doğrulama filtresi hakkında daha fazla ayrıntı ).

Bu özellik ile, herhangi bir eylemin kimliğini doğrulayabilirsiniz: bu özelliği bu eyleme koymanız yeterlidir.

public class ValueController : ApiController
{
    [JwtAuthentication]
    public string Get()
    {
        return "value";
    }
}

WebAPI'niz için gelen tüm istekleri doğrulamak istiyorsanız OWIN ara katman yazılımını veya DelegateHander'ı da kullanabilirsiniz (Denetleyici veya eyleme özgü değildir)

Kimlik doğrulama filtresinin temel yöntemi aşağıdadır:

private static bool ValidateToken(string token, out string username)
{
    username = null;

    var simplePrinciple = JwtManager.GetPrincipal(token);
    var identity = simplePrinciple.Identity as ClaimsIdentity;

    if (identity == null)
        return false;

    if (!identity.IsAuthenticated)
        return false;

    var usernameClaim = identity.FindFirst(ClaimTypes.Name);
    username = usernameClaim?.Value;

    if (string.IsNullOrEmpty(username))
       return false;

    // More validate to check whether username exists in system

    return true;
}

protected Task<IPrincipal> AuthenticateJwtToken(string token)
{
    string username;

    if (ValidateToken(token, out username))
    {
        // based on username to get more information from database 
        // in order to build local identity
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, username)
            // Add more claims if needed: Roles, ...
        };

        var identity = new ClaimsIdentity(claims, "Jwt");
        IPrincipal user = new ClaimsPrincipal(identity);

        return Task.FromResult(user);
    }

    return Task.FromResult<IPrincipal>(null);
}

İş akışı, JWT jetonunu doğrulamak ve sonra geri dönmek için JWT kitaplığını (yukarıdaki NuGet paketi) kullanmaktır ClaimsPrincipal. Kullanıcının sisteminizde var olup olmadığını kontrol etmek gibi daha fazla doğrulama gerçekleştirebilir ve isterseniz diğer özel doğrulamaları ekleyebilirsiniz. JWT jetonunu doğrulamak ve anapara geri almak için kod:

public static ClaimsPrincipal GetPrincipal(string token)
{
    try
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var jwtToken = tokenHandler.ReadToken(token) as JwtSecurityToken;

        if (jwtToken == null)
            return null;

        var symmetricKey = Convert.FromBase64String(Secret);

        var validationParameters = new TokenValidationParameters()
        {
            RequireExpirationTime = true,
            ValidateIssuer = false,
            ValidateAudience = false,
            IssuerSigningKey = new SymmetricSecurityKey(symmetricKey)
        };

        SecurityToken securityToken;
        var principal = tokenHandler.ValidateToken(token, validationParameters, out securityToken);

        return principal;
    }
    catch (Exception)
    {
        //should write log
        return null;
    }
}

JWT belirteci doğrulanırsa ve asıl iade edilirse, rol yetkilendirmesini denetlemek için yeni bir yerel kimlik oluşturmanız ve bu klasöre daha fazla bilgi girmeniz gerekir.

config.Filters.Add(new AuthorizeAttribute());Kaynaklarınıza anonim bir istek gelmesini önlemek için genel kapsamda (varsayılan yetkilendirme) eklemeyi unutmayın .

Demoyu test etmek için Postacı'yı kullanabilirsiniz:

İstek jetonu (yukarıda belirttiğim gibi saf, sadece demo için):

GET http://localhost:{port}/api/token?username=cuong&password=1

Yetkili istek için başlığa JWT jetonu koyun, örnek:

GET http://localhost:{port}/api/value

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s

Demo buraya yerleştirilir: https://github.com/cuongle/WebApi.Jwt


5
@Cuong Le tarafından iyi açıklandı ama daha fazla eklemek istiyorum: OWIN kullanıyorsanız Microsoft.Owin.Security.Jwt kullanılabilir UseJwtBearerAuthentication kontrol edin, gelen her isteği otomatik olarak doğrulamak için WebAPI bu owin ara katmanı kullanabilirsiniz. ara katman yazılımını kaydetmek için owin başlangıç ​​sınıfını kullanın
Jek

5
@AmirPopovich Yanıt üzerinde belirteç ayarlamanıza gerek yoktur, belirteç istemci tarafında başka bir yerde saklanmalıdır, web için, HTTP isteği gönderdiğinizde, belirteci başlığa koyun.
cuongle

7
Vay canına, uzun zamandır gördüğüm en basit açıklama bu. +100 yapabilirsem
gyozo kudor

4
@Homam: Bu geç cevap için üzgünüm, oluşturmanın en iyi yolu: varhmac = new HMACSHA256();var key = Convert.ToBase64String(hmac.Key);
cuongle

4
CuongLe'in deposundan demo kodunu kullanan herkes, yetkilendirme üstbilgisi olmayan isteklerin ele alınmadığı bir hata olduğunu fark edecektir, bu da herhangi bir sorgunun geçemeyeceği anlamına gelir (bir son nokta güvenli değildir). @Magicleon'dan bu sorunu çözmek için bir istek var: github.com/cuongle/WebApi.Jwt/pull/4
Chucky

11

Minimum çaba ile başarmayı başardım (ASP.NET Core gibi basit).

Bunun için OWIN kullanıyorum Startup.cs dosyası ve Microsoft.Owin.Security.Jwtkütüphane kullanıyorum.

Uygulamanın vurması Startup.csiçin değiştirmemiz gerekiyorWeb.config :

<configuration>
  <appSettings>
    <add key="owin:AutomaticAppStartup" value="true" />
    ...

İşte böyle Startup.cs görünmesi gerektiği :

using MyApp.Helpers;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Jwt;
using Owin;

[assembly: OwinStartup(typeof(MyApp.App_Start.Startup))]

namespace MyApp.App_Start
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseJwtBearerAuthentication(
                new JwtBearerAuthenticationOptions
                {
                    AuthenticationMode = AuthenticationMode.Active,
                    TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidAudience = ConfigHelper.GetAudience(),
                        ValidIssuer = ConfigHelper.GetIssuer(),
                        IssuerSigningKey = ConfigHelper.GetSymmetricSecurityKey(),
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true
                    }
                });
        }
    }
}

Birçoğunuz bugünlerde ASP.NET Core kullanıyorsunuz, bu yüzden gördüğünüz gibi orada sahip olduğumuzdan çok farklı değil.

İlk önce beni şaşırttı, özel sağlayıcılar vb. Uygulamaya çalışıyordum. Ama bunun bu kadar basit olmasını beklemiyordum. OWINsadece kayalar!

Bahsetmemiz gereken bir şey var - OWIN Startup'ı etkinleştirdikten sonra NSWag kütüphanesini için çalışmayı durdurdu (örneğin, bazılarınız Açısal uygulama için otomatik olarak daktilo HTTP proxy'leri oluşturmak isteyebilirsiniz).

Çözüm ayrıca çok basitti - yerime başkasının NSWagileSwashbuckle ve ilgili diğer sorunları yoktu.


Tamam, şimdi ConfigHelperkod paylaşılıyor :

public class ConfigHelper
{
    public static string GetIssuer()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["Issuer"];
        return result;
    }

    public static string GetAudience()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["Audience"];
        return result;
    }

    public static SigningCredentials GetSigningCredentials()
    {
        var result = new SigningCredentials(GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256);
        return result;
    }

    public static string GetSecurityKey()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["SecurityKey"];
        return result;
    }

    public static byte[] GetSymmetricSecurityKeyAsBytes()
    {
        var issuerSigningKey = GetSecurityKey();
        byte[] data = Encoding.UTF8.GetBytes(issuerSigningKey);
        return data;
    }

    public static SymmetricSecurityKey GetSymmetricSecurityKey()
    {
        byte[] data = GetSymmetricSecurityKeyAsBytes();
        var result = new SymmetricSecurityKey(data);
        return result;
    }

    public static string GetCorsOrigins()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["CorsOrigins"];
        return result;
    }
}

Başka bir önemli özellik - JWT Token'ı Yetkilendirme başlığı ile , bu yüzden daktilo kodu beni şöyle arar:

(aşağıdaki kod NSWag tarafından oluşturulur )

@Injectable()
export class TeamsServiceProxy {
    private http: HttpClient;
    private baseUrl: string;
    protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;

    constructor(@Inject(HttpClient) http: HttpClient, @Optional() @Inject(API_BASE_URL) baseUrl?: string) {
        this.http = http;
        this.baseUrl = baseUrl ? baseUrl : "https://localhost:44384";
    }

    add(input: TeamDto | null): Observable<boolean> {
        let url_ = this.baseUrl + "/api/Teams/Add";
        url_ = url_.replace(/[?&]$/, "");

        const content_ = JSON.stringify(input);

        let options_ : any = {
            body: content_,
            observe: "response",
            responseType: "blob",
            headers: new HttpHeaders({
                "Content-Type": "application/json", 
                "Accept": "application/json",
                "Authorization": "Bearer " + localStorage.getItem('token')
            })
        };

Üstbilgiler bölümüne bakın - "Authorization": "Bearer " + localStorage.getItem('token')


I replaced NSWag with Swashbuckle and didn't have any further issues.Swashbuckle daktilo dosyaları oluşturma yeteneğine sahip mi, yoksa buna kendiniz eklediğiniz bir şey mi?
ezmek

@crush swashbucle sadece nuget nswag kütüphanesi gibi daha iyi json sağlayan bir arka uç kütüphanesidir. Typescript dosyası oluşturmak için yine de npm'den nswag paketini kullanmalısınız.
Alex Herman

Doğru, zaten bir süredir projemde paçavra var, nswag yerine TypeScript modellerini üretebileceğini öne sürdüğünü söylüyordum. Ben nswag hayranı değilim ... ağır. Swashbuckle'a bağlanmış kendi C # -> TypeScript dönüşümümü oluşturdum - dosyaları bir post-build işlemi olarak oluşturur ve bunları projelerimiz için bir npm yayınına yayınlar. Sadece aynı şeyi yapan bir Swashbuckle projesini göz ardı etmediğimden emin olmak istedim.
ezmek

8

ASP.NET Core Web API'sinde JWT belirteci kullanarak Talep Tabanlı Kimlik Doğrulamanın çok az ve güvenli bir şekilde uygulanması.

her şeyden önce, bir kullanıcıya atanan hak talepleriyle JWT jetonu döndüren bir bitiş noktasını açmanız gerekir:

 /// <summary>
        /// Login provides API to verify user and returns authentication token.
        /// API Path:  api/account/login
        /// </summary>
        /// <param name="paramUser">Username and Password</param>
        /// <returns>{Token: [Token] }</returns>
        [HttpPost("login")]
        [AllowAnonymous]
        public async Task<IActionResult> Login([FromBody] UserRequestVM paramUser, CancellationToken ct)
        {

            var result = await UserApplication.PasswordSignInAsync(paramUser.Email, paramUser.Password, false, lockoutOnFailure: false);

            if (result.Succeeded)
            {
                UserRequestVM request = new UserRequestVM();
                request.Email = paramUser.Email;


                ApplicationUser UserDetails = await this.GetUserByEmail(request);
                List<ApplicationClaim> UserClaims = await this.ClaimApplication.GetListByUser(UserDetails);

                var Claims = new ClaimsIdentity(new Claim[]
                                {
                                    new Claim(JwtRegisteredClaimNames.Sub, paramUser.Email.ToString()),
                                    new Claim(UserId, UserDetails.UserId.ToString())
                                });


                //Adding UserClaims to JWT claims
                foreach (var item in UserClaims)
                {
                    Claims.AddClaim(new Claim(item.ClaimCode, string.Empty));
                }

                var tokenHandler = new JwtSecurityTokenHandler();
                  // this information will be retrived from you Configuration
                //I have injected Configuration provider service into my controller
                var encryptionkey = Configuration["Jwt:Encryptionkey"];
                var key = Encoding.ASCII.GetBytes(encryptionkey);
                var tokenDescriptor = new SecurityTokenDescriptor
                {
                    Issuer = Configuration["Jwt:Issuer"],
                    Subject = Claims,

                // this information will be retrived from you Configuration
                //I have injected Configuration provider service into my controller
                    Expires = DateTime.UtcNow.AddMinutes(Convert.ToDouble(Configuration["Jwt:ExpiryTimeInMinutes"])),

                    //algorithm to sign the token
                    SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)

                };

                var token = tokenHandler.CreateToken(tokenDescriptor);
                var tokenString = tokenHandler.WriteToken(token);

                return Ok(new
                {
                    token = tokenString
                });
            }

            return BadRequest("Wrong Username or password");
        }

şimdi, JWT kimlik doğrulamasını aşağıdaki gibi varsayılan kimlik doğrulama hizmetiniz olarak eklemek ConfigureServicesiçin startup.cs içindeki hizmetlerinize Kimlik Doğrulaması eklemeniz gerekir:

services.AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
             .AddJwtBearer(cfg =>
             {
                 cfg.RequireHttpsMetadata = false;
                 cfg.SaveToken = true;
                 cfg.TokenValidationParameters = new TokenValidationParameters()
                 {
                     //ValidateIssuerSigningKey = true,
                     IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Encryptionkey"])),
                     ValidateAudience = false,
                     ValidateLifetime = true,
                     ValidIssuer = configuration["Jwt:Issuer"],
                     //ValidAudience = Configuration["Jwt:Audience"],
                     //IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Key"])),
                 };
             });

artık yetkilendirme hizmetlerinize aşağıdaki gibi politikalar ekleyebilirsiniz:

services.AddAuthorization(options =>
            {
                options.AddPolicy("YourPolicyNameHere",
                                policy => policy.RequireClaim("YourClaimNameHere"));
            });

ALTERNATİF OLARAK , tüm taleplerinizi veritabanınızdan da doldurabilirsiniz (gerekli değildir);

  services.AddAuthorization(async options =>
            {
                var ClaimList = await claimApplication.GetList(applicationClaim);
                foreach (var item in ClaimList)
                {                        
                    options.AddPolicy(item.ClaimCode, policy => policy.RequireClaim(item.ClaimCode));                       
                }
            });

şimdi Politika filtresini şu şekilde yetkilendirilmesini istediğiniz yöntemlerden herhangi birine ekleyebilirsiniz:

 [HttpPost("update")]
        [Authorize(Policy = "ACC_UP")]
        public async Task<IActionResult> Update([FromBody] UserRequestVM requestVm, CancellationToken ct)
        {
//your logic goes here
}

Bu yardımcı olur umarım


3

JWT jetonunu desteklemek için bazı 3d parti sunucuları kullanmanız gerektiğini düşünüyorum ve WEB API 2'de kutudan JWT desteği yok.

Ancak imzalı jetonun bazı formatlarını desteklemek için bir OWIN projesi vardır (JWT değil). Bir web sitesi için yalnızca basit bir kimlik doğrulama biçimi sağlamak üzere azaltılmış bir OAuth protokolü olarak çalışır.

Bununla ilgili daha fazla bilgi edinebilirsiniz, örneğin burada .

Oldukça uzun, ancak çoğu parça, hiç ihtiyacınız olmayabilecek denetleyiciler ve ASP.NET Kimliği ile ilgili ayrıntılardır. En önemlileri

9. Adım: OAuth Taşıyıcı Jetonları Oluşturma desteği ekleyin

Adım 12: Arka Uç API'sını Test Etme

Burada, ön uçtan erişebileceğiniz uç noktanın (örn. "/ Token") nasıl ayarlanacağını (ve isteğin biçimiyle ilgili ayrıntıları) okuyabilirsiniz.

Diğer adımlar, bu uç noktanın veritabanına vb. Nasıl bağlanacağına dair ayrıntılar sağlar ve istediğiniz parçaları seçebilirsiniz.


2

Benim durumumda JWT ayrı bir API tarafından oluşturulur, böylece ASP.NET'in yalnızca kodunu çözmesi ve doğrulaması gerekir. Kabul edilen cevabın aksine, simetrik olmayan bir algoritma olan RSA kullanıyoruz.SymmetricSecurityKey yukarıda belirtilen sınıf çalışmaz.

İşte sonuç.

using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Threading;
using System.Threading.Tasks;

    public static async Task<JwtSecurityToken> VerifyAndDecodeJwt(string accessToken)
    {
        try
        {
            var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{securityApiOrigin}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
            var openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);
            var validationParameters = new TokenValidationParameters()
            {
                ValidateLifetime = true,
                ValidateAudience = false,
                ValidateIssuer = false,
                RequireSignedTokens = true,
                IssuerSigningKeys = openIdConfig.SigningKeys,
            };
            new JwtSecurityTokenHandler().ValidateToken(accessToken, validationParameters, out var validToken);
            // threw on invalid, so...
            return validToken as JwtSecurityToken;
        }
        catch (Exception ex)
        {
            logger.Info(ex.Message);
            return null;
        }
    }
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.