ASP.NET MVC-alternatywa dla dostawcy ról?
Staram się unikać korzystania z dostawcy roli i dostawcy członkostwa, ponieważ jest zbyt niezgrabny moim zdaniem, a zatem staram się stworzyć własną "wersję", która jest mniej niezgrabna i łatwiejsza w zarządzaniu/elastyczna. Teraz moje pytanie.. czy istnieje alternatywa dla dostawcy roli, która jest przyzwoita? (Wiem, że mogę zrobić niestandardową rolę provier, dostawcę członkostwa itp.)
Przez bardziej zarządzalny / elastyczny mam na myśli, że jestem ograniczony do korzystania z klasy Roles static i nie implementować bezpośrednio w mojej warstwie usług, które wchodzą w interakcję z kontekstem bazy danych, zamiast tego jestem zobowiązany do korzystania ze statycznej klasy Roles, która ma własny kontekst bazy danych itp., również nazwy tabel są okropne..
Z góry dzięki.
5 answers
Jestem w tej samej łodzi co ty - zawsze nienawidziłem Roleproviderów. Tak, są świetne, jeśli chcesz uruchomić małą stronę , ale nie są zbyt realistyczne. Główną wadą, którą zawsze znajdowałem, jest to, że wiążą cię bezpośrednio z ASP.NET.
Sposób, w jaki szedłem do niedawnego projektu, polegał na zdefiniowaniu kilku interfejsów, które są częścią warstwy usług (Uwaga: trochę je uprościłem - ale można łatwo dodać do them):
public interface IAuthenticationService
{
bool Login(string username, string password);
void Logout(User user);
}
public interface IAuthorizationService
{
bool Authorize(User user, Roles requiredRoles);
}
Wtedy twoi użytkownicy mogą mieć Roles
enum:
public enum Roles
{
Accounting = 1,
Scheduling = 2,
Prescriptions = 4
// What ever else you need to define here.
// Notice all powers of 2 so we can OR them to combine role permissions.
}
public class User
{
bool IsAdministrator { get; set; }
Roles Permissions { get; set; }
}
Dla twojego IAuthenticationService
, możesz mieć implementację bazową, która sprawdza standardowe hasło, a następnie możesz mieć FormsAuthenticationService
, która robi trochę więcej, np. ustawia plik cookie itp. Do twojego AuthorizationService
, potrzebujesz czegoś takiego:
public class AuthorizationService : IAuthorizationService
{
public bool Authorize(User userSession, Roles requiredRoles)
{
if (userSession.IsAdministrator)
{
return true;
}
else
{
// Check if the roles enum has the specific role bit set.
return (requiredRoles & user.Roles) == requiredRoles;
}
}
}
Oprócz tych podstawowych usług można łatwo dodać usługi resetowania haseł itp.
Ponieważ używasz MVC, możesz zrobić autoryzację w poziom działania za pomocą ActionFilter
:
public class RequirePermissionFilter : IAuthorizationFilter
{
private readonly IAuthorizationService authorizationService;
private readonly Roles permissions;
public RequirePermissionFilter(IAuthorizationService authorizationService, Roles requiredRoles)
{
this.authorizationService = authorizationService;
this.permissions = requiredRoles;
this.isAdministrator = isAdministrator;
}
private IAuthorizationService CreateAuthorizationService(HttpContextBase httpContext)
{
return this.authorizationService ?? new FormsAuthorizationService(httpContext);
}
public void OnAuthorization(AuthorizationContext filterContext)
{
var authSvc = this.CreateAuthorizationService(filterContext.HttpContext);
// Get the current user... you could store in session or the HttpContext if you want too. It would be set inside the FormsAuthenticationService.
var userSession = (User)filterContext.HttpContext.Session["CurrentUser"];
var success = authSvc.Authorize(userSession, this.permissions);
if (success)
{
// Since authorization is performed at the action level, the authorization code runs
// after the output caching module. In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would later be served the
// cached page. We work around this by telling proxies not to cache the sensitive page,
// then we hook our custom authorization code into the caching mechanism so that we have
// the final say on whether or not a page should be served from the cache.
var cache = filterContext.HttpContext.Response.Cache;
cache.SetProxyMaxAge(new TimeSpan(0));
cache.AddValidationCallback((HttpContext context, object data, ref HttpValidationStatus validationStatus) =>
{
validationStatus = this.OnCacheAuthorization(new HttpContextWrapper(context));
}, null);
}
else
{
this.HandleUnauthorizedRequest(filterContext);
}
}
private void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// Ajax requests will return status code 500 because we don't want to return the result of the
// redirect to the login page.
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new HttpStatusCodeResult(500);
}
else
{
filterContext.Result = new HttpUnauthorizedResult();
}
}
public HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
{
var authSvc = this.CreateAuthorizationService(httpContext);
var userSession = (User)httpContext.Session["CurrentUser"];
var success = authSvc.Authorize(userSession, this.permissions);
if (success)
{
return HttpValidationStatus.Valid;
}
else
{
return HttpValidationStatus.IgnoreThisRequest;
}
}
}
Które możesz następnie udekorować swoimi działaniami kontrolera:
[RequirePermission(Roles.Accounting)]
public ViewResult Index()
{
// ...
}
Zaletą tego podejścia jest to, że można również użyć dependency injection i kontenera IoC do połączenia rzeczy. Można go również używać w wielu aplikacjach (nie tylko w ASP.NET jeden). Możesz użyć swojego ORM do zdefiniowania odpowiedniego schematu.
Jeśli potrzebujesz więcej szczegółów na temat FormsAuthorization/Authentication
usług lub gdzie się stąd udać, daj mi znać.
Edytuj: Aby dodać "przycinanie zabezpieczeń" , możesz to zrobić za pomocą HtmlHelper. To pewnie potrzebuje trochę więcej... ale masz pomysł.
public static bool SecurityTrim<TModel>(this HtmlHelper<TModel> source, Roles requiredRoles)
{
var authorizationService = new FormsAuthorizationService();
var user = (User)HttpContext.Current.Session["CurrentUser"];
return authorizationService.Authorize(user, requiredRoles);
}
A następnie wewnątrz widoku (używając tu składni Razor):
@if(Html.SecurityTrim(Roles.Accounting))
{
<span>Only for accounting</span>
}
EDIT: UserSession
wyglądałoby mniej więcej tak:
public class UserSession
{
public int UserId { get; set; }
public string UserName { get; set; }
public bool IsAdministrator { get; set; }
public Roles GetRoles()
{
// make the call to the database or whatever here.
// or just turn this into a property.
}
}
W ten sposób nie ujawniamy hash hasła i wszystkich innych szczegółów wewnątrz sesji bieżącego użytkownika, ponieważ są one naprawdę nie potrzebne do życia sesji użytkownika.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-09 20:49:10
Zaimplementowałem dostawcę ról w oparciu o post @TheCloudlessSky tutaj. Jest kilka rzeczy, które myślałem, że mogę dodać i podzielić się tym, co zrobiłem.
Najpierw, jeśli chcesz użyć klasy RequirepPermission
dla filtrów akcji jako atrybutu, musisz zaimplementować klasę ActionFilterAttribute
dla klasy RequirepPermission
.
Klasy interfejsu IAuthenticationService
i IAuthorizationService
public interface IAuthenticationService
{
void SignIn(string userName, bool createPersistentCookie);
void SignOut();
}
public interface IAuthorizationService
{
bool Authorize(UserSession user, string[] requiredRoles);
}
FormsAuthenticationService
Klasa
/// <summary>
/// This class is for Form Authentication
/// </summary>
public class FormsAuthenticationService : IAuthenticationService
{
public void SignIn(string userName, bool createPersistentCookie)
{
if (String.IsNullOrEmpty(userName)) throw new ArgumentException(@"Value cannot be null or empty.", "userName");
FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
}
public void SignOut()
{
FormsAuthentication.SignOut();
}
}
UserSession
calss
public class UserSession
{
public string UserName { get; set; }
public IEnumerable<string> UserRoles { get; set; }
}
Kolejny punkt to FormsAuthorizationService
klasa i jak możemy przypisać użytkownika do httpContext.Session["CurrentUser"]
. My Podejściem w tej sytuacji jest utworzenie nowej instancji klasy userSession i bezpośrednie przypisanie użytkownika z httpContext.User.Identity.Name
do zmiennej userSession, jak widać w klasie FormsAuthorizationService
.
[AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)]
public class RequirePermissionAttribute : ActionFilterAttribute, IAuthorizationFilter
{
#region Fields
private readonly IAuthorizationService _authorizationService;
private readonly string[] _permissions;
#endregion
#region Constructors
public RequirePermissionAttribute(string requiredRoles)
{
_permissions = requiredRoles.Trim().Split(',').ToArray();
_authorizationService = null;
}
#endregion
#region Methods
private IAuthorizationService CreateAuthorizationService(HttpContextBase httpContext)
{
return _authorizationService ?? new FormsAuthorizationService(httpContext);
}
public void OnAuthorization(AuthorizationContext filterContext)
{
var authSvc = CreateAuthorizationService(filterContext.HttpContext);
// Get the current user... you could store in session or the HttpContext if you want too. It would be set inside the FormsAuthenticationService.
if (filterContext.HttpContext.Session == null) return;
if (filterContext.HttpContext.Request == null) return;
var success = false;
if (filterContext.HttpContext.Session["__Roles"] != null)
{
var rolesSession = filterContext.HttpContext.Session["__Roles"];
var roles = rolesSession.ToString().Trim().Split(',').ToList();
var userSession = new UserSession
{
UserName = filterContext.HttpContext.User.Identity.Name,
UserRoles = roles
};
success = authSvc.Authorize(userSession, _permissions);
}
if (success)
{
// Since authorization is performed at the action level, the authorization code runs
// after the output caching module. In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would later be served the
// cached page. We work around this by telling proxies not to cache the sensitive page,
// then we hook our custom authorization code into the caching mechanism so that we have
// the final say on whether or not a page should be served from the cache.
var cache = filterContext.HttpContext.Response.Cache;
cache.SetProxyMaxAge(new TimeSpan(0));
cache.AddValidationCallback((HttpContext context, object data, ref HttpValidationStatus validationStatus) =>
{
validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
}, null);
}
else
{
HandleUnauthorizedRequest(filterContext);
}
}
private static void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// Ajax requests will return status code 500 because we don't want to return the result of the
// redirect to the login page.
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new HttpStatusCodeResult(500);
}
else
{
filterContext.Result = new HttpUnauthorizedResult();
}
}
private HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
{
var authSvc = CreateAuthorizationService(httpContext);
if (httpContext.Session != null)
{
var success = false;
if (httpContext.Session["__Roles"] != null)
{
var rolesSession = httpContext.Session["__Roles"];
var roles = rolesSession.ToString().Trim().Split(',').ToList();
var userSession = new UserSession
{
UserName = httpContext.User.Identity.Name,
UserRoles = roles
};
success = authSvc.Authorize(userSession, _permissions);
}
return success ? HttpValidationStatus.Valid : HttpValidationStatus.IgnoreThisRequest;
}
return 0;
}
#endregion
}
internal class FormsAuthorizationService : IAuthorizationService
{
private readonly HttpContextBase _httpContext;
public FormsAuthorizationService(HttpContextBase httpContext)
{
_httpContext = httpContext;
}
public bool Authorize(UserSession userSession, string[] requiredRoles)
{
return userSession.UserRoles.Any(role => requiredRoles.Any(item => item == role));
}
}
Następnie w kontrolerze po uwierzytelnieniu użytkownika można pobrać role z bazy danych i przypisać je do sesji ról:
var roles = Repository.GetRolesByUserId(Id);
if (ControllerContext.HttpContext.Session != null)
ControllerContext.HttpContext.Session.Add("__Roles",roles);
FormsService.SignIn(collection.Name, true);
Po wylogowaniu się użytkownika z systemu możesz wyczyścić sesję
FormsService.SignOut();
Session.Abandon();
return RedirectToAction("Index", "Account");
Zastrzeżenie w tym modelu jest takie, że gdy użytkownik jest zalogowany do system, jeśli rola jest przypisana do użytkownika, autoryzacja nie działa, chyba że wyloguje się i loguje z powrotem w systemie.
Inną rzeczą jest to, że nie ma potrzeby posiadania osobnej klasy dla ról, ponieważ możemy pobrać role bezpośrednio z bazy danych i ustawić je w sesji ról w kontrolerze.
Po zakończeniu implementacji wszystkich tych kodów ostatnim krokiem jest powiązanie tego atrybutu z metodami w kontrolerze:
[RequirePermission("Admin,DM")]
public ActionResult Create()
{
return View();
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-11-01 15:50:08
Jeśli używasz Castle Windsor Dependency Injection, możesz wprowadzić listy Roleproviderów, które mogą być użyte do ustalenia praw Użytkownika z dowolnego źródła, które zdecydujesz się zaimplementować.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-06-18 00:52:45
Nie musisz używać klasy statycznej dla ról. Na przykład, SqlRoleProvider pozwala zdefiniować role w bazie danych.
Oczywiście, jeśli chcesz odzyskać role z własnej warstwy usług, nie jest tak trudno stworzyć własnego dostawcę ról - naprawdę nie ma zbyt wielu metod do wdrożenia.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-01-29 13:58:05
Możesz zaimplementować własne członkostwo i rolę dostawców poprzez nadpisanie odpowiednich interfejsów.
Jeśli chcesz zacząć od zera, zazwyczaj tego typu rzeczy są zaimplementowane jako niestandardowy moduł http, który przechowuje poświadczenia użytkowników w httpcontext lub sesji. Tak czy inaczej prawdopodobnie będziesz chciał ustawić plik cookie z jakimś tokenem uwierzytelniania.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-01-29 14:00:19