I det här inlägget beskrivs hur du kan lägga till Facebook-inloggning på din webbplats i ASP.NET Core. Du måste skapa en Facebook Applikation och lägga till Facebook Login som en produkt. Du behöver App ID och App Secret avseende din Facebook App för att kunna använda api:et, du måste också se till att du lägger till din callback url till Valid OAuth Redirect URI i inställningarna för Facebook Login.
När användaren loggar in med sitt Facebook-konto för första gången eller när användaren ansluter sitt konto till Facebook måste du spara användarens Facebook-ID i din databas. Ett Facebook-ID är ett långt heltal (Int64) men kan med fördel sparas som en sträng. Facebook-ID kommer att användas för att hitta användaren när han loggar in för andra gången.
Tjänster
Vi kommer att behöva en HttpClient
och en sessionstjänst för att kunna implementera Facebook-inloggning, vi lägger till dessa tjänster i metoden ConfigureServices i klassen StartUp i vårt projekt. Det är inte bra att löpande skapa och kassera HttpClients i ett program, man bör använda statiska HttpClients eller IHttpClientFactory
. Vi använder IHttpClientFactory
för att skapa en namngiven klient. Jag måste också lägga till tjänster för autentisering och auktorisation, jag har lagt till ett auktorisationsschema för administratörer.
using System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Annytab.Middleware;
using Annytab.Repositories;
using Annytab.Options;
using Annytab.Rules;
using System.Net.Http.Headers;
using System.Net.Http;
using System.Net;
namespace Hockeytabeller
{
/// <summary>
/// This class handles application startup
/// </summary>
public class Startup
{
/// <summary>
/// Variables
/// </summary>
public IConfiguration configuration { get; set; }
/// <summary>
/// Create a new startup object
/// </summary>
/// <param name="configuration">A reference to configuration</param>
public Startup(IConfiguration configuration)
{
this.configuration = configuration;
} // End of the constructor method
/// <summary>
/// This method is used to add services to the container.
/// </summary>
/// <param name="services"></param>
public void ConfigureServices(IServiceCollection services)
{
// Add the mvc framework
services.AddRazorPages();
// Add memory cache
services.AddDistributedMemoryCache();
// Add redis distributed cache
if (configuration.GetSection("AppSettings")["RedisConnectionString"] != "")
{
services.AddDistributedRedisCache(options =>
{
options.Configuration = configuration.GetSection("AppSettings")["RedisConnectionString"];
options.InstanceName = "Hockeytabeller:";
});
}
// Add cors
services.AddCors(options =>
{
options.AddPolicy("AnyOrigin", builder => builder.AllowAnyOrigin());
});
// Add the session service
services.AddSession(options =>
{
// Set session options
options.IdleTimeout = TimeSpan.FromMinutes(20d);
options.Cookie.Name = ".Hockeytabeller";
options.Cookie.Path = "/";
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});
// Create database options
services.Configure<DatabaseOptions>(options =>
{
options.connection_string = configuration.GetSection("AppSettings")["ConnectionString"];
options.sql_retry_count = 1;
});
// Create cache options
services.Configure<CacheOptions>(options =>
{
options.expiration_in_minutes = 240d;
});
// Add Authentication
services.AddAuthentication()
.AddCookie("Administrator", options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(1);
options.Cookie.MaxAge = TimeSpan.FromDays(1);
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Events.OnRedirectToLogin = (context) =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.Redirect("/admin_login");
return Task.CompletedTask;
};
})
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("ApiAuthentication", null);
// Add clients
services.AddHttpClient("default", client =>
{
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate });
// Add repositories
services.AddSingleton<IDatabaseRepository, MsSqlRepository>();
services.AddSingleton<IWebsiteSettingRepository, WebsiteSettingRepository>();
services.AddSingleton<IAdministratorRepository, AdministratorRepository>();
services.AddSingleton<IFinalRepository, FinalRepository>();
services.AddSingleton<IGroupRepository, GroupRepository>();
services.AddSingleton<IStaticPageRepository, StaticPageRepository>();
services.AddSingleton<IXslTemplateRepository, XslTemplateRepository>();
services.AddSingleton<ISitemapRepository, SitemapRepository>();
services.AddSingleton<IXslProcessorRepository, XslProcessorRepository>();
services.AddSingleton<ICommonServices, CommonServices>();
} // End of the ConfigureServices method
/// <summary>
/// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
/// </summary>
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IWebsiteSettingRepository website_settings_repository)
{
// Use error handling
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseStatusCodePagesWithReExecute("/home/error/{0}");
}
// Get website settings
KeyStringList settings = website_settings_repository.GetAllFromCache();
bool redirect_https = settings.Get("REDIRECT-HTTPS") == "true" ? true : false;
bool redirect_www = settings.Get("REDIRECT-WWW") == "true" ? true : false;
bool redirect_non_www = settings.Get("REDIRECT-NON-WWW") == "true" ? true : false;
// Add redirection and use a rewriter
RedirectHttpsWwwNonWwwRule rule = new RedirectHttpsWwwNonWwwRule
{
status_code = 301,
redirect_to_https = redirect_https,
redirect_to_www = redirect_www,
redirect_to_non_www = redirect_non_www,
hosts_to_ignore = new string[] { "localhost", "hockeytabeller001.azurewebsites.net" }
};
RewriteOptions options = new RewriteOptions();
options.Rules.Add(rule);
app.UseRewriter(options);
// Use static files
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
// Cache static files for 30 days
ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=25920000");
ctx.Context.Response.Headers.Append("Expires", DateTime.UtcNow.AddDays(300).ToString("R", CultureInfo.InvariantCulture));
}
});
// Use sessions
app.UseSession();
// For most apps, calls to UseAuthentication, UseAuthorization, and UseCors must
// appear between the calls to UseRouting and UseEndpoints to be effective.
app.UseRouting();
// Use CORS
app.UseCors();
// Use authentication and authorization middlewares
app.UseAuthentication();
app.UseAuthorization();
// Routing endpoints
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
"default",
"{controller=home}/{action=index}/{id?}");
});
} // End of the Configure method
} // End of the class
} // End of the namespace
Modeller
Svar från anrop till Facebook Api:et kommer som JSON och vi har skapat modeller för att kunna arbeta med svarsdatan på ett bekvämt sätt. Dessa modeller har skapats baserat på rena JSON-svar från Facebook Api:et. Det är enkelt att serialisera en modell till JSON och det är enkelt att deserialisera JSON till en modell i C#.
using System;
namespace Annytab.Models
{
public class FacebookAuthorization
{
#region Variables
public string access_token { get; set; }
public string token_type { get; set; }
#endregion
#region Constructors
public FacebookAuthorization()
{
// Set values for instance variables
this.access_token = null;
this.token_type = null;
} // End of the constructor
#endregion
} // End of the class
public class FacebookErrorRoot
{
#region Variables
public FacebookError error { get; set; }
#endregion
#region Constructors
public FacebookErrorRoot()
{
// Set values for instance variables
this.error = null;
} // End of the constructor
#endregion
} // End of the class
public class FacebookError
{
#region Variables
public string message { get; set; }
public string type { get; set; }
public Int32? code { get; set; }
public Int32? error_subcode { get; set; }
public string fbtrace_id { get; set; }
#endregion
#region Constructors
public FacebookError()
{
this.message = null;
this.type = null;
this.code = null;
this.error_subcode = null;
this.fbtrace_id = null;
} // End of the constructor
#endregion
} // End of the class
public class FacebookUser
{
#region Variables
public string id { get; set; }
public string name { get; set; }
#endregion
#region Constructors
public FacebookUser()
{
// Set values for instance variables
this.id = null;
this.name = null;
} // End of the constructor
#endregion
} // End of the class
} // End of the namespace
Klass för användare
Jag har skapat en klass som innehåller metoder avseende administratörer och jag har skapat följande metoder för att hämta en Facebook-användare. Denna klass injicerar IHttpClientFactory
som client_factory, du måste modifiera FACEBOOK_APP_ID och FACEBOOK_APP_SECRET.
/// <summary>
/// Get a facebook user
/// </summary>
public async Task<FacebookUser> GetFacebookUser(string code)
{
// Create variables
FacebookAuthorization facebook_authorization = null;
FacebookUser facebook_user = new FacebookUser();
// Get a http client
HttpClient client = this.client_factory.CreateClient("default");
// Create the url
string url = "https://graph.facebook.com/oauth/access_token?client_id=" + "FACEBOOK_APP_ID" +
"&redirect_uri=" + "http://localhost:59448" + "/facebook/login_callback" + "&client_secret="
+ "FACEBOOK_APP_SECRET" + "&code=" + code;
// Get the response
HttpResponseMessage response = await client.GetAsync(url);
// Make sure that the response is successful
if (response.IsSuccessStatusCode)
{
// Get facebook authorization
facebook_authorization = JsonSerializer.Deserialize<FacebookAuthorization>(await
response.Content.ReadAsStringAsync());
}
else
{
// Get an error
FacebookErrorRoot root = JsonSerializer.Deserialize<FacebookErrorRoot>(await
response.Content.ReadAsStringAsync());
}
// Make sure that facebook authorization not is null
if (facebook_authorization == null)
{
return null;
}
// Modify the url
url = "https://graph.facebook.com/me?fields=id,name&access_token=" + facebook_authorization.access_token;
// Get the response
response = await client.GetAsync(url);
// Make sure that the response is successful
if (response.IsSuccessStatusCode)
{
// Get a facebook user
facebook_user = JsonSerializer.Deserialize<FacebookUser>(await response.Content.ReadAsStringAsync());
}
else
{
// Get an error
FacebookErrorRoot root = JsonSerializer.Deserialize<FacebookErrorRoot>(await response.Content.ReadAsStringAsync());
}
// Return the facebook user
return facebook_user;
} // End of the GetFacebookUser method
/// <summary>
/// Get one administrator based on facebook user id
/// </summary>
public Administrator GetByFacebookUserId(string facebook_user_id)
{
// Create the sql statement
string sql = "SELECT * FROM dbo.administrators WHERE facebook_user_id = @facebook_user_id;";
// Create parameters
IDictionary<string, object> parameters = new Dictionary<string, object>();
parameters.Add("@facebook_user_id", facebook_user_id);
// Get the post
Administrator post = this.database_repository.GetModel<Administrator>(sql, parameters);
// Return the post
return post;
} // End of the GetByFacebookUserId method
Controller
Vår facebook-controller innehåller två metoder för hantering av Facebook-inloggning. Jag måste använda ett AuthenticationScheme
för att kunna få användarkontextdata, metoderna är markerade med AllowAnonymous
för att tillåta åtkomst från icke autentiserade användare.
using System.Threading.Tasks;
using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Annytab.Repositories;
using Annytab.Models;
using Annytab.Helpers;
namespace Hockeytabeller.Controllers
{
/// <summary>
/// This class handles facebook login
/// </summary>
[Authorize(AuthenticationSchemes = "Administrator")]
public class facebookController : Controller
{
#region Variables
private readonly IDataProtector data_protector;
private readonly IAdministratorRepository administrator_repository;
#endregion
#region Constructors
/// <summary>
/// Create a new controller
/// </summary>
public facebookController(IDataProtectionProvider provider, IAdministratorRepository administrator_repository)
{
// Set values for instance variables
this.data_protector = provider.CreateProtector("AuthTicket");
this.administrator_repository = administrator_repository;
} // End of the constructor
#endregion
#region Post methods
// Redirect the user to the facebook login
// GET: /facebook/login
[HttpGet]
[AllowAnonymous]
public IActionResult login()
{
// Create a random state
string state = DataValidation.GeneratePassword();
// Add session variables
ControllerContext.HttpContext.Session.Set<string>("FacebookState", state);
ControllerContext.HttpContext.Session.Set<string>("FacebookReturnUrl", "/admin_default");
// Create the url
string url = "https://www.facebook.com/dialog/oauth?client_id=" + "FACEBOOK_APP_ID" + "&state=" +
state + "&response_type=code&redirect_uri=" + "http://localhost:59448" + "/facebook/login_callback";
// Redirect the user
return Redirect(url);
} // End of the login method
// Login the user with facebook
// GET: /facebook/login_callback
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> login_callback()
{
// Get the state
string state = "";
if (ControllerContext.HttpContext.Request.Query.ContainsKey("state") == true)
{
state = ControllerContext.HttpContext.Request.Query["state"].ToString();
}
// Get sessions
string session_state = ControllerContext.HttpContext.Session.Get<string>("FacebookState");
string return_url = ControllerContext.HttpContext.Session.Get<string>("FacebookReturnUrl");
// Get the code
string code = "";
if (ControllerContext.HttpContext.Request.Query.ContainsKey("code") == true)
{
code = ControllerContext.HttpContext.Request.Query["code"];
}
// Make sure that the callback is valid
if (state != session_state || code == "")
{
return Redirect("/");
}
// Get a facebook user
FacebookUser facebook_user = await this.administrator_repository.GetFacebookUser(code);
// Get the signed in user
Administrator user = this.administrator_repository.GetOneByUsername(HttpContext.User.Identity.Name);
// Check if the user exists or not
if (facebook_user != null && user != null)
{
// Update the user
user.facebook_user_id = facebook_user.id;
this.administrator_repository.Update(user);
// Redirect the user to the return url
return Redirect(return_url);
}
else if (facebook_user != null && user == null)
{
// Check if we can find a user with the facebook id
user = this.administrator_repository.GetByFacebookUserId(facebook_user.id);
// Check if the user exists
if (user == null)
{
// Create a new user
user = new Administrator();
user.admin_user_name = facebook_user.id + "@" + DataValidation.GeneratePassword();
user.facebook_user_id = facebook_user.id;
user.admin_password = PasswordHash.CreateHash(DataValidation.GeneratePassword());
user.admin_role = "Editor";
// Add a user
int id = this.administrator_repository.Add(user);
this.administrator_repository.UpdatePassword(id, user.admin_password);
}
// Create claims
ClaimsIdentity identity = new ClaimsIdentity("Administrator");
//identity.AddClaim(new Claim("administrator", JsonSerializer.Serialize(user)));
identity.AddClaim(new Claim(ClaimTypes.Name, user.admin_user_name));
identity.AddClaim(new Claim(ClaimTypes.Role, user.admin_role));
ClaimsPrincipal principal = new ClaimsPrincipal(identity);
// Sign in the administrator
await HttpContext.SignInAsync("Administrator", principal);
// Redirect the user to the return url
return Redirect(return_url);
}
else
{
// Redirect the user to the start page
return Redirect("/");
}
} // End of the login_callback method
// Sign out the user
// GET: /facebook/logout
[HttpGet]
public async Task<IActionResult> logout()
{
// Sign out the administrator
await HttpContext.SignOutAsync("Administrator");
// Redirect the user to the login page
return RedirectToAction("index", "admin_login");
} // End of the logout method
#endregion
} // End of the class
} // End of the namespace