本文将向您展示如何创建一个ASP.NET Core Web API项目并使用带有Angular前端的Google Firebase身份验证服务来保护它。我们将创建受保护的API端点,这些端点将允许用户在通过Firebase进行身份验证后进行访问。在Angular前端,我们将使用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拦截器类,以对protected控制器方法进行安全的REST调用。
- 我们将创建一个简单的UI。
上面的Github链接将包含我们正在本文中审阅的所有代码:
- .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 App实例。该实例将由应用程序用来调用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服务器进行实时客户端更新。
该控制器保存将从客户端应用程序调用的端点。
[...]
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对象的默认实例(在启动类中初始化的实例)。然后,我们调用一个方法来针对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!!"
};
}
该控制器的secret端点是一个简单的方法,它将返回string的集合。我们已经添加了Authorize属性,以使用我们的身份验证服务来保护此端点。由于我们使用的是JWT auth机制,因此我们将使客户端应用程序将Firebase检索并验证的Bearer令牌添加到HTTP请求的授权标头中。没有或带有错误令牌的任何调用将收到403禁止错误。
让我们回到命令提示符启动Angular应用程序。从.csproj文件所在的目录开始。首先,让我们获取Angular CLI工具:
npm install -g angular/cli
让我们创建Angular应用程序。如果询问,对路由选项说是。
ng new client-app
创建应用程序输出的文件夹。这是我们在Web API项目的启动类中设置为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。
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模块的,我们现在来看一下。
export const environment = {
production: false,
firebaseConfig : {
apiKey: "",
authDomain: "",
databaseURL: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: ""
}
};
在environment类上,我们创建了一个firebaseConfig属性。该对象是初始化AngularFire模块所需的config对象。目前,我们拥有将从Firebase项目检索的值的占位符。
[...]
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'login', component: LoginComponent },
{ path: 'secret', component: SecretComponent, canActivate: [AuthGuardGuard] },
{ path: '**', component: HomeComponent }
];
[...]
路由模块文件中的这一部分是我们放置Angular路由的地方。每个路由都定义有一个组件,该组件在激活路由时将被利用。注意secret 路由。这是我们应用程序的protected路由。为了完成确定用户是否可以访问的工作,我们提供了防护类AuthGuardGuard(我知道,另一个坏名字)。
此类是大多数繁重任务发生的地方。我们将分别查看每个函数并讨论发生了什么:
[...]
user$: BehaviorSubject = new BehaviorSubject(new CurrentUser());
constructor(private angularAuth: AngularFireAuth, private httpclient: HttpClient) {
this.angularAuth.authState.subscribe((firebaseUser) => {
this.configureAuthState(firebaseUser);
});
}
[...]
该类从BehaviorSubject命名为user$开始。只要更改当前用户的状态,此主题就会触发。在构造函数中,我们将此服务订阅到angularAuth对象的Observable authState属性。每当用户成功登录或注销时,此值将发送到configureAuthState函数:
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对象,然后添加作用域对象,以使Google可以将授权后的项目通知用户。在这种情况下,它将创建一个弹出窗口,它将启动Google身份验证过程。成功后,它将关闭,并且焦点将返回到我们的网站。此时,angularAuth.authState可观察对象将触发继续在我们网站上进行此auth过程。
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));
}
这些其他的都非常容易。注销将用户从Firebase项目中注销。GetUserobservable检索用户对象为可观察对象。这将在guard类中使用。获取令牌从本地存储中检索JWT。拦截器将使用它。最后,getUsersecrets是一个调用我们的protected API端点的函数。
[...]
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函数。此函数将从auth服务调用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”端点时,这是必要的。如果令牌不存在,则将使用下一个参数的handle函数正常进行任何调用。
[...]
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类很简单。它从auth服务订阅user$主题。它使用这些属性来控制模板上显示的值,例如对登录用户显示与未认证用户不同的欢迎消息。
[...]
loginWithGoogle() {
this.authService.doGoogleSignIn().then(() => {
this.router.navigate(['']);
});
}
[...]
登录名与home有相同的身份验证服务订阅。登录还包含对auth服务doGoogleSignin方法的调用。这是从模板中的按钮click事件触发的。
export class SecretComponent implements OnInit {
secrets: string[] = [];
constructor(private authService: AuthServiceService) { }
ngOnInit() {
this.authService.getUserSecrets().subscribe(secretData => { this.secrets = secretData });
}
}
这是由路由防护器保护的组件。在其中,我们要做的就是调用API的secret端点。如果一切正常,拦截器应使用有效的授权标头重写请求,然后我们应取回数据。
这涵盖了代码,但是在某些文件和配置中仍然需要填写一些值。要获得这些值,您需要创建Firebase项目。我们不会深入到如何执行此过程,但这应该是一个很好的起点。
首先,让我们进入网站https://firebase.google.com。
单击转到控制台链接。这将带您进入“Welcome to Firebase”屏幕,或提示您使用Google帐户登录。(如果没有,那么您显然需要注册。)
您可能会看到这样的屏幕:
单击创建项目按钮。接下来,将提示您输入项目名称:
输入一个,然后点击继续。接下来,您应该为您的项目创建一个应用程序。点击创建新的应用程序按钮,其外观应类似于:
https://secureservercdn.net/198.71.233.254/g9k.331.myftpupload.com/wp-content/uploads/2020/02/start_create_new_app.png
接下来是一个对话框,您将在其中命名应用程序:
然后将显示一个对话框,其中将包含配置值。这些值是应该插入到envionment.ts文件中的值。完成此操作后,您应该单击左侧导航中的“身份验证”链接。您应该看到用于身份验证的子菜单,例如:
点击登录方法。使用此功能为您的应用程序启用Google登录方法。
位于Web项目的名为“firebase_admin_sdk.json”的“keys”文件夹中的文件需要具有服务帐户的私钥。所以首先回到您的Firebase项目的仪表板。
在项目概述旁边的左侧菜单上是一个齿轮。按下并进入子菜单,项目设置。进入这里后,您应该转到“服务帐户”标签。在这里,您将获得一个按钮,上面显示了Generate new private key。点击它。将要生成的文件将包含您应粘贴在keys文件夹中的“firebase_admin_sdk.json”内部的内容。这样,您使用Firebase SDK的服务器端代码就可以在Web API项目中对自己进行身份验证。
运行起来!转到项目文件所在的目录,然后在命令提示符下执行。
dotnet run
打开浏览器并转到您为网站设置的URL,应该会弹出非常单调的主页。
单击登录,然后单击“使用Google登录”按钮。这应该会打开一个弹出窗口,将引导您完成Google身份验证过程。屏幕之一应告知您Firebase项目的名称以及项目将要访问的信息。成功通过身份验证后,您将被带到主页,该主页现在将使用Google提供的显示名称显示另一条消息。同样在菜单中,应该显示到秘密路径的链接。单击该链接,您应该看到secret组件,该组件将立即调用您API上的secret端点。这应该带回字符串的集合并将它们绑定到列表。
在研究本文的不同部分时,我学到了很多东西,希望也能帮助您学习一些东西。本教程只是一些有用的库和服务以及如何使用它们的“入门”指南。不应将其作为确保网站安全的完整指南。
有用的信息- Firebase Documentation
- Angular Documentation
- .NET Firebase Admin SDK
https://www.codeproject.com/Articles/5259401/Securing-a-Website-Using-Firebase-Angular-8-and-AS