使用 Firebase、Angular 8 和 ASP.NET Core 3.1 安全地保护网站





5.00/5 (2投票s)
如何使用 Firebase、Angular 8 和 ASP.NET Core 3.1 安全地保护网站
引言
在开始一个 Web 项目时,您必须忍受的最及时和重复的任务之一就是创建身份验证系统。这包括用于存储用户信息的数据存储、用户创建和登录机制、用户管理系统以及视觉元素(创建用户表单、登录表单、注销链接等),这些元素允许用户与您的应用程序进行交互。幸运的是,有许多服务能够通过处理其中的一些步骤来缩短我们在此过程中花费的时间。
Firebase
Firebase 是一个提供多种云端开发者服务的平台。我们将在应用程序中实现和集成的主要服务是 Firebase Auth。Firebase Auth 是一种多平台身份验证服务,提供用户创建和存储、各种注册机制等功能,并为我们提供易于使用的库,以便将社交媒体平台身份验证添加到我们的系统中。
计划
本文我们将要制作的是
- 我们将创建一个 ASP.NET Core Web API 项目。
- 我们将使用 Firebase 服务器端库,通过使用 Firebase 系统创建的 JWT(JSON-Web_Token)承载令牌来初始化我们 Web 应用程序的身份验证和授权中间件。
- 我们将使用提供的授权属性来保护 Web API 控制器方法。
- 我们将创建一个 Angular 客户端应用程序作为我们的“前端”。
- 我们将在 Angular 应用中创建一个授权服务,它将使用 Firebase 系统作为其授权机制。
- 我们将创建一个方法,该方法使用 Google 社交媒体身份验证提供商让我们的用户登录。
- 我们将创建受保护的路由和 Angular 拦截器类,以向我们的受保护控制器方法发出安全的 REST 调用。
- 我们将创建一个简单的 UI。
您将需要
- .NET Core 3.1(我确信 3.0 版本很可能也能正常工作。)
- Node 包管理器 – Npm(我当前的版本是 6.13)
- 一个代码编辑器(我使用的是 Visual Studio 2019 社区版)
创建 Web 应用程序
要创建 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<IActionResult> 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<string> GetSecrets()
{
return new List<string>()
{
"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<IActionResult> 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<string> GetSecrets()
{
return new List<string>()
{
"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 身份验证机制,我们将使我们的客户端应用在 HTTP 请求的授权标头中添加由 Firebase 检索和验证的承载令牌。任何没有令牌或令牌无效的调用都将收到 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
太好了!现在让我们看看一些代码。
Angular.json
[...]
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "wwwroot",
"index": "src/index.html",
[...]
让我们注意一下我们必须在这里进行的更改。在此文件中,我们必须将 outputPath
属性设置为“wwwroot
”值。这将告诉 Angular 在构建应用程序时将输出文件存放到我们的 wwwroot 文件夹中,这将允许我们的 dotnet core Web 应用程序正确托管 SPA。
app.module.ts
import { 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.ts
export 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 路由的地方。每个路由都定义了一个将在路由激活时使用的组件。注意 secret 路由。这是我们应用程序的受保护路由。为了确定用户是否可以访问,我们提供了守卫类 AuthGuardGuard
(我知道,又一个糟糕的名字)。
auth-service.service.ts
这个类是大部分繁重工作发生的地方。我们将单独查看每个函数并讨论它们的作用。
[...]
user$: BehaviorSubject<CurrentUser> = new BehaviorSubject<CurrentUser>(new CurrentUser());
constructor(private angularAuth: AngularFireAuth, private httpclient: HttpClient) {
this.angularAuth.authState.subscribe((firebaseUser) => {
this.configureAuthState(firebaseUser);
});
}
[...]
该类首先有一个名为 user$
的 BehaviorSubject
。每当当前用户状态发生变化时,此主题就会触发。在构造函数中,我们将此服务订阅到 angularAuth
对象的 Observable authState
属性。每当用户成功登录或注销时,其值就会发送到 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$
主题的 next 方法。如果有一个空对象或服务器失败,我们会清除所有内容并确保用户已注销。
doGoogleSignIn(): Promise<void> {
var googleProvider = new firebase.auth.GoogleAuthProvider();
googleProvider.addScope('email');
googleProvider.addScope('profile');
return this.angularAuth.auth.signInWithPopup(googleProvider).then((auth) => {});
}
此函数创建一个 GoogleAuthProvider
对象,然后添加 scope
对象,以让 Google 告知用户我们的项目在授权后可以访问什么。在这种情况下,它将创建一个弹出窗口,启动 Google 身份验证过程。成功后,它将关闭并使焦点返回到我们的网站。此时,angularAuth.authState
observable 将触发,继续我们网站上的身份验证过程。
private doSignedOutUser() {
let theUser = new CurrentUser();
theUser.displayName = null;
theUser.email = null;
theUser.isSignedIn = false;
localStorage.removeItem("jwt");
this.user$.next(theUser);
}
非常简单明了。它将用户属性置为 null,并从本地存储中删除令牌,然后触发 user$
主题的 next 函数。
logout(): Promise<void> {
return this.angularAuth.auth.signOut();
}
getUserobservable(): Observable<CurrentUser> {
return this.user$.asObservable();
}
getToken(): string {
return localStorage.getItem("jwt");
}
getUserSecrets(): Observable<string[]> {
return this.httpclient.get("/api/users/secrets").pipe(map((resp: string[]) => resp));
}
其他的也很简单。Logout 会将用户从您的 Firebase
项目中注销。GetUserobservable
以 observable 的形式检索用户对象。这将在守卫类中使用。Get
token 从本地存储中检索 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<boolean> {
return this.authservice
.getUserobservable()
.pipe(map(u => u != null && u.isSignedIn));
}
}
[...]
此类保护指定的路由。要确定用户是否可以访问某个路由,将使用 canActivate
函数。此函数将调用 auth 服务中的 getUserobservable
方法。如果存在并且 isSignedIn
属性为 true
,则批准路由激活,用户可以访问;否则,路由访问将失败,并返回到主页组件。
auth-interceptor.ts
[...]
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
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
时发出的 REST 调用。我们在这里要做的是尝试检索令牌。如果用户已登录并且拥有有效令牌,那么我们可以添加一个包含 JWT 的授权标头并发出服务器调用。当调用我们 Web API 项目中的“secrets”端点时,这是必需的。如果令牌不存在,那么任何调用都将像往常一样通过 next 参数的 handle 函数进行。
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;
});
}
主页类非常简单。它订阅了 auth 服务中的 user$
主题。它使用这些属性来控制模板中显示的值,例如为已登录用户显示不同于未认证用户的欢迎消息。
login.component.ts
[...]
loginWithGoogle() {
this.authService.doGoogleSignIn().then(() => {
this.router.navigate(['']);
});
}
[...]
Login
与主页一样,也订阅了 auth 服务。Login
还包含对 auth
服务中的 doGoogleSignin
方法的调用。这是从模板中按钮的点击事件触发的。
secret.component.ts
export 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 网站。
点击 Go to Console 链接。这将带您进入“Welcome to Firebase”屏幕,或者提示您使用您的 Google 帐户登录。(如果您还没有,那么您显然需要注册。)
您可能会看到这样的屏幕
点击 Create a project 按钮。接下来会提示您输入项目名称
输入一个名称,然后按 Continue。接下来,您应该为您的项目创建一个应用程序。按创建新应用程序按钮,该按钮应该看起来像这样
随后会出现一个对话框,您将在其中命名您的应用程序
然后您将看到一个包含配置值的对话框。这些值应插入到 environment.ts 文件中。完成此操作后,您应该单击左侧导航栏中的 Authentication 链接。您应该会看到身份验证的子菜单,有点像这样
点击 Sign-in method。使用此选项为您的应用程序启用 Google 登录方法。
位于 Web 项目“keys”文件夹中的名为“firebase_admin_sdk.json”的文件需要包含您的服务帐户的私钥。所以首先返回到您的 Firebase 项目仪表板。
在左侧菜单中,紧挨着“Project Overview”有一个齿轮图标。点击它,然后转到子菜单 **Project Settings**。在此处,您应该转到 **Service accounts** 选项卡。在这里,您将看到一个名为 **Generate new private key** 的按钮。点击它。生成的文件将包含您应该粘贴到 keys 文件夹下的“firebase_admin_sdk.json”文件中的内容。这样,您的服务器端代码在使用 Firebase SDK 时就可以在 Web API 项目中进行身份验证。
出发吧!
转到项目文件所在的目录,并在命令提示符中执行。
dotnet run
在浏览器中打开并访问您为站点设置的 URL,应该会显示非常普通的首页。
点击登录,然后点击 Login With Google 按钮。这应该会打开一个弹出窗口,带您完成 Google 身份验证过程。其中一个屏幕应该会告知您 Firebase 项目的名称以及您的项目将要访问的信息。成功验证后,您将被带到主页,现在会显示一条不同的消息,使用您从 Google 提供的显示名称。菜单中还应该会显示指向 secret 路由的链接。点击该链接,您应该会看到 secret 组件,它会立即调用您 API 上的 secrets 端点。这应该会取回字符串集合并将它们绑定到列表中。
思考
我在研究本文的各个部分时学到了很多东西,希望我也能帮助您学到一些东西。本教程只是一个关于一些有用库和服务及其使用方法的“入门”指南。它不应被视为保护网站的完整指南。