مطالب
اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
AuthenticationMiddleware در ASP.NET Core 2.0، فقط مختص به کار با کوکی‌ها جهت اعتبارسنجی کاربران نیست. از این میان‌افزار می‌توان برای اعتبار سنجی‌های مبتنی بر JSON Web Tokens نیز استفاده کرد. مطلبی را که در ادامه مطالعه خواهید کرد دقیقا بر اساس نکات مطلب «پیاده سازی JSON Web Token با ASP.NET Web API 2.x» تدارک دیده شده‌است و به همراه نکاتی مانند تولید Refresh Tokens و یا غیرمعتبر سازی توکن‌ها نیز هست. همچنین ساختار جداول کاربران و نقش‌های آن‌ها، سرویس‌های مرتبط و قسمت تنظیمات Context آن با مطلب «اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» یکی است. در اینجا بیشتر به تفاوت‌های پیاده سازی این روش نسبت به حالت اعتبارسنجی مبتنی بر کوکی‌ها خواهیم پرداخت.
همچنین باید درنظر داشت، ASP.NET Core Identity یک سیستم اعتبارسنجی مبتنی بر کوکی‌ها است. دقیقا زمانیکه کار AddIdentity را انجام می‌دهیم، در پشت صحنه همان  services.AddAuthentication().AddCookie قسمت قبل فراخوانی می‌شود. بنابراین بکارگیری آن با JSON Web Tokens هرچند مشکلی را به همراه ندارد و می‌توان یک سیستم اعتبارسنجی «دوگانه» را نیز در اینجا داشت، اما ... سربار اضافی تولید کوکی‌ها را نیز به همراه دارد؛ هرچند برای کار با میان‌افزار اعتبارسنجی، الزامی به استفاده‌ی از ASP.NET Core Identity نیست و عموما اگر از آن به همراه JWT استفاده می‌کنند، بیشتر به دنبال پیاده سازی‌های پیش‌فرض مدیریت کاربران و نقش‌های آن هستند و نه قسمت تولید کوکی‌های آن. البته در مطلب جاری این موارد را نیز همانند مطلب اعتبارسنجی مبتنی بر کوکی‌ها، خودمان مدیریت خواهیم کرد و در نهایت سیستم تهیه شده، هیچ نوع کوکی را تولید و یا مدیریت نمی‌کند.



تنظیمات آغازین برنامه جهت فعالسازی اعتبارسنجی مبتنی بر JSON Web Tokens

اولین تفاوت پیاده سازی یک سیستم اعتبارسنجی مبتنی بر JWT، با روش مبتنی بر کوکی‌ها، تنظیمات متد ConfigureServices فایل آغازین برنامه است:
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<BearerTokensOptions>(options => Configuration.GetSection("BearerTokens").Bind(options));

            services
                .AddAuthentication(options =>
                {
                    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(cfg =>
                {
                    cfg.RequireHttpsMetadata = false;
                    cfg.SaveToken = true;
                    cfg.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidIssuer = Configuration["BearerTokens:Issuer"],
                        ValidAudience = Configuration["BearerTokens:Audience"],
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["BearerTokens:Key"])),
                        ValidateIssuerSigningKey = true,
                        ValidateLifetime = true,
                        ClockSkew = TimeSpan.Zero
                    };
                    cfg.Events = new JwtBearerEvents
                    {
                        OnAuthenticationFailed = context =>
                        {
                            var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(JwtBearerEvents));
                            logger.LogError("Authentication failed.", context.Exception);
                            return Task.CompletedTask;
                        },
                        OnTokenValidated = context =>
                        {
                            var tokenValidatorService = context.HttpContext.RequestServices.GetRequiredService<ITokenValidatorService>();
                            return tokenValidatorService.ValidateAsync(context);
                        },
                        OnMessageReceived = context =>
                         {
                             return Task.CompletedTask;
                         },
                        OnChallenge = context =>
                        {
                            var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(JwtBearerEvents));
                            logger.LogError("OnChallenge error", context.Error, context.ErrorDescription);
                            return Task.CompletedTask;
                        }
                    };
                });
