You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 {}
<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">&copy; 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 21:10:28