
免費API深度求索之路:獲取、調用與應用
在客戶端,您有一個供用戶填寫的登錄或注冊表單。然后,客戶端將用戶的憑據發送到服務器。服務器驗證憑據并為用戶創建新會話。它發回身份驗證令牌或會話 ID。
客戶端將此 ID 或令牌存儲在前端。然后,它會隨客戶端向服務器發出的每個請求將其發送回去。這就是服務器識別哪個用戶正在發出請求以及資源是否允許傳入請求的方式。
服務器負責生成會話 ID 或身份驗證令牌。另一方面,客戶端負責安全地存儲它。如果泄露,此身份驗證令牌或會話 ID 可能會在您的身份驗證工作流程中造成漏洞。然后,攻擊者可以利用它使用合法用戶的帳戶侵入您的應用程序。
此外,攻擊者可以竊取用戶的所有憑據,訪問私有資源并篡改用戶的數據。由于您的服務器識別出該活動來自合法用戶,因此您和用戶都不會對此情況感到驚慌。這可能會對您的產品、用戶和業務造成很大損害。
但是在什么情況下此令牌或 ID 可能會泄露?讓我們討論一下 Angular 在服務器端和客戶端上身份驗證失敗的幾種情況。
當用戶通過注銷結束會話時,服務器應刪除該會話 ID 或身份驗證令牌。然后,下次用戶創建新會話時,服務器還應生成一個全新的會話 ID。
然而,很多時候服務器只是更新數據庫中的會話標志以注銷用戶。因此,它會對較新的會話使用相同的會話 ID。這種做法可能會有問題。如果攻擊者或第三方之前見過您的會話 ID,即使您結束會話,他們也可以使用相同的會話 ID 劫持您的帳戶。
因此,服務器絕不應該對新會話使用舊的會話 ID。相反,它應該為每個新會話創建一個新的會話 ID 或身份驗證令牌。此外,服務器應該為每個會話輪換會話 ID。換句話說,會話 ID 應該有一個TTL。在 TTL 過期后,應該終止或重新創建會話。這很有用,因為服務器不需要完全依賴客戶端來終止會話。
現在您已經了解了一些服務器端漏洞以及如何應對它們,接下來讓我們來討論客戶端漏洞。
您的大多數客戶端代碼都可在瀏覽器中使用。因此,至關重要的是,您要以某種方式構建您的系統,以便在源代碼的全局變量暴露時,系統仍能保持完整。
您應該遵循的最佳做法之一是確保會話 ID 不會被任何人輕易看到或訪問。首先,您不應該在前端 URL 中存儲或附加身份驗證令牌或會話 ID。
您應該在前端實現會話管理,使會話 ID 遠離 UI。為此,您可以將會話 ID 存儲在瀏覽器存儲中,而不是路由參數中。
讓我們看看如何在 Angular 應用程序中實現上述實現。結果是我們應該對 Angular 破壞身份驗證有一些防御措施。
首先,我們將創建一個帶有路由的新 Angular 應用:
ng new my-app
接下來,我們將創建兩個組件:主頁組件和登錄組件。
ng g c home login
然后我們將為這些組件配置路由。轉到app-routing.module.ts文件并添加以下內容:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { HomeComponent } from './home/home.component';
const routes: Routes = [
{ path:"", component:LoginComponent},
{ path:"home", component:HomeComponent},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
接下來,我們將創建一個模擬登錄 API 操作的服務。
ng g s auth
在此服務中,我們將有一個函數,它簡單地向我們返回一個模擬會話 ID。auth.service.ts 文件應如下所示:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class AuthService {
baseURL: string = "https://random-data-api.com/api/users/random_user";
constructor(private http: HttpClient) { }
getAuthUser(): Observable<any> {
return this.http.get(this.baseURL)
}
}
我們的服務有一個getAuthUser函數,它只是向模擬 API 發出 HTTP 請求,該 API 將返回一些數據。在這些數據中,我們將獲得有關用戶和id屬性的一些信息。出于本教程的目的,我們假設此id是用戶的會話 ID。明白了嗎?
讓我們回到我們的登錄組件。在login.component.html文件中,我有一個簡單的標記來呈現登錄表單。
<section>
<h3>Login Page</h3>
<form>
<div class="mb-3">
<label for="exampleInputEmail1" class="form-label">Email address</label>
<input type="email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp">
<div id="emailHelp" class="form-text">We'll never share your email with anyone else.</div>
</div>
<div class="mb-3">
<label for="exampleInputPassword1" class="form-label">Password</label>
<input type="password" class="form-control" id="exampleInputPassword1">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="exampleCheck1">
<label class="form-check-label" for="exampleCheck1">Remember Me</label>
</div>
<button (click)="login($event)" type="submit" class="btn btn-primary">Submit</button>
</form>
</section>
我在login.component.css文件中也添加了一些樣式,如下所示:
section{
margin: 20px auto;
max-width: 400px;
}
section h3{
margin: 20px 0;
}
這應該使登錄頁面看起來如下所示:
我使用 Bootstrap 快速將一些樣式引入到我的模板中。
在login.component.ts文件中,我只需從我們的Auth服務中調用getAuthUser函數并以編程方式路由到 home 組件。我還將從getAuthUser函數收到的id作為查詢參數傳遞給 home 組件。
login.component.ts文件如下所示:
import { Component, OnInit } from '@angular/core';
import { AuthService } from '../auth.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
constructor(private AuthService:AuthService, private router: Router) { }
ngOnInit(): void {
}
login(e:any){
e.preventDefault()
this.AuthService.getAuthUser().subscribe(data=>
this.router.navigate(['/home'],{queryParams:{session_id:data.id}})
)
}
}
如果您注意到模板,當從模板單擊登錄按鈕時,將調用上面創建的登錄函數。
Home 組件的 HTML 文件有一個簡單的模板,如下所示:
<section>
<h3>This is the home page</h3>
<p>Session ID of user: {{sessionId}}</p>
</section>
現在,我們在/home URL 中獲取sessionId作為查詢參數。我們將從路由中獲取它并將其附加到我們組件的狀態變量中。以下是home.component.ts文件應有的樣子:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
sessionId:any;
constructor(private route:ActivatedRoute) { }
ngOnInit(): void {
this.route.queryParams.subscribe(params=>this.sessionId=params['session_id'])
}
}
如果您現在單擊登錄頁面上的登錄按鈕,您將獲得以下內容:
如您所見,URL 中的查詢參數清楚地顯示了用戶的會話 ID。但正如我之前提到的,這不是一個好方法,可能會導致 Angular 在您的應用程序中破壞身份驗證。如果用戶在公共計算機上使用您的應用程序,在主頁上,然后忘記注銷,該怎么辦?
或者,如果有人只是偷看了您的用戶的計算機并看到前端 URL 上清晰可見的會話 ID,會怎么樣?
我們不必在前端路由中傳遞會話 ID,而是可以將其推送到瀏覽器存儲(如本地存儲或會話存儲)中。
更新后的登錄功能應如下所示:
login(e:any){
e.preventDefault()
this.AuthService.getAuthUser().subscribe(data=>{
// this.router.navigate(['/home'],{queryParams:{session_id:data.id}})
localStorage.setItem('session_id',data.id);
this.router.navigateByUrl('/home')
})
}
然后,在我們的主組件中,我們可以從localStorage中獲取會話 ID :
ngOnInit(): void {
//this.route.queryParams.subscribe(params=>this.sessionId=params['session_id'])
this.sessionId=localStorage.getItem('session_id')
}
現在,如果您返回/login并再次按登錄按鈕,您仍然應該在/home頁面上看到會話 ID,但它不應該出現在您的 URL 中:
您可以通過在 Angular 應用中使用 NgRX 的全局狀態來進一步改進此實現。
我們已經了解了會話管理如何幫助您預防身份驗證漏洞。但是,您可以實施預防措施來降低因 Angular 身份驗證失敗而遭受損害的可能性。這個想法很簡單:如果您創建一個萬無一失的身份驗證系統,那么您已經可以預防應用程序中未來出現身份驗證漏洞了!
首先,您應該在他們的系統中實現密碼強度驗證。核心功能將在后端實現,但如果您希望更快地交付此功能,您也可以使用這樣的客戶端庫。
其次,您還應該為空閑用戶實現自動退出功能。這對應于用戶忘記從應用程序退出以及在公共計算機上關閉應用程序窗口的用例。
您可以檢測用戶是否閑置了超過指定的時間,并自動向服務器發送會話終止或注銷請求。這里有一個Angular庫,可以方便地實現這一點。
防止 Angular 身份驗證失敗的第一步始終是實現萬無一失的身份驗證。在服務器端實現這些身份驗證并讓 Angular 客戶端在需要時與這些服務交互更安全、更好。這是因為前端檢查、驗證和障礙更容易繞過。
文章來源:Angular Broken Authentication Guide: Examples and Prevention