در اینجا در ابتدا تنظیمات JWT فایل appsettings.json
{
  "BearerTokens": {
    "Key": "This is my shared key, not so secret, secret!",
    "Issuer": "http://localhost/",
    "Audience": "Any",
    "AccessTokenExpirationMinutes": 2,
    "RefreshTokenExpirationMinutes": 60
  }
}
به کلاسی دقیقا با همین ساختار به نام BearerTokensOptions، نگاشت شده‌اند. به این ترتیب می‌توان با تزریق اینترفیس <IOptionsSnapshot<BearerTokensOptions در قسمت‌های مختلف برنامه، به این تنظیمات مانند کلید رمزنگاری، مشخصات صادر کننده، مخاطبین و طول عمرهای توکن‌های صادر شده، دسترسی یافت.

سپس کار فراخوانی  services.AddAuthentication صورت گرفته‌است. تفاوت این مورد با حالت اعتبارسنجی مبتنی بر کوکی‌ها، ثوابتی است که با JwtBearerDefaults شروع می‌شوند. در حالت استفاده‌ی از کوکی‌ها، این ثوابت بر اساس CookieAuthenticationDefaults تنظیم خواهند شد.
البته می‌توان متد AddAuthentication را بدون هیچگونه پارامتری نیز فراخوانی کرد. این حالت برای اعتبارسنجی‌های دوگانه مفید است. برای مثال زمانیکه پس از AddAuthentication هم AddJwtBearer را ذکر کرده‌اید و هم AddCookie اضافه شده‌است. اگر چنین کاری را انجام دادید، اینبار باید درحین تعریف فیلتر Authorize، دقیقا مشخص کنید که حالت مبتنی بر JWT مدنظر شما است، یا حالت مبتنی بر کوکی‌ها:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
اگر متد AddAuthentication، مانند تنظیمات فوق به همراه این تنظیمات پیش‌فرض بود، دیگر نیازی به ذکر صریح AuthenticationSchemes در فیلتر Authorize نخواهد بود.


بررسی تنظیمات متد AddJwtBearer

در کدهای فوق، تنظیمات متد AddJwtBearer یک چنین مفاهیمی را به همراه دارند:
- تنظیم SaveToken به true، به این معنا است که می‌توان به توکن دریافتی از سمت کاربر، توسط متد HttpContext.GetTokenAsync در کنترلرهای برنامه دسترسی یافت.
در قسمت تنظیمات TokenValidationParameters آن:
- کار خواندن فایل appsettings.json برنامه جهت تنظیم صادر کننده و مخاطبین توکن انجام می‌شود. سپس IssuerSigningKey به یک کلید رمزنگاری متقارن تنظیم خواهد شد. این کلید نیز در تنظیمات برنامه قید می‌شود.
- تنظیم ValidateIssuerSigningKey به true سبب خواهد شد تا میان‌افزار اعتبارسنجی، بررسی کند که آیا توکن دریافتی از سمت کاربر توسط برنامه‌ی ما امضاء شده‌است یا خیر؟
- تنظیم ValidateLifetime به معنای بررسی خودکار طول عمر توکن دریافتی از سمت کاربر است. اگر توکن منقضی شده باشد، اعتبارسنجی به صورت خودکار خاتمه خواهد یافت.
- ClockSkew به معنای تنظیم یک تلرانس و حد تحمل مدت زمان منقضی شدن توکن در حالت ValidateLifetime است. در اینجا به صفر تنظیم شده‌است.

