Angular认证守卫实现求助:会话Token有效性校验逻辑困惑
实现带Token有效性校验的Angular认证守卫
我正在尝试为登录组件实现一个认证守卫(Authentication guard),该守卫仅在以下条件满足时允许用户访问仪表盘:
- 本地存储中存在会话Token
- 通过GET请求校验该Token的有效性
若Token无效,需删除本地存储中的Token并跳转至登录页。
以下是我目前完成的代码:
core/guard/auth.guard.ts
import { Injectable } from '@angular/core'; import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from '@angular/router'; import { Observable } from 'rxjs'; import {AuthService} from "../services/auth.service"; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { constructor( private service: AuthService, private router:Router) { } canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { if(this.service.isLoggedIn()){ return true; }else { return this.router.navigate(["login"]); } } }
app.module.ts
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { StoreModule } from '@ngrx/store'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { KpiComponent } from './kpi/kpi.component'; import { MatCardModule } from '@angular/material/card'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { PieComponent } from './kpi/pie/pie.component'; import { BarComponent } from './kpi/bar/bar.component'; import { MainTemplateComponent } from './atomic/main-template/main-template.component'; import { LoginPageComponent } from './atomic/organisms/login-page/login-page.component'; import { DashboardPageComponent } from './atomic/organisms/dashboard-page/dashboard-page.component'; import { NavbarComponent } from './navbar/navbar.component'; import { MatMenuModule } from '@angular/material/menu'; import { ReactiveFormsModule } from '@angular/forms'; import { reducers } from './store'; import { HrMaterialModule } from './material/material.module'; @NgModule({ declarations: [ AppComponent, KpiComponent, PieComponent, BarComponent, MainTemplateComponent, LoginPageComponent, DashboardPageComponent, NavbarComponent, ], imports: [ BrowserModule, AppRoutingModule, StoreModule.forRoot(reducers, {}), StoreDevtoolsModule.instrument(), BrowserAnimationsModule, MatCardModule, MatButtonModule, MatIconModule, ReactiveFormsModule, MatMenuModule, HrMaterialModule, ], providers: [], bootstrap: [AppComponent], }) export class AppModule {}
navbar.component.html
<div class="navbar"> <a routerLink="/dashboard"> <img src="../../assets/imbus-logo.svg" alt="imbus-logo"> </a> <div class="navbar-elements"> <div class="user-elements"> <a routerLink="/dashboard" class="account-icon" [matMenuTriggerFor]="menu"> <img src="../../assets/manage-account.svg" alt=""> </a> <mat-menu #menu="matMenu" xPosition="before"> <button mat-menu-item> <mat-icon>settings</mat-icon> <span>My Settings</span> </button> <button mat-menu-item routerLink="/login"> <mat-icon>keyboard_backspace</mat-icon> <span>Logout</span> </button> </mat-menu> </div> <a routerLink="/dashboard" class="settings-icon"> <img src="../../assets/setttings.svg" alt=""> </a> </div> </div>
login-page.component.html
<div class="container"> <div class="screen"> <div class="screen-content"> <form [formGroup]="loginForm" (ngSubmit)="loginUser()" class="login"> <div class="logo"> <img src="../../../../assets/imbus-logo.svg" alt="imbus-logo" /> </div> <div class="user-field"> <input #email formControlName="email" type="text" class="login-input" placeholder="User login" /> <br /> <span *ngIf="user && user.invalid && user.touched" style="color: red" >User name is required.</span > </div> <div class="password-field"> <input formControlName="password" type="{{ type }}" class="login-input" placeholder="Password" /> <mat-icon (click)="togglePassword($event)" *ngIf="showPassword" svgIcon="hr:hide-text" class="hide-icon" ></mat-icon> <mat-icon (click)="togglePassword($event)" *ngIf="!showPassword" svgIcon="hr:show-text" class="show-icon" ></mat-icon> <span *ngIf="password && password.invalid && password.touched" style="color: red" > Password is required.</span > </div> <button [disabled]="loginForm.invalid" class="button login-submit"> <span class="button-text" (click)="proceedlogin(name.value)">LogIn</span> </button> </form> <p id="copy-rights-text" class="copy-right">© IMBUS HR DASHBOARD 2023</p> </div> </div> </div>
login-page.component.ts
import { Component, OnInit } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; import {Router} from "@angular/router"; import {AuthService} from "../../../core/services/auth.service"; @Component({ selector: 'app-login-page', templateUrl: './login-page.component.html', styleUrls: ['./login-page.component.scss'], }) export class LoginPageComponent implements OnInit { public showPassword = false; public type = 'password'; constructor( private service: AuthService, private router: Router) { localStorage.clear(); } loginForm = new FormGroup({ email: new FormControl('', [Validators.required]), password: new FormControl('', [ Validators.required, Validators.minLength(8), ]), }); loginUser() { console.warn(this.loginForm.value); if (this.loginForm.valid) { console.log('Form Submitted!', this.loginForm.value); } } ngOnInit():void {} proceedlogin(email:any){ localStorage.setItem("user", email); this.router.navigate(["/dashboard"]) } get user() { return this.loginForm.get('user'); } get password() { return this.loginForm.get('password'); } togglePassword(e: Event) { e.stopPropagation(); e.preventDefault(); this.showPassword = !this.showPassword; this.type = this.showPassword ? 'text' : 'password'; } }
core/service/authservice.ts
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class AuthService { constructor() {} isLoggedIn(){ return localStorage.getItem("user")!=null; } }
解决方案:实现Token有效性校验逻辑
步骤1:完善AuthService,添加Token校验与管理逻辑
修改core/service/authservice.ts,加入HTTP请求校验Token,并调整存储逻辑为Token而非用户名:
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable, of } from 'rxjs'; import { map, catchError } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class AuthService { private validateTokenUrl = 'http://your-api-domain/auth/validate'; // 替换为实际后端校验接口 private loginUrl = 'http://your-api-domain/auth/login'; // 替换为实际后端登录接口 constructor(private http: HttpClient) {} // 存储Token到本地存储 setAuthToken(token: string): void { localStorage.setItem('authToken', token); } // 获取本地存储的Token getAuthToken(): string | null { return localStorage.getItem('authToken'); } // 清除本地存储的Token clearAuthToken(): void { localStorage.removeItem('authToken'); } // 调用后端接口校验Token有效性 validateToken(): Observable<boolean> { const token = this.getAuthToken(); if (!token) return of(false); const headers = new HttpHeaders({ 'Authorization': `Bearer ${token}` }); return this.http.get<{ valid: boolean }>(this.validateTokenUrl, { headers }).pipe( map(res => res.valid), catchError(() => { this.clearAuthToken(); return of(false); }) ); } // 发起登录请求,获取Token login(email: string, password: string): Observable<string> { return this.http.post<{ token: string }>(this.loginUrl, { email, password }).pipe( map(res => res.token) ); } }
步骤2:修改Login组件,对接后端登录接口
调整login-page.component.ts,替换本地存储用户名为获取并存储Token:
import { Component, OnInit } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; import { Router } from "@angular/router"; import { AuthService } from "../../../core/services/auth.service"; @Component({ selector: 'app-login-page', templateUrl: './login-page.component.html', styleUrls: ['./login-page.component.scss'], }) export class LoginPageComponent implements OnInit { public showPassword = false; public type = 'password'; constructor( private authService: AuthService, private router: Router ) { this.authService.clearAuthToken(); } loginForm = new FormGroup({ email: new FormControl('', [Validators.required]), password: new FormControl('', [ Validators.required, Validators.minLength(8), ]), }); loginUser() { if (this.loginForm.invalid) return; const { email, password } = this.loginForm.value; this.authService.login(email!, password!).subscribe({ next: (token) => { this.authService.setAuthToken(token); this.router.navigate(["/dashboard"]); }, error: () => alert('登录失败,请检查账号密码') }); } ngOnInit():void {} get user() { return this.loginForm.get('email'); // 修正字段引用 } get password() { return this.loginForm.get('password'); } togglePassword(e: Event) { e.stopPropagation(); e.preventDefault(); this.showPassword = !this.showPassword; this.type = this.showPassword ? 'text' : 'password'; } }
同时修改login-page.component.html中的按钮,移除多余的proceedlogin调用:
<button [disabled]="loginForm.invalid" class="button login-submit" type="submit"> <span class="button-text">LogIn</span> </button>
步骤3:更新AuthGuard,实现异步Token校验
修改core/guard/auth.guard.ts,调用AuthService的校验方法并处理异步结果:
import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { AuthService } from "../services/auth.service"; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { constructor( private authService: AuthService, private router: Router ) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<boolean | UrlTree> { // 先检查本地是否存在Token if (!this.authService.getAuthToken()) { return this.router.navigate(['/login']); } // 调用后端校验Token有效性 return this.authService.validateToken().pipe( map(isValid => isValid ? true : this.router.navigate(['/login'])) ); } }
步骤4:在AppModule中导入HttpClientModule
在app.module.ts的imports数组中添加HttpClientModule,才能发起HTTP请求:
import { HttpClientModule } from '@angular/common/http'; // ... @NgModule({ // ... imports: [ BrowserModule, HttpClientModule, // 添加此行 AppRoutingModule, // ... 其他模块 ], // ... }) export class AppModule {}
补充说明
- 请将代码中的接口地址替换为你的实际后端接口
- 后端校验接口需要接收
Authorization: Bearer <token>请求头,并返回{ valid: boolean }格式的响应 - 登出功能可在Navbar的Logout按钮点击时调用
this.authService.clearAuthToken(),再跳转登录页
内容的提问来源于stack exchange,提问作者Ami Smith
相关产品推荐
相关产品推荐

