目录
介绍
Firebase
计划
你需要什么
创建Web应用程序
Startup.cs
Userscontroller.cs
Angular应用程序
Angular.json
app.module.ts
environment.ts
app-routing.module.ts
auth-service.service.ts
auth-guard.guard.ts
auth-interceptor.ts
home.component.ts
login.component.ts
secret.component.ts
设置Firebase项目
我们走!
想法
有用的信息
- 这篇文章的Github
在启动Web项目时,您必须忍受的最及时和最重复的任务之一是创建身份验证系统。这包括用于存储用户信息的数据存储、用于创建和登录用户的机制、用于用户帐户的管理系统以及允许用户与您的应用程序交互的一种方式。对我们来说幸运的是,有许多服务选项可以通过为我们处理一些步骤来缩短我们在此过程中花费的时间。
FirebaseFirebase是一个提供多种基于云的开发人员服务的平台。我们将在我们的应用程序中实现和合并的服务是Firebase Auth。Firebase Auth是一种多平台身份验证服务,可提供用户创建和存储、各种注册机制等功能,并为我们提供简单的库以将社交媒体平台身份验证添加到我们的系统中。
计划我们将在本文中做的是:
- 我们将创建一个ASP.NET Core Web API项目。
- 我们将使用Firebase服务器端库通过使用从Firebase系统创建的JWT (JSON-Web_Token)承载令牌来初始化我们的Web应用程序的身份验证和授权中间件。
- 我们将使用提供的授权属性保护Web API控制器方法。
- 我们将为我们的“前端”创建一个Angular客户端应用程序。
- 我们将在我们的Angular应用程序中创建一个授权服务,它将使用Firebase系统作为其授权机制。
- 我们将创建一种使用Google社交媒体身份验证提供程序让我们的用户登录的方法。
- 我们将创建受保护的路由和一个angular拦截器类,以对受保护的控制器方法进行安全的休息调用。
- 我们将创建一个简单的UI。
- .NET Core 3.1(我确信3,0版本可能也能工作。)
- 节点包管理器——Npm(我当前的版本是6.13)
- 一个代码编辑器(我用的是Visual Studio 2019社区版)
要创建Web应用程序,我们将打开一个命令提示符:
创建解决方案和Web API项目并丢弃不需要的代码类。
dotnet new sln --name FirebaseAndAngular
dotnet new webapi --name FirebaseAndAngular.Web --output .
dotnet sln add .\FirebaseAndAngular.Web.csproj
rm .\WeatherForecaset.cs
rm .\Controllers\WeatherForecastController.cs
dotnet restore .\FirebaseAndAngular.sln
添加Web应用程序所需的包:
dotnet add package FirebaseAdmin
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.AspNetCore.SpaServices
dotnet add package Microsoft.AspNetCore.SpaServices.Extensions
Startup.cs
更新startup.cs:
public class Startup
{
public Startup(IConfiguration configuration,IWebHostEnvironment env)
{
Configuration = configuration;
HostingEnvironment = env;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment HostingEnvironment { get; set; }
// This method gets called by the runtime.
// Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSpaStaticFiles(config =>
{
config.RootPath = "wwwroot";
});
services.AddControllers();
var pathToKey = Path.Combine(Directory.GetCurrentDirectory(),
"keys", "firebase_admin_sdk.json");
if (HostingEnvironment.IsEnvironment("local"))
pathToKey = Path.Combine(Directory.GetCurrentDirectory(),
"keys", "firebase_admin_sdk.local.json");
FirebaseApp.Create(new AppOptions
{
Credential = GoogleCredential.FromFile(pathToKey)
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
var firebaseProjectName = Configuration["FirebaseProjectName"];
options.Authority =
"https://securetoken.google.com/" + firebaseProjectName;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "https://securetoken.google.com/" + firebaseProjectName,
ValidateAudience = true,
ValidAudience = firebaseProjectName,
ValidateLifetime = true
};
});
}
// This method gets called by the runtime.
// Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "client-app";
if (env.IsDevelopment() || env.IsEnvironment("local"))
{
var startScript = env.IsEnvironment("local") ? "start-local" : "start";
spa.UseAngularCliServer(npmScript: startScript);
}
});
}
}
让我们来看看代码在做什么:
services.AddSpaStaticFiles(config =>
{
config.RootPath = "wwwroot";
});
此块注册SPA(单页应用程序)静态文件提供程序。这为我们提供了一种为单页应用程序(例如Angular站点)提供服务的方法。该RootPath属性是我们将从中提供已编译的angular应用程序的地方。
FirebaseApp.Create(new AppOptions
{
Credential = GoogleCredential.FromFile(pathToKey)
});
此代码实例化Firebase应用程序实例。应用程序将使用此实例来调用firebase服务。该GoogleCrendential.FromFile函数从文件创建Firebase SDK的凭据。在应用程序的后面,我将向您展示如何从Firebase管理仪表板中检索这些值。
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
var firebaseProjectName = Configuration["FirebaseProjectName"];
options.Authority = "https://securetoken.google.com/" + firebaseProjectName;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "https://securetoken.google.com/" + firebaseProjectName,
ValidateAudience = true,
ValidAudience = firebaseProjectName,
ValidateLifetime = true
};
});
此代码块在我们的应用程序中启动身份验证服务。这将允许使用框架的身份验证和授权中间件。我们的身份验证机制将使用JWT。我们在AddJwtBearer函数内部设置这些属性。当我们在本文后面回顾Firebase项目的创建时,我将向您展示在何处检索Firebase项目的名称。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "client-app";
if (env.IsDevelopment() || env.IsEnvironment("local"))
{
var startScript = env.IsEnvironment("local") ? "start-local" : "start";
spa.UseAngularCliServer(npmScript: startScript);
}
});
}
在Startup类的Configure方法中,注意中间件的顺序。我们添加了app.UseAuthentication以确保对API的调用在适当的时候利用我们的身份验证服务。该UseSpaStaticFiles和UseSpa方法的中间件,这将有助于正确地服务于我们的angular应用程序。它甚至包含在我们调试应用程序时命令Angular cli服务器进行实时客户端更新的部分。
Userscontroller.cs这个控制器持有将从我们的客户端应用程序调用的端点。
[...]
namespace FirebaseAndAngular.Web.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
[HttpPost("verify")]
public async Task VerifyToken(TokenVerifyRequest request)
{
var auth = FirebaseAdmin.Auth.FirebaseAuth.DefaultInstance;
try
{
var response = await auth.VerifyIdTokenAsync(request.Token);
if (response != null)
return Accepted();
}
catch (FirebaseException ex)
{
return BadRequest();
}
return BadRequest();
}
[HttpGet("secrets")]
[Authorize]
public IEnumerable GetSecrets()
{
return new List()
{
"This is from the secret controller",
"Seeing this means you are authenticated",
"You have logged in using your google account from firebase",
"Have a nice day!!"
};
}
}
}
让我们仔细看看:
[HttpPost("verify")]
public async Task VerifyToken(TokenVerifyRequest request)
{
var auth = FirebaseAdmin.Auth.FirebaseAuth.DefaultInstance;
try
{
var response = await auth.VerifyIdTokenAsync(request.Token);
if (response != null)
return Accepted();
}
catch (FirebaseException ex)
{
return BadRequest();
}
return BadRequest();
}
验证端点是我们在客户端验证用户身份后将调用的端点。我们将从Firebase用户获取检索到的令牌并从服务器验证它。对于我们目前的情况并不完全需要,但这是传递您从Firebase用户对象检索的其他信息的绝佳位置。特别是如果您想使用社交身份验证提供程序进行身份验证,但要将用户记录存储到您自己的数据存储中。在这里,我们获得了Firebase Auth对象的默认实例(我们在startup类中初始化的那个)。然后,我们调用一个方法来针对Firebase验证令牌,以检查我们是否有合法用户在我们的应用程序中对其进行了身份验证。
[HttpGet("secrets")]
[Authorize]
public IEnumerable GetSecrets()
{
return new List()
{
"This is from the secret controller",
"Seeing this means you are authenticated",
"You have logged in using your google account from firebase",
"Have a nice day!!"
};
}
这个控制器的secrets端点是一个简单的方法,它将返回一个字符串集合。我们已添加Authorize属性以使用我们的身份验证服务保护此端点。由于我们使用的是JWT身份验证机制,因此我们将使我们的客户端应用程序将Firebase检索和验证的Bearer令牌添加到我们的HTTP请求的授权标头中。任何没有或有错误令牌的调用都将收到403禁止错误。
Angular应用程序让我们回到命令提示符来启动我们的angular应用程序。从.csproj文件所在的目录开始。首先,让我们获取angular CLI工具。
npm install -g angular/cli
让我们创建Angular应用程序。如果被问到,请对路由选项说是。
ng new client-app
为应用程序的输出创建文件夹。这是我们在Web API项目的startup类中设置为SPA根文件夹的文件夹。
mkdir wwwroot
进入Angular应用程序的目录。
cd client-app
我们将创建一些需要的组件、类和服务:
ng generate component home
ng generate component login
ng generate component secret
ng g class models/currentUser
ng g guard security/authGuard
安装Firebase库所需的包。这个包叫做AngularFire。它是Firebase的官方Angular库。你可以在这里查看。
npm install firebase @angular/fire --save
很好!现在让我们看一些代码。
[...]
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "wwwroot",
"index": "src/index.html",
[...]
让我们注意这里我们必须改变的地方。在此文件中,我们必须将属性outputPath设置为值“wwwroot”。这将告诉Angular在构建应用程序时将输出文件存放在我们的wwwroot文件夹中,这将允许我们的dotnet Core Web应用程序正确托管SPA。
app.module.tsimport { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { LoginComponent } from './login/login.component';
import { SecretComponent } from './secret/secret.component';
import { AngularFireModule } from '@angular/fire';
import { AngularFireAuthModule } from '@angular/fire/auth';
import { environment } from '../environments/environment';
import { AuthGuardGuard } from './security/auth-guard.guard';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './security/auth-interceptor';
@NgModule({
declarations: [
AppComponent,
HomeComponent,
LoginComponent,
SecretComponent
],
imports: [
BrowserModule,
AppRoutingModule,
AngularFireModule.initializeApp(environment.firebaseConfig),
AngularFireAuthModule,
HttpClientModule
],
providers: [
AuthGuardGuard,
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
],
bootstrap: [AppComponent]
})
export class AppModule { }
这里要注意的重要事情是我们添加了将我们的AuthInterceptor类设置为我们的HTTP拦截器。还有这一节:
[...]
AngularFireModule.initializeApp(environment.firebaseConfig),
[...]
这一行是用我们环境类中的一个对象初始化AngularFire模块,我们现在来看看。
environment.tsexport const environment = {
production: false,
firebaseConfig : {
apiKey: "",
authDomain: "",
databaseURL: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: ""
}
};
在环境类中,我们创建了一个firebaseConfig属性。该对象是初始化AngularFire模块所需的config对象。现在,我们为将从Firebase项目中检索的值设置了占位符。
app-routing.module.ts[...]
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'login', component: LoginComponent },
{ path: 'secret', component: SecretComponent, canActivate: [AuthGuardGuard] },
{ path: '**', component: HomeComponent }
];
[...]
路由模块文件的这一部分是我们放置angular路由的地方。每个路由都定义了一个组件,当路由激活时将使用该组件。注意秘密路线。这是我们应用程序的受保护路径。为了确定用户是否可以访问,我们提供了我们的守卫类AuthGuardGuard(我知道,另一个坏名字)。
auth-service.service.ts这个类是大部分升沉发生的地方。我们将分别查看每个函数并讨论发生了什么。
[...]
user$: BehaviorSubject = new BehaviorSubject(new CurrentUser());
constructor(private angularAuth: AngularFireAuth, private httpclient: HttpClient) {
this.angularAuth.authState.subscribe((firebaseUser) => {
this.configureAuthState(firebaseUser);
});
}
[...]
类开始时有一个命名user$的BehaviorSubject。每当当前用户的状态改变时,这个主题就会触发。在构造函数中,我们将此服务订阅到angularAuth对象的Observable authState属性。每当用户成功登录或注销时, this的值都会发送到configureAuth state函数:
configureAuthState(firebaseUser: firebase.User): void {
if (firebaseUser) {
firebaseUser.getIdToken().then((theToken) => {
console.log('we have a token');
this.httpclient.post('/api/users/verify', { token: theToken }).subscribe({
next: () => {
let theUser = new CurrentUser();
theUser.displayName = firebaseUser.displayName;
theUser.email = firebaseUser.email;
theUser.isSignedIn = true;
localStorage.setItem("jwt", theToken);
this.user$.next(theUser);
},
error: (err) => {
console.log('inside the error from server', err);
this.doSignedOutUser()
}
});
}, (failReason) => {
this.doSignedOutUser();
});
} else {
this.doSignedOutUser();
}
}
这个函数首先检查我们是否有一个有效的firebaseUser对象。当身份验证成功时,此对象将有一个值,否则,当用户注销时,它将是null。成功后,我们将从firebaseUser检索到的令牌发送到服务器进行验证。验证令牌后,我们可以将其添加到本地存储以在整个应用程序中使用。我们还从Firebase返回的属性创建我们自己的用户对象,然后我们触发user$主题的下一个方法。如果服务器出现空对象或故障,我们会清除所有内容并确保用户已注销。
doGoogleSignIn(): Promise {
var googleProvider = new firebase.auth.GoogleAuthProvider();
googleProvider.addScope('email');
googleProvider.addScope('profile');
return this.angularAuth.auth.signInWithPopup(googleProvider).then((auth) => {});
}
这个函数创建一个GoogleAuthProvider对象,然后添加scope对象让谷歌通知用户我们的项目在授权后可以访问什么。在这种情况下,它将创建一个弹出窗口,启动Google身份验证过程。成功后,它将关闭,焦点将返回到我们的网站。此时,angularAuth.authState可观测对象将触发继续我们网站上的身份验证过程。
private doSignedOutUser() {
let theUser = new CurrentUser();
theUser.displayName = null;
theUser.email = null;
theUser.isSignedIn = false;
localStorage.removeItem("jwt");
this.user$.next(theUser);
}
不言自明。它使用户的属性无效并从本地存储中删除令牌并触发user$主题的下一个函数。
logout(): Promise {
return this.angularAuth.auth.signOut();
}
getUserobservable(): Observable {
return this.user$.asObservable();
}
getToken(): string {
return localStorage.getItem("jwt");
}
getUserSecrets(): Observable {
return this.httpclient.get("/api/users/secrets").pipe(map((resp: string[]) => resp));
}
这些其他的都很容易。Logout将用户从您的Firebase项目中注销。GetUserobservable检索用户对象作为可观察对象。这将在警卫类中使用。Get令牌从本地存储中检索JWT。这将被拦截器使用。最后,getUsersecrets是一个调用我们受保护的API端点的函数。
auth-guard.guard.ts[...]
export class AuthGuardGuard implements CanActivate {
constructor(private authservice: AuthServiceService, private router: Router) {
}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable {
return this.authservice
.getUserobservable()
.pipe(map(u => u != null && u.isSignedIn));
}
}
[...]
此类保护指定的路由。为了确定用户是否可以访问路线,使用该canActivate函数。此函数将从getUserobservable身份验证服务调用该方法。如果它在那里并且isSignedIn属性是true,那么路由被批准激活并且用户可以访问,否则,路由访问将失败并最终返回到home组件。
auth-interceptor.ts[...]
intercept(req: HttpRequest, next: HttpHandler): Observable {
var token = this.authService.getToken();
if (token) {
var header = "Bearer " + token;
var reqWithAuth = req.clone({ headers: req.headers.set("Authorization", header) });
return next.handle(reqWithAuth);
}
return next.handle(req);
}
拦截器将拦截使用HttpModule。我们在这里所做的是尝试检索令牌。如果用户已登录并拥有有效的令牌,那么我们可以使用JWT添加一个授权标头并调用服务器。在我们的Web API项目中调用“secrets”端点时,这是必要的。如果令牌不存在,则将使用下一个参数的句柄函数正常进行任何调用。
home.component.ts[...]
export class HomeComponent implements OnInit {
currentUser: CurrentUser = new CurrentUser();
$authSubscription: Subscription;
constructor(private authService: AuthServiceService, private router: Router) {
this.$authSubscription = this.authService.user$.subscribe(u => {
this.currentUser = u;
});
}
home类很简单。它从身份验证服务订阅user$主题。它使用这些属性来控制模板上存在的值,例如为登录用户显示不同的欢迎消息,而不是为未经身份验证的用户显示不同的欢迎消息。
login.component.ts[...]
loginWithGoogle() {
this.authService.doGoogleSignIn().then(() => {
this.router.navigate(['']);
});
}
[...]
Login与home具有相同的auth服务订阅。Login还包含doGoogleSignin对auth服务方法的调用。这是从模板中按钮的单击事件触发的。
secret.component.tsexport class SecretComponent implements OnInit {
secrets: string[] = [];
constructor(private authService: AuthServiceService) { }
ngOnInit() {
this.authService.getUserSecrets().subscribe(secretData => { this.secrets = secretData });
}
}
这是受路由保护的组件。在其中,我们所做的就是调用API的secrets端点。如果一切正常,拦截器应该使用有效的授权标头重写请求,我们应该取回我们的数据。
设置Firebase项目这涵盖了代码,但在某些文件和配置中仍有一些值需要您填写。要获得这些值,您需要创建Firebase项目。我们不会深入探讨如何执行此操作的过程,但这应该是一个很好的起点。
首先,让我们访问https://firebase.google.com网站。
单击转到控制台链接。这将带您进入“欢迎使用Firebase”屏幕或提示您使用您的Google帐户登录。(如果你没有,那么你显然需要注册。)
您可能会看到这样的屏幕:
单击创建项目按钮。接下来,系统会提示您输入项目名称:
输入一个然后点击继续。接下来,您应该为您的项目创建一个应用程序。点击创建新应用程序按钮,它应该看起来像:
接下来是一个对话框,您将在其中命名您的应用程序:
然后您将看到一个包含配置值的对话框。这些值是应该插入到environment.ts文件中的值。执行此操作后,您应该单击左侧导航中的身份验证链接。您应该会看到一个用于身份验证的子菜单,如下所示:
单击登录方法。使用它可以为您的应用程序启用Google登录方法。
位于名为“firebase_admin_sdk.json”的Web项目的“keys”文件夹中的文件需要在其中包含您的服务帐户的私钥。所以首先返回到您的Firebase项目的仪表板。
在项目概览旁边的左侧菜单上是一个齿轮。按它并转到子菜单。项目设置。到达此处后,您应该转到“服务帐户”选项卡。在这里你会看到一个按钮,上面写着Generate new private key。点击它。将生成的文件将包含您应该粘贴到密钥文件夹中的“firebase_admin_sdk.json”中的内容。这是为了让您使用Firebase SDK的服务器端代码可以在Web API项目中进行身份验证。
我们走!转到项目文件所在的目录并从命令提示符执行。
dotnet run
打开浏览器并转到您为该站点设置的URL应该会打开非常普通的主页。
单击登录,然后单击使用 Google 登录按钮。这应该会打开一个弹出窗口,引导您完成 Google 身份验证过程。其中一个屏幕应告知您 Firebase 项目的名称以及您的项目想要访问的信息。身份验证成功后,您将进入主页,现在它将使用您从 Google 提供的显示名称显示不同的消息。同样在菜单中,应该显示到秘密路线的链接。单击该链接,您应该会看到 secret 组件,该组件将立即调用 API 上的 secrets 端点。这应该带回字符串集合并将它们绑定到列表。
我在研究本文的不同部分时学到了很多东西,我希望我也能帮助你学到一些东西。本教程只是一些有用的库和服务以及如何使用它们的“入门”指南。它不应被视为保护网站的完整指南。
有用的信息- Firebase Documentation
- Angular Documentation
- .NET Firebase Admin SDK
https://www.codeproject.com/Articles/5308552/Securing-a-Website-Using-Firebase-Angular-8-and-2