سپس به قسمت JwtBearerEvents می‌رسیم:
- OnAuthenticationFailed زمانی فراخوانی می‌شود که اعتبارسنج‌های تنظیمی فوق، با شکست مواجه شوند. برای مثال طول عمر توکن منقضی شده باشد و یا توسط ما امضاء نشده‌باشد. در اینجا می‌توان به این خطاها دسترسی یافت و درصورت نیاز آن‌ها را لاگ کرد.
- OnChallenge نیز یک سری دیگر از خطاهای اعتبارسنجی را پیش از ارسال آن‌ها به فراخوان در اختیار ما قرار می‌دهد.
- OnMessageReceived برای حالتی است که توکن دریافتی، توسط هدر مخصوص Bearer به سمت سرور ارسال نمی‌شود. عموما هدر ارسالی به سمت سرور یک چنین شکلی را دارد:
$.ajax({
     headers: { 'Authorization': 'Bearer ' + jwtToken },
اما اگر توکن شما به این شکل استاندارد دریافت نمی‌شود، می‌توان در رخ‌داد OnMessageReceived به اطلاعات درخواست جاری دسترسی یافت، توکن را از آن استخراج کرد و سپس آن‌را به خاصیت context.Token انتساب داد، تا به عنوان توکن اصلی مورد استفاده قرار گیرد. برای مثال:
const string tokenKey = "my.custom.jwt.token.key";
if (context.HttpContext.Items.ContainsKey(tokenKey))
{
    context.Token = (string)context.HttpContext.Items[tokenKey];
}
 - OnTokenValidated پس از کامل شدن اعتبارسنجی توکن دریافتی از سمت کاربر فراخوانی می‌شود. در اینجا اگر متد context.Fail را فراخوانی کنیم، این توکن، به عنوان یک توکن غیرمعتبر علامتگذاری می‌شود و عملیات اعتبارسنجی با شکست خاتمه خواهد یافت. بنابراین می‌توان از آن دقیقا مانند CookieValidatorService قسمت قبل که جهت واکنش نشان دادن به تغییرات اطلاعات کاربر در سمت سرور مورد استفاده قرار دادیم، در اینجا نیز یک چنین منطقی را پیاده سازی کنیم.


تهیه یک اعتبارسنج توکن سفارشی

قسمت OnTokenValidated تنظیمات ابتدای برنامه به این صورت مقدار دهی شده‌است:
OnTokenValidated = context =>
{
      var tokenValidatorService = context.HttpContext.RequestServices.GetRequiredService<ITokenValidatorService>();
      return tokenValidatorService.ValidateAsync(context);
},
TokenValidatorService سفارشی ما چنین پیاده سازی را دارد:
    public class TokenValidatorService : ITokenValidatorService
    {
        private readonly IUsersService _usersService;
        private readonly ITokenStoreService _tokenStoreService;

        public TokenValidatorService(IUsersService usersService, ITokenStoreService tokenStoreService)
        {
            _usersService = usersService;
            _usersService.CheckArgumentIsNull(nameof(usersService));

            _tokenStoreService = tokenStoreService;
            _tokenStoreService.CheckArgumentIsNull(nameof(_tokenStoreService));
        }

        public async Task ValidateAsync(TokenValidatedContext context)
        {
            var userPrincipal = context.Principal;

            var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
            if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
            {
                context.Fail("This is not our issued token. It has no claims.");
                return;
            }

            var serialNumberClaim = claimsIdentity.FindFirst(ClaimTypes.SerialNumber);
            if (serialNumberClaim == null)
            {
                context.Fail("This is not our issued token. It has no serial.");
                return;
            }

            var userIdString = claimsIdentity.FindFirst(ClaimTypes.UserData).Value;
            if (!int.TryParse(userIdString, out int userId))
            {
                context.Fail("This is not our issued token. It has no user-id.");
                return;
            }

            var user = await _usersService.FindUserAsync(userId).ConfigureAwait(false);
            if (user == null || user.SerialNumber != serialNumberClaim.Value || !user.IsActive)
            {
                // user has changed his/her password/roles/stat/IsActive
                context.Fail("This token is expired. Please login again.");
            }

            var accessToken = context.SecurityToken as JwtSecurityToken;
            if (accessToken == null || string.IsNullOrWhiteSpace(accessToken.RawData) ||
                !await _tokenStoreService.IsValidTokenAsync(accessToken.RawData, userId).ConfigureAwait(false))
            {
                context.Fail("This token is not in our database.");
                return;
            }

            await _usersService.UpdateUserLastActivityDateAsync(userId).ConfigureAwait(false);
        }
    }
در اینجا بررسی می‌کنیم:
- آیا توکن دریافتی به همراه Claims تنظیم شده‌ی درحین لاگین هست یا خیر؟
- آیا توکن دریافتی دارای یک Claim سفارشی به نام SerialNumber است؟ این SerialNumber معادل چنین فیلدی در جدول کاربران است.
- آیا توکن دریافتی دارای user-id است؟
- آیا کاربر یافت شده‌ی بر اساس این user-id هنوز فعال است و یا اطلاعات او تغییر نکرده‌است؟
- همچنین در آخر کار بررسی می‌کنیم که آیا اصل توکن دریافتی، در بانک اطلاعاتی ما پیشتر ثبت شده‌است یا خیر؟

اگر خیر، بلافاصله متد context.Fail فراخوانی شده و کار اعتبارسنجی را با اعلام شکست آن، به پایان می‌رسانیم.

در قسمت آخر، نیاز است اطلاعات توکن‌های صادر شده را ذخیره کنیم. به همین جهت نسبت به مطلب قبلی، جدول UserToken ذیل به برنامه اضافه شده‌است:
    public class UserToken
    {
        public int Id { get; set; }

        public string AccessTokenHash { get; set; }

        public DateTimeOffset AccessTokenExpiresDateTime { get; set; }

        public string RefreshTokenIdHash { get; set; }

        public DateTimeOffset RefreshTokenExpiresDateTime { get; set; }

        public int UserId { get; set; } // one-to-one association
        public virtual User User { get; set; }
    }
در اینجا هش‌های توکن‌های صادر شده‌ی توسط برنامه و طول عمر آن‌ها را ذخیره خواهیم کرد.
از اطلاعات آن در دو قسمت TokenValidatorService فوق و همچنین قسمت logout برنامه استفاده می‌کنیم. در سیستم JWT، مفهوم logout سمت سرور وجود خارجی ندارد. اما با ذخیره سازی هش توکن‌ها در بانک اطلاعاتی می‌توان لیستی از توکن‌های صادر شده‌ی توسط برنامه را تدارک دید. سپس در حین logout فقط کافی است tokenهای یک کاربر را حذف کرد. همینقدر سبب خواهد شد تا قسمت آخر TokenValidatorService با شکست مواجه شود؛ چون توکن ارسالی به سمت سرور دیگر در بانک اطلاعاتی وجود ندارد.


سرویس TokenStore

    public interface ITokenStoreService
    {
        Task AddUserTokenAsync(UserToken userToken);
        Task AddUserTokenAsync(
                User user, string refreshToken, string accessToken,
                DateTimeOffset refreshTokenExpiresDateTime, DateTimeOffset accessTokenExpiresDateTime);
        Task<bool> IsValidTokenAsync(string accessToken, int userId);
        Task DeleteExpiredTokensAsync();
        Task<UserToken> FindTokenAsync(string refreshToken);
        Task DeleteTokenAsync(string refreshToken);
        Task InvalidateUserTokensAsync(int userId);
        Task<(string accessToken, string refreshToken)> CreateJwtTokens(User user);
    }
در قسمت آخر اعتبارسنج سفارشی توکن، بررسی وجود توکن دریافتی، توسط سرویس TokenStore فوق صورت می‌گیرد. از این سرویس برای تولید، ذخیره سازی و حذف توکن‌ها استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.


تولید Access Tokens و Refresh Tokens

پس از تنظیمات ابتدایی برنامه، اکنون می‌توانیم دو نوع توکن را تولید کنیم:

تولید Access Tokens
        private async Task<string> createAccessTokenAsync(User user, DateTime expires)
        {
            var claims = new List<Claim>
            {
                // Unique Id for all Jwt tokes
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                // Issuer
                new Claim(JwtRegisteredClaimNames.Iss, _configuration.Value.Issuer),
                // Issued at
                new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToUnixEpochDate().ToString(), ClaimValueTypes.Integer64),
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.Username),
                new Claim("DisplayName", user.DisplayName),
                // to invalidate the cookie
                new Claim(ClaimTypes.SerialNumber, user.SerialNumber),
                // custom data
                new Claim(ClaimTypes.UserData, user.Id.ToString())
            };

            // add roles
            var roles = await _rolesService.FindUserRolesAsync(user.Id).ConfigureAwait(false);
            foreach (var role in roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role.Name));
            }

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.Value.Key));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken(
                issuer: _configuration.Value.Issuer,
                audience: _configuration.Value.Audience,
                claims: claims,
                notBefore: DateTime.UtcNow,
                expires: expires,
                signingCredentials: creds);
            return new JwtSecurityTokenHandler().WriteToken(token);
        }
