اعتبارسنجی مبتنی بر کوکیها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
نویسنده: وحید نصیری
تاریخ: ۱۳۹۶/۰۶/۰۶ ۱۱:۴۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
public class User
{
public User()
{
UserRoles = new HashSet<UserRole>();
}
public int Id { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string DisplayName { get; set; }
public bool IsActive { get; set; }
public DateTimeOffset? LastLoggedIn { get; set; }
/// <summary>
/// every time the user changes his Password,
/// or an admin changes his Roles or stat/IsActive,
/// create a new `SerialNumber` GUID and store it in the DB.
/// </summary>
public string SerialNumber { get; set; }
public virtual ICollection<UserRole> UserRoles { get; set; }
} public class Role
{
public Role()
{
UserRoles = new HashSet<UserRole>();
}
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<UserRole> UserRoles { get; set; }
} public static class CustomRoles
{
public const string Admin = nameof(Admin);
public const string User = nameof(User);
} public class UserRole
{
public int UserId { get; set; }
public int RoleId { get; set; }
public virtual User User { get; set; }
public virtual Role Role { get; set; }
} public interface IUnitOfWork : IDisposable
{
DbSet<TEntity> Set<TEntity>() where TEntity : class;
int SaveChanges(bool acceptAllChangesOnSuccess);
int SaveChanges();
Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken());
Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken());
}
public class ApplicationDbContext : DbContext, IUnitOfWork
{
public ApplicationDbContext(DbContextOptions options) : base(options)
{ }
public virtual DbSet<User> Users { set; get; }
public virtual DbSet<Role> Roles { set; get; }
public virtual DbSet<UserRole> UserRoles { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
// it should be placed here, otherwise it will rewrite the following settings!
base.OnModelCreating(builder);
// Custom application mappings
builder.Entity<User>(entity =>
{
entity.Property(e => e.Username).HasMaxLength(450).IsRequired();
entity.HasIndex(e => e.Username).IsUnique();
entity.Property(e => e.Password).IsRequired();
entity.Property(e => e.SerialNumber).HasMaxLength(450);
});
builder.Entity<Role>(entity =>
{
entity.Property(e => e.Name).HasMaxLength(450).IsRequired();
entity.HasIndex(e => e.Name).IsUnique();
});
builder.Entity<UserRole>(entity =>
{
entity.HasKey(e => new { e.UserId, e.RoleId });
entity.HasIndex(e => e.UserId);
entity.HasIndex(e => e.RoleId);
entity.Property(e => e.UserId);
entity.Property(e => e.RoleId);
entity.HasOne(d => d.Role).WithMany(p => p.UserRoles).HasForeignKey(d => d.RoleId);
entity.HasOne(d => d.User).WithMany(p => p.UserRoles).HasForeignKey(d => d.UserId);
});
}
} /// <summary>
/// Only used by EF Tooling
/// </summary>
public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
{
public ApplicationDbContext CreateDbContext(string[] args)
{
var basePath = Directory.GetCurrentDirectory();
Console.WriteLine($"Using `{basePath}` as the BasePath");
var configuration = new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json")
.Build();
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
var connectionString = configuration.GetConnectionString("DefaultConnection");
builder.UseSqlServer(connectionString);
return new ApplicationDbContext(builder.Options);
}
} {
"ConnectionStrings": {
"DefaultConnection": "Data Source=(LocalDB)\\MSSQLLocalDB;Initial Catalog=ASPNETCore2CookieAuthenticationDB;Integrated Security=True;MultipleActiveResultSets=True;"
},
"LoginCookieExpirationDays": 30
} public interface IUsersService
{
Task<string> GetSerialNumberAsync(int userId);
Task<User> FindUserAsync(string username, string password);
Task<User> FindUserAsync(int userId);
Task UpdateUserLastActivityDateAsync(int userId);
} public interface IRolesService
{
Task<List<Role>> FindUserRolesAsync(int userId);
Task<bool> IsUserInRole(int userId, string roleName);
Task<List<User>> FindUsersInRoleAsync(string roleName);
} public interface IDbInitializerService
{
void Initialize();
void SeedData();
} public interface ICookieValidatorService
{
Task ValidateAsync(CookieValidatePrincipalContext context);
}
public class CookieValidatorService : ICookieValidatorService
{
private readonly IUsersService _usersService;
public CookieValidatorService(IUsersService usersService)
{
_usersService = usersService;
_usersService.CheckArgumentIsNull(nameof(usersService));
}
public async Task ValidateAsync(CookieValidatePrincipalContext context)
{
var userPrincipal = context.Principal;
var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
{
// this is not our issued cookie
await handleUnauthorizedRequest(context);
return;
}
var serialNumberClaim = claimsIdentity.FindFirst(ClaimTypes.SerialNumber);
if (serialNumberClaim == null)
{
// this is not our issued cookie
await handleUnauthorizedRequest(context);
return;
}
var userIdString = claimsIdentity.FindFirst(ClaimTypes.UserData).Value;
if (!int.TryParse(userIdString, out int userId))
{
// this is not our issued cookie
await handleUnauthorizedRequest(context);
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
await handleUnauthorizedRequest(context);
}
await _usersService.UpdateUserLastActivityDateAsync(userId).ConfigureAwait(false);
}
private Task handleUnauthorizedRequest(CookieValidatePrincipalContext context)
{
context.RejectPrincipal();
return context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
} public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IUnitOfWork, ApplicationDbContext>();
services.AddScoped<IUsersService, UsersService>();
services.AddScoped<IRolesService, RolesService>();
services.AddScoped<ISecurityService, SecurityService>();
services.AddScoped<ICookieValidatorService, CookieValidatorService>();
services.AddScoped<IDbInitializerService, DbInitializerService>(); services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection"),
serverDbContextOptionsBuilder =>
{
var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
serverDbContextOptionsBuilder.CommandTimeout(minutes);
serverDbContextOptionsBuilder.EnableRetryOnFailure();
});
}); // Only needed for custom roles.
services.AddAuthorization(options =>
{
options.AddPolicy(CustomRoles.Admin, policy => policy.RequireRole(CustomRoles.Admin));
options.AddPolicy(CustomRoles.User, policy => policy.RequireRole(CustomRoles.User));
}); // Needed for cookie auth.
services
.AddAuthentication(options =>
{
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.SlidingExpiration = false;
options.LoginPath = "/api/account/login";
options.LogoutPath = "/api/account/logout";
//options.AccessDeniedPath = new PathString("/Home/Forbidden/");
options.Cookie.Name = ".my.app1.cookie";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = context =>
{
var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
return cookieValidatorService.ValidateAsync(context);
}
};
}); public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication(); var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
using (var scope = scopeFactory.CreateScope())
{
var dbInitializer = scope.ServiceProvider.GetService<IDbInitializerService>();
dbInitializer.Initialize();
dbInitializer.SeedData();
} [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)
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Unauthorized();
}
var loginCookieExpirationDays = _configuration.GetValue<int>("LoginCookieExpirationDays", defaultValue: 30);
var cookieClaims = await createCookieClaimsAsync(user).ConfigureAwait(false);
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
cookieClaims,
new AuthenticationProperties
{
IsPersistent = true, // "Remember Me"
IssuedUtc = DateTimeOffset.UtcNow,
ExpiresUtc = DateTimeOffset.UtcNow.AddDays(loginCookieExpirationDays)
});
await _usersService.UpdateUserLastActivityDateAsync(user.Id).ConfigureAwait(false);
return Ok();
} private async Task<ClaimsPrincipal> createCookieClaimsAsync(User user)
{
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
identity.AddClaim(new Claim("DisplayName", user.DisplayName));
// to invalidate the cookie
identity.AddClaim(new Claim(ClaimTypes.SerialNumber, user.SerialNumber));
// custom data
identity.AddClaim(new Claim(ClaimTypes.UserData, user.Id.ToString()));
// add roles
var roles = await _rolesService.FindUserRolesAsync(user.Id).ConfigureAwait(false);
foreach (var role in roles)
{
identity.AddClaim(new Claim(ClaimTypes.Role, role.Name));
}
return new ClaimsPrincipal(identity);
} [Route("api/[controller]")]
[Authorize(Policy = CustomRoles.Admin)]
public class MyProtectedAdminApiController : Controller [AllowAnonymous]
[HttpGet("[action]"), HttpPost("[action]")]
public async Task<bool> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return true;
} Authorize(Policy = CustomRoles.Admin)]
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
cookieClaims,
new AuthenticationProperties
{
IsPersistent =false, // "Remember Me"
IssuedUtc = DateTimeOffset.UtcNow,
ExpiresUtc = DateTimeOffset.UtcNow.AddDays(loginCookieExpirationDays)
}); InvalidOperationException: No IAuthenticationSignInHandler is configured to handle sign in for the scheme: Cookies
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// services.AddScoped<IUnitOfWork, ApplicationDbContext>();
//services.AddScoped<IUsersService, UsersService>();
//services.AddScoped<IRolesService, RolesService>();
//services.AddScoped<ISecurityService, SecurityService>();
//services.AddScoped<ICookieValidatorService, CookieValidatorService>();
//services.AddScoped<IDbInitializerService, DbInitializerService>();
var container = new Container();
container.Configure(config =>
{
config.AddRegistry(new MyStructuremapRegistry());
config.Populate(services);
});
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection"),
serverDbContextOptionsBuilder => {
var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
serverDbContextOptionsBuilder.CommandTimeout(minutes);
serverDbContextOptionsBuilder.EnableRetryOnFailure();
});
});
// Only needed for custom roles.
services.AddAuthorization(options =>
{
options.AddPolicy(CustomRoles.Admin, policy => policy.RequireRole(CustomRoles.Admin));
options.AddPolicy(CustomRoles.User, policy => policy.RequireRole(CustomRoles.User));
});
// Needed for cookie auth.
services
.AddAuthentication(options =>
{
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.SlidingExpiration = false;
options.LoginPath = "/api/account/login";
options.LogoutPath = "/api/account/logout";
//options.AccessDeniedPath = new PathString("/Home/Forbidden/");
options.Cookie.Name = ".my.app1.cookie";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = context =>
{
var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
return cookieValidatorService.ValidateAsync(context);
}
};
});
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
services.AddMvc();
return container.GetInstance<IServiceProvider>();
} public CookieValidatorService(IUsersService usersService)
{
_usersService = usersService;
_usersService.CheckArgumentIsNull(nameof(usersService));
} _usersService.CheckArgumentIsNull(nameof(usersService));
The TimeSpan after which the authentication ticket stored inside the cookie expires. ExpireTimeSpan is added to the current time to create the expiration time for the ticket. The ExpiredTimeSpan value always goes into the encrypted AuthTicket verified by the server. It may also go into the Set-Cookie header, but only if IsPersistent is set. To set IsPersistent to true, configure the AuthenticationProperties passed to SignInAsync. The default value of ExpireTimeSpan is 14 days.
beforeSend: function (request) {
request.withCredentials = true;
sendRequestVerificationToken(request);
},
function sendRequestVerificationToken(request) {
request.setRequestHeader("RequestVerificationToken", $("[name='__RequestVerificationToken']").val());
} public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAntiforgery(options => options.HeaderName = "__RequestVerificationToken");
} @inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
@functions{
public string GetAntiXsrfRequestToken()
{
return Xsrf.GetAndStoreTokens(Context).RequestToken;
}
} <script type="text/javascript">
$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
jqXHR.setRequestHeader("__RequestVerificationToken", '@GetAntiXsrfRequestToken()');
});
</script> app.UseCookieAuthentication(new CookieAuthenticationOptions {
LoginPath = new PathString("/Login/"),
AuthenticationType = "My-Magical-Authentication",
// etc...
},
});
و به شکل زیر پیاده سازی CheckArgumentIsNull را در ApplicationDbContext انجام دادم:
public interface IUnitOfWork : IDisposable
{
DbSet<TEntity> Set<TEntity>() where TEntity : class;
int SaveChanges(bool acceptAllChangesOnSuccess);
int SaveChanges();
Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken());
Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken());
void CheckArgumentIsNull(string v);
}
void CheckArgumentIsNull(string arg)
{
if (arg == null)
throw new ArgumentNullException(nameof(arg));
} آیا کاری که کردم درست است؟
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
namespace BlazorServerTestDynamicAccess.Services;
public class CustomAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
private readonly IServiceScopeFactory _scopeFactory;
public CustomAuthenticationStateProvider(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory)
: base(loggerFactory) =>
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
protected override TimeSpan RevalidationInterval { get; } = TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user from a new scope to ensure it fetches fresh data
var scope = _scopeFactory.CreateScope();
try
{
var userManager = scope.ServiceProvider.GetRequiredService<IUsersService>();
return await ValidateUserAsync(userManager, authenticationState?.User);
}
finally
{
if (scope is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
private async Task<bool> ValidateUserAsync(IUsersService userManager, ClaimsPrincipal? principal)
{
if (principal is null)
{
return false;
}
var userIdString = principal.FindFirst(ClaimTypes.UserData)?.Value;
if (!int.TryParse(userIdString, out var userId))
{
return false;
}
var user = await userManager.FindUserAsync(userId);
return user is not null;
}
} public async Task<int?> GetUserIdAsync()
{
var authenticationState = await _authenticationStateProvider.GetAuthenticationStateAsync();
if (!authenticationState.User.Identity.IsAuthenticated)
return 0;
return Convert.ToInt32(authenticationState.User.Claims?.First().Value);
} GetAuthenticationStateAsync was called before SetAuthenticationState
@inherits OwningComponentBase
...
private IUserInfoService userInfoService { get; set; }
...
userInfoService = ScopedServices.GetRequiredService<IUserInfoService>(); @inject IUserInfoService UserInfoService
public class ManualLog: IManualLog
{
private readonly IUserInfoService _userInfoService;
public ManualLog(IUserInfoService userInfoService)
{
_userInfoService = userInfoService ?? throw new ArgumentNullException(nameof(userInfoService));
}
public async task Log()
{
int userId = await _userInfoService.GetUserIdAsync();
}
} GetAuthenticationStateAsync was called before SetAuthenticationState
identity.AddClaim(new Claim("DisplayName", user.DisplayName)); @context.User.Claims.Where(c => c.Type == "DisplayName").FirstOrDefault().Value.ToString() await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, cookieClaims,
new AuthenticationProperties
{
// ...
RedirectUri = GetSafeRedirectUri(ReturnUrl)
});Unhandled exception rendering component: Invalid non-ASCII or control character in header: 0x062A System.InvalidOperationException: Invalid non-ASCII or control character in header: 0x062A at void Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowInvalidHeaderCharacter(char ch)