Share via


Il presente articolo è stato tradotto automaticamente.

ASP.NET

Abilitazione e personalizzazione della sicurezza dei servizi API Web ASP.NET.

Peter Vogel

Per lo scenario più comune — JavaScript in una pagina Web accede a un servizio Web API sullo stesso sito — discutendo di sicurezza per ASP.NET Web API è quasi ridondante. Purché si agli utenti di autenticare e autorizzare l'accesso a Web Form/viste tenendo il JavaScript che utilizza i servizi, hai probabilmente fornito tutta la sicurezza bisogno di vostri servizi. Questo è un risultato di ASP.NET inviando i cookies e le informazioni di autenticazione per convalidare le richieste di pagina come parte di eventuali richieste di JavaScript lato client per i metodi di servizio. C'è una sola eccezione (e si tratta di un'importante): ASP.NET non automaticamente vi protegge contro gli attacchi Cross-Site Request Forgery (CSRF/XSRF) (più avanti).

Oltre a CSRF, ci sono due scenari quando ha senso discutere di assicurare i servizi di Web API. Il primo scenario è quando il vostro servizio è consumato da un client diverso da una pagina nello stesso sito come il vostro ApiControllers. I clienti non sono stati autenticati tramite l'autenticazione basata su form e non hanno acquisito i cookie e i token che ASP.NET utilizza per controllare l'accesso ai servizi.

Il secondo scenario si verifica quando si desidera aggiungere ulteriore autorizzazione ai servizi di là di ciò che è fornito attraverso la sicurezza ASP.NET . L'autorizzazione predefinita che fornisce ASP.NET è basato sull'identità ASP.NET assegna alla richiesta durante l'autenticazione. Si potrebbe voler estendere tale identità per autorizzare l'accesso basato su qualcosa di diverso dal nome o il ruolo dell'identità.

Web API ti dà un numero di scelte per affrontare entrambi gli scenari. Infatti, mentre mi occuperò di sicurezza nel contesto di accettare le richieste Web API, perché le API Web è basato sulla stessa Fondazione ASP.NET come MVC e Web Form, strumenti che tratterò in questo articolo stanno per essere familiare a chiunque è andato sotto il cofano con sicurezza nel Web Form o MVC.

Una sola avvertenza: Mentre le API Web fornisce che con diverse scelte per l'autenticazione e autorizzazione, la sicurezza inizia con l'host, l'hosting IIS o un host che si crea quando auto. Se, ad esempio, si desidera garantire la privacy nella comunicazione tra il client e un servizio di Web API, quindi dovrebbe, perlomeno, girare su SSL. Questo, tuttavia, è una responsabilità dell'amministratore del sito, piuttosto che lo sviluppatore. In questo articolo ho intenzione di ignorare l'host a concentrarsi su ciò che uno sviluppatore può — e dovrebbe — fare per garantire un servizio di Web API (e gli strumenti che tratterò qui funzionano se SSL è acceso o spento).

Prevenzione degli attacchi di Cross-Site richiesta Forgery

Quando un utente accede a un sito ASP.NET utilizzando l'autenticazione basata su form, ASP.NET genera un cookie che prevede che l'utente è autenticato. Il browser continuerà a inviare tale cookie su ogni richiesta successiva per il sito, non importa da dove proviene la richiesta. Questo apre il tuo sito per attacchi CSRF, come fa qualsiasi schema di autenticazione dove il browser invia automaticamente le informazioni di autenticazione precedentemente ricevute. Se, dopo che il tuo sito fornisce il browser con il cookie di sicurezza, l'utente visita un sito dannoso, che il sito può inviare richieste al vostro servizio, piggy-backing sul cookie di autenticazione il browser ricevuto in precedenza.

Per prevenire gli attacchi CSRF, sarà necessario generare token antiforgery al server e incorporarli nella pagina per essere utilizzato nelle chiamate client-side. Microsoft fornisce la classe AntiForgery con un metodo GetToken che genera token specifici per l'utente che ha effettuato la richiesta (che può, naturalmente, essere l'utente anonimo). Questo codice genera due token e li mette nella ASP.NET MVC ViewBag dove possono essere utilizzati nella visualizzazione:

[Authorize(Roles="manager")]
public ActionResult Index()
{
  string cookieToken;
  string formToken;
  AntiForgery.GetTokens(null, out cookieToken, out formToken);
  ViewBag.cookieToken = cookieToken;
  ViewBag.formToken = formToken;
  return View("Index");
}

Eventuali chiamate JavaScript al server saranno necessario restituire i token come parte della richiesta (un sito CSRF non hanno questi token e non sarà in grado di restituire loro). Questo codice, in vista, genera dinamicamente una chiamata JavaScript che aggiunge i gettoni per le intestazioni di richiesta:

$.ajax("http://phvis.com/api/Customers",{
  type: "get",
  contentType: "application/json",
  headers: {
    'formToken': '@ViewBag.formToken',
    'cookieToken': '@ViewBag.cookieToken' }});

Una soluzione un po' più complessa sarebbe consentono di utilizzare unobtrusive JavaScript incorporando i token in campi nascosti nella visualizzazione. Il primo passo in questo processo sarebbe aggiungere il token al dizionario ViewData:

ViewData["cookieToken"] = cookieToken;
ViewData["formToken"] = formToken;

Ora, nella visualizzazione, è possibile incorporare i dati nei campi nascosti. Metodo nascosto di HtmlHelper ha solo bisogno di essere superato il valore di una chiave in ViewDate per generare il giusto tag input:

@Html.Hidden("formToken")

Il tag input risultante sarà utilizzare la chiave ViewData per il nome del tag e gli attributi id e mettere i dati recuperati dal dizionario ViewData nell'attributo value del tag. Il tag input generati dal codice precedente sarebbe simile a questa:

    <input id="formToken" name="formToken" type="hidden" value="...token..." />

Codice JavaScript (mantenuto in un file separato dalla vista) può quindi recuperare i valori da tag input e utilizzarli nella vostra chiamata ajax:

$.ajax("http://localhost:49226/api/Customers", {
  type: "get",
  contentType: "application/json",
  headers: {
    'formToken': $("#formToken").val(),
    'cookieToken': $("#cookieToken").val()}});

È possibile ottenere gli stessi obiettivi in un sito Web Form ASP.NET utilizzando il metodo RegisterClientScriptBlock sull'oggetto ClientScriptManager (recuperabile dalla proprietà ClientScript della pagina) per inserire del codice JavaScript con il token incorporato:

string CodeString = "function CallService(){" +
  "$.ajax('http://phvis.com/api/Customers',{" +
  "type: 'get', contentType: 'application/json'," +
  "headers: {'formToken': '" & formToken & "',” +
  "'cookieToken': '" & cookieToken & "'}});}"
this.ClientScript.RegisterClientScriptBlock(
  typeOf(this), "loadCustid", CodeString, true);

Infine, è necessario convalidare il token al server quando stai restituiti dalla chiamata JavaScript. Visual Studio 2012 agli utenti che hanno applicato l'aggiornamento ASP.NET e Web Tools 2012.2 troverete che il nuovo modello di pagina singola applicazione (SPA) include un filtro di ValidateHttpAntiForgeryToken che può essere utilizzato su metodi Web API. In assenza di tale filtro, è necessario recuperare i token e passarli al metodo Validate della classe AntiForgery (il metodo Validate genererà un'eccezione se il token non valido o sono stati generati per un altro utente). Il codice in Figura 1, utilizzato in un metodo di servizio Web API, recupera il token da intestazioni e li convalida.

Figura 1 convalida token CSRF in un metodo di servizio.

public HttpResponseMessage Get(){
  if (Request.Headers.TryGetValues("cookieToken", out tokens))
  {
    string cookieToken = tokens.First();
    Request.Headers.TryGetValues("formToken", out tokens);
    string formToken = tokens.First();
    AntiForgery.Validate(cookieToken, formToken);
  }
  else
  {
    HttpResponseMessage hrm =
      new HttpResponseMessage(HttpStatusCode.Unauthorized);
    hrm.ReasonPhrase = "CSRF tokens not found";
    return hrm;
  } 
  // ...
Code to process request ...

Usando il ValidateHttpAntiForgeryToken (anziché il codice all'interno del metodo) si muove elaborazione in precedenza nel ciclo (prima, ad esempio, modello vincolante), che è una buona cosa.

Perché nessun OAuth?

Questo articolo ignora studiosamente OAuth. La specifica OAuth­tion definisce come token possono essere Estratto da un client da un server di terze parti per essere inviati a un servizio che, a sua volta, convaliderà il token con il server di terze parti. Una discussione di come accedere a un provider di token OAuth dal client o del servizio è oltre la portata di questo articolo.

La versione iniziale di OAuth, inoltre, non è una buona partita per il Web API. Presumibilmente, uno dei principali motivi per l'utilizzo dell'API Web consiste nell'utilizzare le richieste di peso leggero basate su REST e JSON. Quell'obiettivo rende la prima versione di OAuth un'opzione attraente per i servizi Web API. I token specificati dalla prima versione di OAuth sono ingombranti e basato su XML. Fortunatamente, OAuth 2.0 ha introdotto una specifica per un token JSON di peso leggero che è più compatto il token da versioni precedenti. Presumibilmente, le tecniche descritte in questo articolo potrebbero essere utilizzate per elaborare qualsiasi token OAuth inviati al vostro servizio.

Autenticazione di base

La prima delle due responsabilità primaria che hanno nel garantire un servizio di Web API è l'autenticazione (le altre responsabilità essendo autorizzazione). Darò per scontato altri problemi — tutela della privacy, per esempio — sono gestiti presso l'host.

Idealmente, l'autenticazione e l'autorizzazione verrà eseguite il prima possibile nella pipeline di Web API per evitare di spendere i cicli di lavorazione su una richiesta che si intende negare. Soluzioni di autenticazione di questo articolo sono utilizzati molto presto nella pipeline — praticamente non appena viene ricevuta la richiesta. Queste tecniche consentono anche di integrare l'autenticazione con qualunque utente liste già sta mantenendo. Le tecniche di autorizzazione discusse possono essere applicate in una varietà di luoghi nella pipeline (inclusi come fine come il metodo del servizio stesso) e possono lavorare con autenticazione per autorizzare le richieste sulla base di alcuni altri criteri rispetto a nome dell'utente o ruolo.

È possibile supportare client che non sono passati attraverso l'autenticazione basata su form fornendo il proprio metodo di autenticazione in un modulo HTTP personalizzato (sto ancora assumendo qui che sta autenticando non contro gli account di Windows, ma contro la vostra lista di utenti validi). Ci sono due vantaggi principali di usando un modulo HTTP: un modulo partecipa HTTP logging e auditing; Inoltre, i moduli vengono richiamati molto presto nella pipeline. Mentre questi sono entrambi cose buone, moduli di venire con due costi: i moduli sono globali e vengono applicati a tutte le richieste per il sito, non solo le richieste Web API; Inoltre, per utilizzare i moduli di autenticazione, è necessario ospitare il servizio in IIS. Più avanti in questo articolo, mi occuperò di mediante delega i gestori che vengono richiamati solo per le richieste Web API e sono indipendente dall'host.

In questo esempio, utilizzando un modulo HTTP, presumo che IIS è utilizzando l'autenticazione di base e le credenziali utilizzate per authen­ticate un utente sono username e password, inviati dal client (in questo articolo, ti ignora la certificazione Windows ma discuterà utilizzando i certificati client). Presumo anche che il servizio Web API che io proteggo è assicurato mediante un attributo autorizza come questa, che consente di specificare un utente:

public class CustomersController : ApiController
{
  [Authorize(Users="Peter")]
  public Customer Get()
  {

Il primo passo nella creazione di una modulo HTTP di autorizzazione personalizzato è quello di aggiungere una classe al progetto di servizio che implementa le interfacce IHttpModule e IDisposable. In Init della classe passata al metodo che avrete bisogno di cablare due eventi dall'oggetto HttpApplication per il metodo. Il metodo che si collega all'evento AuthenticateRequest verrà chiamato quando vengono presentate le credenziali del client. Ma deve anche cablare il metodo EndRequest per generare il messaggio che fa sì che il client di inviare le proprie credenziali. Avrete anche bisogno di un metodo Dispose, ma non è necessario mettere qualcosa in esso per supportare il codice utilizzato qui:

public class PHVHttpAuthentication : IHttpModule, IDisposable
{
  public void Init(HttpApplication context)
  {
    context.AuthenticateRequest += AuthenticateRequests;
    context.EndRequest += TriggerCredentials;
  }
  public void Dispose()
  {
  }

Un HttpClient invierà le credenziali in risposta a un WWW -­Authenticate header che si includono nella risposta HTTP. È necessario includere tale intestazione quando una richiesta genera un codice di 401 stato (ASP.NET genererà un codice di 401 risposta quando il client è negato l'accesso a un servizio protetto). L'intestazione deve fornire un suggerimento per quanto riguarda il metodo di autenticazione utilizzato ed il Regno in cui si applica l'autenticazione (il Regno può essere qualsiasi stringa arbitraria e viene utilizzato per contrassegnare per le diverse aree del browser sul server). Il codice per inviare il messaggio è ciò che si mette nel metodo cablato per l'evento EndRequest. Questo esempio genera un messaggio che specifica che l'autenticazione di base è utilizzato all'interno del Regno PHVIS:

private static void TriggerCredentials(object sender, EventArgs e)
{
  HttpResponse resp = HttpContext.Current.Response;
  if (resp.StatusCode == 401)
  {
    resp.Headers.Add("WWW-Authenticate", @"Basic realm='PHVIS'");
  }
}

All'interno del metodo che hai cablato fino al metodo AuthenticateRequest, sarà necessario recuperare le intestazioni di autorizzazione che il cliente invierà a seguito di ricevere il tuo messaggio 401/WWW-Authenticate:

private static void AuthenticateRequests(object sender,
  EventArgs e)
{
  string authHeader =     
    HttpContext.Current.
Request.Headers["Authorization"];
  if (authHeader != null)
  {

Una volta stabilito che il client ha superato autorizzazione intestazione elementi (e continuando con la mia precedente ipotesi che il sito utilizza l'autenticazione di base), è necessario analizzare i dati in possesso di username e password. Il nome utente e la password sono separati da due punti e con codifica Base64. Questo codice recupera il nome utente e la password in una matrice di stringhe di due posizioni:

AuthenticationHeaderValue authHeaderVal =
  AuthenticationHeaderValue.Parse(authHeader);
if (authHeaderVal.Parameter != null)
{
  byte[] unencoded = Convert.FromBase64String(
    authHeaderVal.Parameter);
  string userpw =
    Encoding.GetEncoding("iso-8859-1").GetString(unencoded);
  string[] creds = userpw.Split(':');

Come dimostra questo codice, nomi utente e password vengono inviate in testo non crittografato. Se non si trasforma in SSL poi il tuo username e la password possono essere facilmente catturate (e questo codice funziona anche se è attivato SSL).

Il passo successivo è quello di convalidare il nome utente e password utilizzando qualsiasi meccanismo ha senso per te. Indipendentemente da come si convalida la richiesta (il codice da utilizzare nell'esempio seguente è probabilmente troppo semplice), il passo finale è quello di creare un'identità per l'utente che verrà utilizzato nei processi di autorizzazione più tardi nella pipeline ASP.NET .

Per passare le informazioni di identità attraverso la pipeline, è creare un oggetto GenericIdentity con il nome dell'identità che si desidera assegnare all'utente (nel codice seguente che ho assunto che l'identità è il nome utente inviato nell'intestazione). Una volta che hai creato l'oggetto GenericIdentity, è necessario inserire in proprietà CurrentPrincipal della classe Thread. ASP.NET mantiene anche un secondo contesto di protezione dell'oggetto HttpContext e, se il tuo host è IIS, è necessario supportare che impostando anche la proprietà User nella proprietà dell'oggetto HttpContext corrente oggetto GenericIdentity:

if (creds[0] == "Peter" && creds[1] == "pw")
{
  GenericIdentity gi = new GenericIdentity(creds[0]);
  Thread.CurrentPrincipal = new GenericPrincipal(gi, null);
  HttpContext.Current.User = Thread.CurrentPrincipal;
}

Se si desidera supportare la protezione basata sui ruoli, è necessario passare un array di nomi di ruolo come secondo parametro al costruttore GenericPrincipal. Questo esempio assegna ogni utente ai ruoli di manager e admin:

string[] roles = "manager,admin".Split(',');
Thread.CurrentPrincipal = new GenericPrincipal(gi, roles);

Per integrare il vostro modulo HTTP nell'elaborazione del tuo sito, nel file Web. config del progetto, utilizzare il tag Aggiungi all'interno dell'elemento di moduli. L'Aggiungi attributo del tag tipo deve essere impostata su una stringa composta dal nome di classe completo seguito dal nome dell'assembly del modulo:

<modules>
  <add name="myCustomerAuth"
    type="SecureWebAPI.PHVHttpAuthentication, SecureWebAPI"/>
</modules>

L'oggetto GenericIdentity creato funzionerà con l'attributo autorizza ASP.NET . È inoltre possibile accedere GenericIdentity all'interno di un metodo di servizio per eseguire le attività di autorizzazione. È possibile, ad esempio, fornire diversi servizi per gli utenti loggati e anonimi da determinare se un utente è stato autenticato controllando l'oggetto GenericIdentity proprietà IsAuthenticated (IsAuthenticated restituisce false per l'utente anonimo):

if (Thread.CurrentPrincipal.Identity.IsAuthenticated)
{

È possibile recuperare l'oggetto GenericIdentity più semplicemente attraverso la proprietà User:

if (User.Identity.IsAuthenticated)
{

Creazione di un Client compatibile

Al fine di consumare servizi protetti da questo modulo, un non-­JavaScript client deve fornire un'accettabile username e password. Per fornire le credenziali utilizzando il HttpClient .NET, si prima crea un oggetto HttpClientHandler e impostare la proprietà credenziali su un oggetto NetworkCredential tenendo il nome utente e la password (o impostare la proprietà UseDefaultCredentials dell'oggetto HttpClientHandler su true per utilizzare le credenziali di Windows dell'utente corrente). Quindi si crea il tuo oggetto HttpClient, passando l'oggetto HttpClientHandler:

HttpClientHandler hch = new HttpClientHandler();
hch.Credentials = new NetworkCredential ("Peter", "pw");
HttpClient hc = new HttpClient(hch);

Con tale configurazione fatta, è possibile emettere la richiesta al servizio. Il HttpClient non presentare le credenziali finché ha negato l'accesso al servizio e ha ricevuto il messaggio di WWW-Authenticate. Se le credenziali fornite dal HttpClient non sono accettabili, il servizio restituisce un HttpResponseMessage con StatusCode del suo risultato impostato su "non autenticato".

Il seguente codice chiama un servizio utilizzando il metodo GetAsync, verifica la presenza di un risultato positivo (se non c'è uno) viene visualizzato il codice di stato restituito dal servizio:

hc.GetAsync("http://phvis.com/api/Customers").ContinueWith(r =>
{
  HttpResponseMessage hrm = r.Result;
  if (hrm.IsSuccessStatusCode)
  {
    // ...
Process response ...
}
  else
  {
    MessageBox.Show(hrm.StatusCode.ToString());
  }
});

Supponendo che si aggira il processo di login ASP.NET per non-­JavaScript client, come ho fatto qui, alcun cookie di autenticazione non verranno creati e sarà convalidato individualmente ogni richiesta dal client. Per ridurre il sovraccarico su ripetutamente convalida le credenziali fornite dal client, è necessario considerare la memorizzazione nella cache delle credenziali è recuperare presso il servizio (e utilizzando il metodo Dispose per scartare quelle credenziali memorizzate nella cache).

Lavorando con i certificati Client

In un modulo HTTP, si recupera un oggetto del certificato client (e assicurarsi che sia presente e valido) con codice come questo:

System.Web.HttpClientCertificate cert =
  HttpContext.Current.Request.ClientCertificate;
if (cert!= null && cert.IsPresent && cert.IsValid)
{

Più avanti nella pipeline di elaborazione — in un metodo di servizio, per esempio — si recupera l'oggetto certificato (e controllare che ne esiste uno) con questo codice:

X509Certificate2 cert = Request.GetClientCertificate();
if (cert!= null)
{

Se un certificato è valido e attuale, possono inoltre verificare valori specifici nelle proprietà del certificato (ad esempio, soggetto o emittente).

Per inviare i certificati con un HttpClient, il primo passo è creare un oggetto WebRequestHandler anziché un HttpClientHandler (il WebRequestHandler offre più opzioni di configurazione che la HttpClientHandler):

WebRequestHandler wrh = new WebRequestHandler();

Si può avere il HttpClient ricerca automaticamente archivi certificati del client impostando ClientCertificateOptions dell'oggetto WebRequestHandler al valore automatico da enum ClientCertificateOption:

wrh.ClientCertificateOptions = ClientCertificateOption.Manual;

Per impostazione predefinita, tuttavia, il client deve esplicitamente allegare i certificati per il WebRequestHandler dal codice. È possibile recuperare il certificato da uno degli archivi certificati del client come in questo esempio, che recupera un certificato dall'archivio di CurrentUser utilizzando il nome dell'emittente:

X509Store certStore;
X509Certificate x509cert;
certStore = new X509Store(StoreName.My, 
  StoreLocation.CurrentUser);  
certStore.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
x509cert = certStore.Certificates.Find(
  X509FindType.FindByIssuerName, "PHVIS", true)[0];
store.Close();

Se l'utente è stato inviato un certificato client che, per qualche ragione, non sta per essere aggiunto all'archivio certificati dell'utente, è possibile creare un oggetto X509Certificate dal file del certificato con codice come questo:

x509cert = new X509Certificate2(@"C:\PHVIS.pfx");

Indipendentemente da come viene creato il X509Certificate, le fasi finali del client sono per aggiungere il certificato per la collezione WebRequestHandler ClientCertificates e quindi utilizzare il Web configurato­/RequestHandler per creare il HttpClient:

wrh.ClientCertificates.Add(x509cert);
hc = new HttpClient(wrh);

Autorizzazione in un ambiente indipendente

Mentre è possibile utilizzare un HttpModule in un ambiente indipendente, il processo per garantire le richieste all'inizio nella pipeline di elaborazione di un servizio indipendente è lo stesso: Ottenere le credenziali dalla richiesta, utilizzare tali informazioni per autenticare la richiesta e creare un'identità da passare alla proprietà CurrentPrincipal del thread corrente. Il meccanismo più semplice è quello di creare una convalida di nome utente e password. Per convalidare più appena una combinazione nome utente e password, è possibile creare un gestore delega. Sarò primo sguardo integrando una convalida di nome utente e password.

Per creare una convalida (supponendo ancora che stai utilizzando l'autenticazione di base), è necessario creare una classe che eredita da utente­NamePasswordValidator (è necessario aggiungere un riferimento alla libreria System. IdentityModel al progetto). L'unico metodo da classe di base che è necessario eseguire l'override è il metodo Validate, che verrà passato l'username e la password inviata al servizio del cliente. Come prima, una volta che tu hai convalidato il username e la password, è necessario creare un oggetto GenericPrincipal e utilizzarlo per impostare la proprietà CurrentPrincipal sulla classe Thread (perché non si utilizza IIS come il vostro ospite, non si imposta la proprietà HttpContext utente):

public class PHVValidator :
  System.IdentityModel.Selectors.UserNamePasswordValidator
{
  public override void Validate(string userName, string password)
  {
    if (userName == "Peter" && password == "pw")
    {
      GenericIdentity gi = new GenericIdentity(username, null);
      Thread.CurrentPrincipal = gi;
    }

Il codice seguente crea un host per un controller denominato clienti con un endpoint di http://phvis.com/MyServices e specifica una nuova convalida:

partial class PHVService : ServiceBase
{
  private HttpSelfHostServer shs;
  protected override void OnStart(string[] args)
  {
    HttpSelfHostConfiguration hcfg =
      new HttpSelfHostConfiguration("http://phvis.com/MyServices");
    hcfg.Routes.MapHttpRoute("CustomerServiceRoute",
      "Customers", new { controller = "Customers" });
    hcfg.UserNamePasswordValidator = new PHVValidator;       
    shs = new HttpSelfHostServer(hcfg);
    shs.OpenAsync();

Gestori di messaggi

Per più di convalidare il nome utente e la password, è possibile creare un gestore di messaggio API Web personalizzato. Gestori di messaggi hanno diversi vantaggi rispetto ad un modulo HTTP: gestori di messaggi non è legati a IIS, quindi protezione applicata a un gestore messaggi funzionerà con qualsiasi host; gestori di messaggi sono utilizzati solo da Web API, quindi forniscono un modo semplice per eseguire l'autorizzazione (e assegnare identità) per i servizi mediante un processo diverso da quello utilizzato con le pagine del sito Web; ed è anche possibile assegnare gestori di messaggi a specifici percorsi così che il tuo codice di sicurezza viene richiamato solo dove è necessario.

Il primo passo nella creazione di un gestore di messaggi è quello di scrivere una classe che eredita da DelegatingHandler ed eseguire l'override del relativo metodo SendAysnc:

public class PHVAuthorizingMessageHandler: DelegatingHandler
{
  protected override System.Threading.Tasks.Task<HttpResponseMessage>
    SendAsync(HttpRequestMessage request,
      System.Threading.CancellationToken cancellationToken)
  {

All'interno di tale metodo (e supponendo che si sta creando un gestore per itinerario) è possibile impostare la proprietà InnerHandler di DelegatingHandler affinché questo gestore può essere collegato alla pipeline con altri gestori:

HttpConfiguration hcon = request.GetConfiguration();
InnerHandler = new HttpControllerDispatcher(hcon);

In questo esempio, ho intenzione di assumere che una richiesta valida deve avere un token semplice nella sua querystring (abbastanza semplice: una coppia nome/valore di "authToken = xyx"). Se il token è mancante o non impostato xyx, il codice restituisce un codice di stato 403 (Forbidden).

Mi rivolgo in primo luogo la querystring in un insieme di coppie nome/valore chiamando il metodo GetQueryNameValuePairs dell'oggetto HttpRequestMessage passato al metodo. Quindi utilizzare LINQ per recuperare il token (o null se il token è manca). Se il token è mancante o non valido, creare un HttpResponseMessage con il codice di stato HTTP appropriato, eseguire il wrapping in un oggetto TaskCompletionSource e restituirlo:

string usingRegion = (from kvp in request.GetQueryNameValuePairs()
                      where kvp.Key == "authToken"
                      select kvp.Value).FirstOrDefault();
if (usingRegion == null || usingRegion != "xyx")
{
  HttpResponseMessage resp =
     new HttpResponseMessage(HttpStatusCode.Forbidden);
  TaskCompletionSource tsc =
     new TaskCompletionSource<HttpResponseMessage>();
  tsc.SetResult(resp);
  return tsc.Task;
}

Se il token è presente e impostato il valore corretto, creare un generico­principale oggetto e utilizzarlo per impostare la proprietà CurrentPrincipal del Thread (per supportare l'utilizzo di questo gestore messaggi sotto IIS, anche impostare la proprietà HttpContext utente se l'oggetto HttpContext non è null):

Thread.CurrentPrincipal = new GenericPrincipal(
  Thread.CurrentPrincipal.Identity.Name, null);     
if (HttpContext.Current != null)
{
  HttpContext.Current.User = Thread.CurrentPrincipal;
}

Con la richiesta di autenticazione tramite il token e l'identità impostata, il gestore di messaggi chiama il metodo base per continuare l'elaborazione:

return base.SendAsync(request, cancellationToken);

Se il tuo gestore dei messaggi deve essere utilizzato su tutti i controller, è possibile aggiungere alla pipeline di elaborazione Web API come qualsiasi altro gestore di messaggio. Tuttavia, per limitare il tuo gestore ad essere utilizzato solo su percorsi specifici, è necessario aggiungerla tramite il metodo MapHttpRoute. In primo luogo, creare un'istanza di classe e quindi passarla come parametro quinto a MapHttpRoute (questo codice richiede un'istruzione Imports/using per System.Web.Http):

routes.MapHttpRoute(
  "ServiceDefault",
  "api/Customers/{id}",
  new { id = RouteParameter.Optional },
  null,
  new PHVAuthorizingMessageHandler());

Anziché impostare le InnerHandler all'interno del DelegatingHandler, è possibile impostare la proprietà InnerHandler al dispatcher predefinito come parte di definire il tuo percorso:

routes.MapHttpRoute(
  "ServiceDefault",
  "api/{controller}/{id}",
  new { id = RouteParameter.Optional },
  null,
  new PHVAuthorizingMessageHandler
  {InnerHandler = new HttpControllerDispatcher(
    GlobalConfiguration.Configuration)});

Ora, anziché l'impostazione InnerHandler diffuse tra i più DelegatingHandlers, si sta gestendo e dalla posizione unica dove si definiscono i percorsi.

Estendendo il Principal

Se autorizzare richieste dal nome e dal ruolo non è sufficiente, è possibile estendere il processo di autorizzazione creando la propria classe principal implementando l'interfaccia IPrincipal. Tuttavia, per sfruttare i vantaggi di una classe di entità personalizzata, è necessario creare il proprio attributo di autorizzazione personalizzato o aggiungere codice personalizzato per i metodi di servizio.

Ad esempio, se avete una serie di servizi che possono essere raggiunte solo da utenti da una regione specifica, è possibile creare una semplice classe principale che implementa l'interfaccia IPrincipal e aggiunge una proprietà della regione, come mostrato Figura 2.

Figura 2 creazione di un Custom principale con proprietà aggiuntive

public class PHVPrincipal: IPrincipal
{
  public PHVPrincipal(string Name, string Region)
  {
    this.Name = Name;
    this.Region = Region;
  }
  public string Name { get; set; }
  public string Region { get; set; }
  public IIdentity Identity
  {
    get
    {
      return new GenericIdentity(this.Name);
    }
    set
    {
      this.Name = value.Name;
    }
   }
   public bool IsInRole(string role)
   {
     return true;
   }

Per approfittare di questa nuova classe principale (che funzionerà con qualsiasi host), è solo necessario crearne un'istanza e quindi utilizzarlo per impostare la proprietà CurrentPrincipal e utente. Il codice seguente cerca un valore nella stringa di query della richiesta associato il nome "regione. Dopo il recupero di tale valore, il codice viene utilizzato per impostare la proprietà Region del principale passando il valore al costruttore della classe:

string region = (from kvp in request.GetQueryNameValuePairs()
                 where kvp.Key == "region"
                 select kvp.Value).FirstOrDefault();
Thread.CurrentPrincipal = new PHVPrincipal(userName, region);

Se stai lavorando in ambito Microsoft .NET 4.5, piuttosto che implementa l'interfaccia IPrincipal, deve ereditare dalla classe ClaimsPrincipal nuova. ClaimsPrincipal supporta l'elaborazione basata su attestazioni e l'integrazione con Windows Identity Foundation (WIF). Che, tuttavia, è fuori portata per questo articolo (ti rivolgo quell'argomento in un prossimo articolo su claims-based security).

Che autorizza un'entità personalizzata

Con un nuovo oggetto principal in luogo è possibile creare un attributo di autorizzazione che si avvale dei nuovi dati trasportati dall'obbligato principale. In primo luogo, creare una classe che eredita da System.Web.Http.AuthorizeAttri­bute ed esegue l'override del relativo metodo IsAuthorized (questo è un diverso pro­cess dalla pratica ASP.NET MVC dove si creano nuovi attributi di autorizzazione estendendo System.Web.Http.Filters.Autho­rizationFilterAttribute). Il metodo IsAuthorized è passato un HttpActionContext, la cui proprietà possono essere utilizzate come parte del processo di autorizzazione. Tuttavia, questo esempio ha solo bisogno di estrarre l'oggetto principale da proprietà CurrentPrincipal del Thread, eseguire il cast al tipo principale personalizzato e controllare la proprietà Region. Se l'autorizzazione ha esito positivo, il codice restituisce true. Se l'autorizzazione non riesce, è necessario la proprietà ActionContext Response per creare una risposta personalizzata prima di restituire false, come illustrato Figura 3.

Figura 3 filtro un oggetto Principal Custom

public class RegionAuthorizeAttribute : System.Web.Http.AuthorizeAttribute
{
  public string Region { get; set; }
  protected override bool IsAuthorized(HttpActionContext actionContext)
  {
    PHVPrincipal phvPcp = Thread.CurrentPrincipal as PHVPrincipal;
    if (phvPcp != null && phvPcp.Region == this.Region)
    {
      return true;
    }
    else
    {
      actionContext.Response =
        new HttpResponseMessage(
          System.Net.HttpStatusCode.Unauthorized)
        {
          ReasonPhrase = "Invalid region"
        };
      return false;
    }        
  }
}

Il filtro di autorizzazione personalizzato può essere utilizzato solo come il filtro predefinito ASP.NET autorizza. Perché questo filtro ha una proprietà della regione, tale proprietà deve essere impostata la regione accettabile per questo metodo come parte della decorazione di un metodo di servizio con esso:

[RegionAuthorize(Region = "East")]
public HttpResponseMessage Get()
{

In questo esempio, ho scelto di ereditare l'autorizza­attributo perché il mio codice di autorizzazione è puramente CPU bound. Se il mio codice necessario per accedere ad alcune risorse di rete (o fare qualsiasi I/O a tutti), sarebbe stato una scelta migliore per implementare la IAuthorization­filtro interfaccia perché supporta le chiamate asincrone.

Come ho detto all'inizio di questo articolo: Il tipico scenario Web API non necessita di ulteriore autorizzazione, tranne per proteggere contro RFCS exploit. Ma quando è necessario estendere il sistema di sicurezza di default, l'API Web fornisce numerose scelte per tutta la pipeline di elaborazione dove è possibile integrare qualsiasi protezione è necessario. Ed è sempre meglio avere scelte.

Peter Vogel è un'entità a PH & V Information Services, specializzata in sviluppo ASP.NET con esperienza in architettura service-oriented architecture, XML, database e progettazione dell'interfaccia utente.

Grazie ai seguenti esperti tecnici per la revisione di questo articolo: Dominick Baier (thinktecture GmbH & Co KG), Barry Dorrans (Microsoft) e Mike Wasson (Microsoft)
Mike Wasson (mwasson@microsoft.com) è un programmatore Microsoft. Attualmente scrive su ASP.NET, focalizzata sul Web API.

Barry Dorrans (Barry.Dorrans@Microsoft.com) è uno sviluppatore di sicurezza di Microsoft, lavorando con il team di Azure Platform. Ha scritto "Inizio ASP.NET Security" e fu un Developer Security MVP prima di unirsi a Microsoft. Nonostante ciò egli misspells ancora crittografia su base regolare.

Dominick (dominick.baier@thinktecture.com) è un consulente per la protezione presso thinktecture (thinktecture.com). Il suo obiettivo principale è il controllo di identità e accessi in applicazioni distribuite ed egli è il creatore dei progetti open source popolari IdentityModel e IdentityServer. È possibile trovare il suo blog a leastprivilege.com.