این امکانات در اسمبلی زیر قرار دارند:
<ItemGroup>
   <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.0.0" />
</ItemGroup>
در اینجا ابتدا همانند کار با سیستم اعتبارسنجی مبتنی بر کوکی‌ها، نیاز است یک سری Claim تهیه شوند. به همین جهت SerialNumber، UserId و همچنین نقش‌های کاربر لاگین شده‌ی به سیستم را در اینجا به مجموعه‌ی Claims اضافه می‌کنیم. وجود این Claims است که سبب می‌شود فیلتر Authorize بتواند نقش‌ها را تشخیص داده و یا کاربر را اعتبارسنجی کند.
پس از تهیه‌ی Claims، اینبار بجای یک کوکی، یک JSON Web Toekn را توسط متد new JwtSecurityTokenHandler().WriteToken تهیه خواهیم کرد. این توکن حاوی Claims، به همراه اطلاعات طول عمر و امضای مرتبطی است.
حاصل آن نیز یک رشته‌است که دقیقا به همین فرمت به سمت کلاینت ارسال خواهد شد. البته ما در اینجا دو نوع توکن را به سمت کلاینت ارسال می‌کنیم:
        public async Task<(string accessToken, string refreshToken)> CreateJwtTokens(User user)
        {
            var now = DateTimeOffset.UtcNow;
            var accessTokenExpiresDateTime = now.AddMinutes(_configuration.Value.AccessTokenExpirationMinutes);
            var refreshTokenExpiresDateTime = now.AddMinutes(_configuration.Value.RefreshTokenExpirationMinutes);
            var accessToken = await createAccessTokenAsync(user, accessTokenExpiresDateTime.UtcDateTime).ConfigureAwait(false);
            var refreshToken = Guid.NewGuid().ToString().Replace("-", "");

            await AddUserTokenAsync(user, refreshToken, accessToken, refreshTokenExpiresDateTime, accessTokenExpiresDateTime).ConfigureAwait(false);
            await _uow.SaveChangesAsync().ConfigureAwait(false);

            return (accessToken, refreshToken);
        }
