feat(api): аутентификация и авторизация. Кастомные ParrAuthenticationHandler и BaseSimpleRoleProvider.
This commit is contained in:
8
PARR.API/Authentication/Models/UserCache.cs
Normal file
8
PARR.API/Authentication/Models/UserCache.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace PARR.API.Authentication.Models
|
||||
{
|
||||
public class UserCache
|
||||
{
|
||||
public required string UserIp { get; set; }
|
||||
public List<UserRoleInCache> Roles { get; set; } = new List<UserRoleInCache>();
|
||||
}
|
||||
}
|
||||
8
PARR.API/Authentication/Models/UserRoleInCache.cs
Normal file
8
PARR.API/Authentication/Models/UserRoleInCache.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace PARR.API.Authentication.Models
|
||||
{
|
||||
public class UserRoleInCache
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public required string Description { get; set; }
|
||||
}
|
||||
}
|
||||
58
PARR.API/Authentication/ParrAuthenticationHandler.cs
Normal file
58
PARR.API/Authentication/ParrAuthenticationHandler.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Options;
|
||||
using PARR.API.Services.Interfaces;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
namespace PARR.API.Authentication
|
||||
{
|
||||
public class ParrAuthenticationHandler : AuthenticationHandler<ParrAuthenticationOptions>
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
public ParrAuthenticationHandler(
|
||||
IOptionsMonitor<ParrAuthenticationOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock,
|
||||
IServiceProvider serviceProvider) : base(options, logger, encoder, clock)
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var ipClient = Request.HttpContext.Connection.RemoteIpAddress?.MapToIPv4().ToString();
|
||||
|
||||
if (ipClient == null)
|
||||
return AuthenticateResult.Fail($"IP address not defined.");
|
||||
|
||||
// Аутентификация - просто проверка, есть ли у нас такой пользователь
|
||||
using (var scope = serviceProvider.CreateScope())
|
||||
{
|
||||
var authService = scope.ServiceProvider.GetRequiredService<IAuthService>();
|
||||
|
||||
var userIsBlocked = await authService.UserIsBlockedAsync(ipClient);
|
||||
if (userIsBlocked)
|
||||
return AuthenticateResult.Fail($"IP address is on the blocking list.");
|
||||
|
||||
var user = await authService.GetUserAsync(ipClient);
|
||||
if (user == null)
|
||||
return AuthenticateResult.Fail($"IP address not defined.");
|
||||
|
||||
|
||||
// пользователь найден, аутентификация пройдена
|
||||
|
||||
var claims = new List<Claim> {
|
||||
new Claim(ClaimTypes.Name, user.UserIp)
|
||||
};
|
||||
|
||||
var claimsIdentity = new ClaimsIdentity(claims, Scheme.Name);
|
||||
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
|
||||
|
||||
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, Scheme.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
PARR.API/Authentication/ParrAuthenticationOptions.cs
Normal file
10
PARR.API/Authentication/ParrAuthenticationOptions.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace PARR.API.Authentication
|
||||
{
|
||||
public class ParrAuthenticationOptions : AuthenticationSchemeOptions
|
||||
{
|
||||
public const string DefaultScheme = "ParrAuthenticationScheme";
|
||||
//public string TokenHeaderName { get; set; } = "MyToken";
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PARR.API.Contracts.V1;
|
||||
using PARR.API.Contracts.V1.Responses.Base;
|
||||
using PARR.API.Controllers.V1.Base;
|
||||
using PARR.API.Services.Interfaces;
|
||||
using PARR.Constants;
|
||||
using PARR.DAL.CacheServices;
|
||||
using PARR.DAL.TransformServices;
|
||||
|
||||
@@ -26,13 +28,13 @@ namespace PARR.API.Controllers.V1
|
||||
/// Получить мой ip
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
//[Authorize(Roles = ParrRoles.Administrator.Role)]
|
||||
[Authorize]
|
||||
[HttpGet(ApiRoutes.Test.GetMyIp)]
|
||||
public IActionResult GetMyIp()
|
||||
{
|
||||
var ipAddress = clientService.GetClientIp();
|
||||
|
||||
//var ipV4 = ipAddress?.MapToIPv4();
|
||||
|
||||
var ip = ipAddress?.ToString();
|
||||
|
||||
return Ok(new Response<object>(new { ip }, true));
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace PARR.API.Installers
|
||||
});
|
||||
|
||||
services.AddTransient<IClientService, ClientService>();
|
||||
|
||||
services.AddTransient<IAuthService, AuthService>();
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -13,10 +13,9 @@ namespace PARR.API.Installers
|
||||
configuration.GetSection(nameof(MqSettings)).Bind(mqSettings);
|
||||
services.AddSingleton(mqSettings);
|
||||
|
||||
|
||||
//var storageSettings = new StorageSettings();
|
||||
//configuration.GetSection(nameof(StorageSettings)).Bind(storageSettings);
|
||||
//services.AddSingleton(storageSettings);
|
||||
var userCacheSettings = new UserCacheSettings();
|
||||
configuration.GetSection(nameof(UserCacheSettings)).Bind(userCacheSettings);
|
||||
services.AddSingleton(userCacheSettings);
|
||||
|
||||
//TODO: add other
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using Elastic.CommonSchema.Serilog;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Server.HttpSys;
|
||||
using PARR.API.Authentication;
|
||||
using PARR.API.Installers;
|
||||
using PARR.API.RoleProvider;
|
||||
using PARR.BLL;
|
||||
using PARR.DAL;
|
||||
using Serilog;
|
||||
@@ -29,6 +32,12 @@ builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
|
||||
builder.Configuration.AddDalConfigurations(builder.Services);
|
||||
builder.Services.AddDallSettings(builder.Configuration);
|
||||
|
||||
// Auth
|
||||
builder.Services.AddAuthentication(ParrAuthenticationOptions.DefaultScheme)
|
||||
.AddScheme<ParrAuthenticationOptions, ParrAuthenticationHandler>(ParrAuthenticationOptions.DefaultScheme, opt => { });
|
||||
builder.Services.AddSimpleRoleAuthorization<BaseSimpleRoleProvider>();
|
||||
|
||||
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
@@ -44,6 +53,7 @@ var app = builder.Build();
|
||||
// Configure the HTTP request pipeline.
|
||||
app.InstallSwagger();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
48
PARR.API/RoleProvider/BaseSimpleRoleProvider.cs
Normal file
48
PARR.API/RoleProvider/BaseSimpleRoleProvider.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using PARR.API.Services.Interfaces;
|
||||
|
||||
namespace PARR.API.RoleProvider
|
||||
{
|
||||
public class BaseSimpleRoleProvider : ISimpleRoleProvider
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
public BaseSimpleRoleProvider(IServiceProvider serviceProvider)
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task<ICollection<string>> GetUserRolesAsync(string ipClient)
|
||||
{
|
||||
// Авторизация - проверка на разрешения
|
||||
// В данном случае, получаем все разрешения (роли)
|
||||
|
||||
ICollection<string> defaultResult = new string[0];
|
||||
|
||||
using (var scope = serviceProvider.CreateScope())
|
||||
{
|
||||
//var clientService = scope.ServiceProvider.GetRequiredService<IClientService>();
|
||||
|
||||
//var ipClient = clientService.GetClientIp()?.ToString();
|
||||
//if (string.IsNullOrEmpty(ipClient))
|
||||
// return await Task.FromResult(defaultResult);
|
||||
|
||||
var authService = scope.ServiceProvider.GetRequiredService<IAuthService>();
|
||||
|
||||
// проверяем блокировку пользователя
|
||||
var userIsBlocked = await authService.UserIsBlockedAsync(ipClient);
|
||||
if (userIsBlocked)
|
||||
return await Task.FromResult(defaultResult);
|
||||
|
||||
// получаем пользователя из кэша или бд
|
||||
var user = await authService.GetUserAsync(ipClient);
|
||||
if (user == null)
|
||||
return await Task.FromResult(defaultResult);
|
||||
|
||||
|
||||
return user.Roles.Select(t => t.Name).ToList();
|
||||
}
|
||||
|
||||
// return await Task.FromResult(defaultResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
PARR.API/RoleProvider/ISimpleRoleProvider.cs
Normal file
7
PARR.API/RoleProvider/ISimpleRoleProvider.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace PARR.API.RoleProvider
|
||||
{
|
||||
public interface ISimpleRoleProvider
|
||||
{
|
||||
Task<ICollection<string>> GetUserRolesAsync(string ipClient);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace PARR.API.RoleProvider
|
||||
{
|
||||
public static class SimpleRoleAuthorizationServiceCollectionExtensions
|
||||
{
|
||||
public static void AddSimpleRoleAuthorization<TRoleProvider>(this IServiceCollection services)
|
||||
where TRoleProvider : class, ISimpleRoleProvider
|
||||
{
|
||||
services.AddSingleton<ISimpleRoleProvider, TRoleProvider>();
|
||||
services.AddSingleton<IClaimsTransformation, SimpleRoleAuthorizationTransform>();
|
||||
}
|
||||
}
|
||||
}
|
||||
39
PARR.API/RoleProvider/SimpleRoleAuthorizationTransform.cs
Normal file
39
PARR.API/RoleProvider/SimpleRoleAuthorizationTransform.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace PARR.API.RoleProvider
|
||||
{
|
||||
public class SimpleRoleAuthorizationTransform : IClaimsTransformation
|
||||
{
|
||||
//private static readonly string RoleClaimType = $"http://{typeof(SimpleRoleAuthorizationTransform).FullName.Replace('.', '/')}/role";
|
||||
private static readonly string RoleClaimType = ClaimTypes.Role;
|
||||
|
||||
private readonly ISimpleRoleProvider roleProvider;
|
||||
|
||||
public SimpleRoleAuthorizationTransform(ISimpleRoleProvider roleProvider)
|
||||
{
|
||||
this.roleProvider = roleProvider ?? throw new ArgumentNullException(nameof(roleProvider));
|
||||
}
|
||||
|
||||
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
|
||||
{
|
||||
// Cast the principal identity to a Claims identity to access claims etc...
|
||||
var oldIdentity = (ClaimsIdentity)principal.Identity!;
|
||||
|
||||
// "Clone" the old identity to avoid nasty side effects.
|
||||
// NB: We take a chance to replace the claim type used to define the roles with our own.
|
||||
var newIdentity = new ClaimsIdentity(
|
||||
oldIdentity.Claims,
|
||||
oldIdentity.AuthenticationType,
|
||||
oldIdentity.NameClaimType,
|
||||
RoleClaimType);
|
||||
|
||||
// Fetch the roles for the user and add the claims of the correct type so that roles can be recognized.
|
||||
var roles = await roleProvider.GetUserRolesAsync(newIdentity.Name!);
|
||||
newIdentity.AddClaims(roles.Select(r => new Claim(RoleClaimType, r)));
|
||||
|
||||
// Create and return a new claims principal
|
||||
return new ClaimsPrincipal(newIdentity);
|
||||
}
|
||||
}
|
||||
}
|
||||
87
PARR.API/Services/Implementations/AuthService.cs
Normal file
87
PARR.API/Services/Implementations/AuthService.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using PARR.API.Authentication.Models;
|
||||
using PARR.API.Services.Interfaces;
|
||||
using PARR.API.Settings;
|
||||
using PARR.Constants;
|
||||
using PARR.DAL.CacheServices;
|
||||
using PARR.DAL.Services.Interfaces;
|
||||
|
||||
namespace PARR.API.Services.Implementations
|
||||
{
|
||||
public class AuthService : IAuthService
|
||||
{
|
||||
private readonly IRedisCacheService cacheService;
|
||||
private readonly IUserService userService;
|
||||
private readonly IHostService hostService;
|
||||
private readonly UserCacheSettings userCacheSettings;
|
||||
|
||||
public AuthService(IRedisCacheService cacheService, IUserService userService, IHostService hostService, UserCacheSettings userCacheSettings)
|
||||
{
|
||||
this.cacheService = cacheService;
|
||||
this.userService = userService;
|
||||
this.hostService = hostService;
|
||||
this.userCacheSettings = userCacheSettings;
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> UserIsBlockedAsync(string ipAddress)
|
||||
{
|
||||
//пользователь есть в списке блокировки?
|
||||
var existUserInBlockList = await cacheService.GetCachedDataAsync<DateTimeOffset?>(GetBlockKey(ipAddress));
|
||||
|
||||
return existUserInBlockList != null;
|
||||
}
|
||||
|
||||
|
||||
public async Task<UserCache?> GetUserAsync(string ipAddress)
|
||||
{
|
||||
// смотрим, есть ли в кэше
|
||||
var userInCache = await cacheService.GetCachedDataAsync<UserCache>(GetAllowKey(ipAddress));
|
||||
if (userInCache != null)
|
||||
return userInCache;
|
||||
|
||||
// нет в кэше
|
||||
var roles = new List<UserRoleInCache>();
|
||||
|
||||
var user = await userService.GetByIpWithRolesAsync(ipAddress);
|
||||
if (user != null)
|
||||
user.Roles.ToList().ForEach(t =>
|
||||
{
|
||||
if (t.Role != null)
|
||||
roles.Add(new UserRoleInCache { Name = t.Role.Name, Description = t.Role.Description });
|
||||
});
|
||||
|
||||
var host = await hostService.GetByIpAsync(ipAddress);
|
||||
if (host != null)
|
||||
roles.Add(new UserRoleInCache { Name = ParrRoles.Agent.Role, Description = ParrRoles.Agent.Description });
|
||||
|
||||
if (user == null && host == null)
|
||||
{
|
||||
// если это не пользователь и не хост, добавляем в кэш на блокировку
|
||||
await cacheService.SetCachedDataAsync(GetBlockKey(ipAddress), DateTimeOffset.UtcNow, userCacheSettings.UserBlockTtl);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// пользователь есть, добавляем в КЭШ и возвращаем роли
|
||||
var cacheData = new UserCache { UserIp = ipAddress, Roles = roles };
|
||||
await cacheService.SetCachedDataAsync(GetAllowKey(ipAddress), cacheData, userCacheSettings.UserCacheTtl);
|
||||
|
||||
return cacheData;
|
||||
}
|
||||
|
||||
|
||||
private string GetBlockKey(string ipAddress)
|
||||
{
|
||||
// d - deny
|
||||
return $"d_{ipAddress}";
|
||||
}
|
||||
|
||||
private string GetAllowKey(string ipAddress)
|
||||
{
|
||||
// a - allow
|
||||
return $"a_{ipAddress}";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
22
PARR.API/Services/Interfaces/IAuthService.cs
Normal file
22
PARR.API/Services/Interfaces/IAuthService.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using PARR.API.Authentication.Models;
|
||||
|
||||
namespace PARR.API.Services.Interfaces
|
||||
{
|
||||
public interface IAuthService
|
||||
{
|
||||
/// <summary>
|
||||
/// Получить пользователя с ролями по ip адресу.
|
||||
/// Смотрим в кэше, затем в БД. При необходимости блокирует или добавлет в кэш.
|
||||
/// </summary>
|
||||
/// <param name="ipAddress"></param>
|
||||
/// <returns></returns>
|
||||
Task<UserCache?> GetUserAsync(string ipAddress);
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет, блокировку пользователя
|
||||
/// </summary>
|
||||
/// <param name="ipAddress"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> UserIsBlockedAsync(string ipAddress);
|
||||
}
|
||||
}
|
||||
18
PARR.API/Settings/UserCacheSettings.cs
Normal file
18
PARR.API/Settings/UserCacheSettings.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace PARR.API.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Настройки кэша пользователй
|
||||
/// </summary>
|
||||
public class UserCacheSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Сколько хранить в кэше авторизованных пользователей
|
||||
/// </summary>
|
||||
public TimeSpan UserCacheTtl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Сколько хранить в кэше заблокированных пользователей (как долго блокировать пользователя)
|
||||
/// </summary>
|
||||
public TimeSpan UserBlockTtl { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"RedisConnection": "10.99.253.216:6379"
|
||||
"RedisConnection": "10.99.253.216:6379,password=ParrP@ssPtk202MMdevDvs"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
|
||||
@@ -45,5 +45,9 @@
|
||||
"User": "generate_templates_api",
|
||||
"Password": "DHhjgdsf*&%95"
|
||||
}
|
||||
},
|
||||
"UserCacheSettings": {
|
||||
"UserCacheTtl": "00:05:00",
|
||||
"UserBlockTtl": "00:03:00"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace PARR.DAL.CacheServices
|
||||
public async Task<T?> GetCachedDataAsync<T>(string key)
|
||||
{
|
||||
var jsonData = await cache.GetStringAsync(key);
|
||||
|
||||
|
||||
if (jsonData == null)
|
||||
return default(T);
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ namespace PARR.DAL
|
||||
services.AddTransient<IAgentHistoryService, AgentHistoryService>();
|
||||
services.AddTransient<IOrderService, OrderService>();
|
||||
services.AddTransient<IOrderStatusService, OrderStatusService>();
|
||||
services.AddTransient<IUserService, UserService>();
|
||||
|
||||
|
||||
// TransformServices
|
||||
|
||||
@@ -40,6 +40,11 @@ namespace PARR.DAL.Services.Implementations
|
||||
.FirstOrDefaultAsync(h => h.IP == IP);
|
||||
}
|
||||
|
||||
public async Task<Host?> GetByIpAsync(string ip)
|
||||
{
|
||||
return await EntitySet.FirstOrDefaultAsync(t => t.IP == ip);
|
||||
}
|
||||
|
||||
//public async Task<Host?> GetHostByRegionalEKAsync(string RegionalEK)И
|
||||
//{
|
||||
// try
|
||||
|
||||
30
PARR.DAL/Services/Implementations/UserService.cs
Normal file
30
PARR.DAL/Services/Implementations/UserService.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Models;
|
||||
using PARR.DAL.Services.Abstracts;
|
||||
using PARR.DAL.Services.Interfaces;
|
||||
|
||||
namespace PARR.DAL.Services.Implementations
|
||||
{
|
||||
internal class UserService : BaseService<User>, IUserService
|
||||
{
|
||||
private readonly DataContext dataContext;
|
||||
|
||||
public UserService(DataContext dataContext, ILogger<UserService> logger) : base(logger)
|
||||
{
|
||||
this.dataContext = dataContext;
|
||||
}
|
||||
|
||||
protected override DbSet<User> EntitySet => dataContext.Users;
|
||||
|
||||
protected override DataContext EntitiContext => dataContext;
|
||||
|
||||
public async Task<User?> GetByIpWithRolesAsync(string ipAddress)
|
||||
{
|
||||
return await EntitySet
|
||||
.Include(t => t.Roles).ThenInclude(t => t.Role)
|
||||
.FirstOrDefaultAsync(t => t.Ip == ipAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@ namespace PARR.DAL.Services.Interfaces
|
||||
{
|
||||
public interface IHostService : IBaseService<Host>
|
||||
{
|
||||
Task<Host?> GetHostWithAppsAsync(string IP);
|
||||
Task<Host?> GetHostWithAppsAsync(string ip);
|
||||
|
||||
Task<Host?> GetByIpAsync(string ip);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
10
PARR.DAL/Services/Interfaces/IUserService.cs
Normal file
10
PARR.DAL/Services/Interfaces/IUserService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using PARR.DAL.Models;
|
||||
using PARR.DAL.Services.Interfaces.Base;
|
||||
|
||||
namespace PARR.DAL.Services.Interfaces
|
||||
{
|
||||
public interface IUserService: IBaseService<User>
|
||||
{
|
||||
Task<User?> GetByIpWithRolesAsync(string ipAddress);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user