accessToken همان JSON Web Token اصلی است. refreshToken فقط یک Guid است. کار آن ساده سازی و به روز رسانی عملیات Login بدون ارائه‌ی نام کاربری و کلمه‌ی عبور است. به همین جهت است که نیاز داریم تا این اطلاعات را در سمت بانک اطلاعاتی برنامه نیز ذخیره کنیم. فرآیند اعتبارسنجی یک refreshToken بدون ذخیره سازی این Guid در بانک اطلاعاتی مسیر نیست که در اینجا در فیلد RefreshTokenIdHash جدول UserToken ذخیره می‌شود.
جهت بالا رفتن امنیت سیستم، این Guid را هش کرد و سپس این هش را در بانک اطلاعاتی ذخیره می‌کنیم. به این ترتیب دسترسی غیرمجاز به این هش‌ها، امکان بازیابی توکن‌های اصلی را غیرممکن می‌کند.


پیاده سازی Login

پس از پیاده سازی متد CreateJwtTokens، کار ورود به سیستم به سادگی ذیل خواهد بود:
        [AllowAnonymous]
        [HttpPost("[action]")]
        public async Task<IActionResult> Login([FromBody]  User loginUser)
        {
            if (loginUser == null)
            {
                return BadRequest("user is not set.");
            }

            var user = await _usersService.FindUserAsync(loginUser.Username, loginUser.Password).ConfigureAwait(false);
            if (user == null || !user.IsActive)
            {
                return Unauthorized();
            }

            var (accessToken, refreshToken) = await _tokenStoreService.CreateJwtTokens(user).ConfigureAwait(false);
            return Ok(new { access_token = accessToken, refresh_token = refreshToken });
        }
ابتدا بررسی می‌شود که آیا کلمه‌ی عبور و نام کاربری وارد شده صحیح هستند یا خیر و آیا کاربر متناظر با آن هنوز فعال است. اگر بله، دو توکن دسترسی و به روز رسانی را تولید و به سمت کلاینت ارسال می‌کنیم.


پیاده سازی Refresh Token

پیاده سازی توکن به روز رسانی همانند عملیات لاگین است:
        [AllowAnonymous]
        [HttpPost("[action]")]
        public async Task<IActionResult> RefreshToken([FromBody]JToken jsonBody)
        {
            var refreshToken = jsonBody.Value<string>("refreshToken");
            if (string.IsNullOrWhiteSpace(refreshToken))
            {
                return BadRequest("refreshToken is not set.");
            }

            var token = await _tokenStoreService.FindTokenAsync(refreshToken);
            if (token == null)
            {
                return Unauthorized();
            }

            var (accessToken, newRefreshToken) = await _tokenStoreService.CreateJwtTokens(token.User).ConfigureAwait(false);
            return Ok(new { access_token = accessToken, refresh_token = newRefreshToken });
        }
با این تفاوت که در اینجا فقط یک Guid از سمت کاربر دریافت شده، سپس بر اساس این Guid، توکن و کاربر متناظر با آن یافت می‌شوند. سپس یک توکن جدید را بر اساس این اطلاعات تولید کرده و به سمت کاربر ارسال می‌کنیم.


پیاده سازی Logout

در سیستم‌های مبتنی بر JWT، پیاده سازی Logout سمت سرور بی‌مفهوم است؛ از این جهت که تا زمان انقضای یک توکن می‌توان از آن توکن جهت ورود به سیستم و دسترسی به منابع آن استفاده کرد. بنابراین تنها راه پیاده سازی Logout، ذخیره سازی توکن‌ها در بانک اطلاعاتی و سپس حذف آن‌ها در حین خروج از سیستم است. به این ترتیب اعتبارسنج سفارشی توکن‌ها، از استفاده‌ی مجدد از توکنی که هنوز هم معتبر است و منقضی نشده‌است، جلوگیری خواهد کرد:
        [AllowAnonymous]
        [HttpGet("[action]"), HttpPost("[action]")]
        public async Task<bool> Logout()
        {
            var claimsIdentity = this.User.Identity as ClaimsIdentity;
            var userIdValue = claimsIdentity.FindFirst(ClaimTypes.UserData)?.Value;

            // The Jwt implementation does not support "revoke OAuth token" (logout) by design.
            // Delete the user's tokens from the database (revoke its bearer token)
            if (!string.IsNullOrWhiteSpace(userIdValue) && int.TryParse(userIdValue, out int userId))
            {
                await _tokenStoreService.InvalidateUserTokensAsync(userId).ConfigureAwait(false);
            }
            await _tokenStoreService.DeleteExpiredTokensAsync().ConfigureAwait(false);
            await _uow.SaveChangesAsync().ConfigureAwait(false);

            return true;
        }


آزمایش نهایی برنامه

در فایل index.html، نمونه‌ای از متدهای لاگین، خروج و فراخوانی اکشن متدهای محافظت شده را مشاهده می‌کنید. این روش برای برنامه‌های تک صفحه‌ای وب یا SPA نیز می‌تواند مفید باشد و به همین نحو کار می‌کنند.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
اشتراک‌ها
9 نکته برای نوشتن برنامه های امن asp.net mvc

Security is one of the most important aspects of any application – and when we talk about security, particularly in ASP.NET applications, it is not limited to development. A secure app involves multiple layers of security in the configuration, framework, web server, database server, and more. In this post, we’ll take a look at the top nine tips for writing secure applications in ASP.NET. 

9 نکته برای نوشتن برنامه های امن asp.net mvc
اشتراک‌ها
سری آموزش Vue.js از مایکروسافت
Vue.js is a progressive front-end JavaScript framework designed to allow you to add dynamic capabilities to your web pages. The addition of a script tag allows you to utilize the functionality in existing applications, or you can build completely from scratch using the framework. It's designed to grow with you and allow you to quickly create rich pages. 
سری آموزش Vue.js از مایکروسافت
نظرات مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت اول - معرفی و ایجاد ساختار برنامه
یک نکته‌ی تکمیلی
اگر NET Core 3 SDK. را نصب کنید، امکان ایجاد یک برنامه‌ی Angular مبتنی بر Microsoft.AspNetCore.ApiAuthorization.IdentityServer به قالب‌های پیش‌فرض آن اضافه شده‌است. روش ایجاد آن توسط دستورات CLI به صورت زیر است:
dotnet new angular --auth Individual

همین مورد برای برنامه‌های React نیز پیش بینی شده‌است:
dotnet new react --auth Individual
//or
dotnet new reactredux --auth Individual