登录注册前端详细实现 (Angular 15+)

1. 项目结构

src/
├── app/
│   ├── core/
│   │   ├── guards/
│   │   │   ├── auth.guard.ts
│   │   │   └── role.guard.ts
│   │   ├── interceptors/
│   │   │   └── auth.interceptor.ts
│   │   └── services/
│   │       ├── auth.service.ts
│   │       └── token.service.ts
│   ├── modules/
│   │   ├── auth/
│   │   │   ├── login/
│   │   │   │   ├── login.component.ts
│   │   │   │   ├── login.component.html
│   │   │   │   └── login.component.scss
│   │   │   ├── register/
│   │   │   │   ├── register.component.ts
│   │   │   │   ├── register.component.html
│   │   │   │   └── register.component.scss
│   │   │   └── auth-routing.module.ts
│   │   └── dashboard/
│   ├── shared/
│   │   ├── components/
│   │   │   └── error-message/
│   │   ├── models/
│   │   │   ├── user.model.ts
│   │   │   └── api-response.model.ts
│   │   └── validators/
│   │       └── password.validator.ts
│   ├── app-routing.module.ts
│   ├── app.component.ts
│   ├── app.component.html
│   └── app.module.ts
├── environments/

2. 核心服务

auth.service.ts (认证服务):

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, tap } from 'rxjs';
import { Router } from '@angular/router';
import { environment } from '../../../environments/environment';
import { User } from '../shared/models/user.model';
import { TokenService } from './token.service';

@Injectable({ providedIn: 'root' })
export class AuthService {
  private apiUrl = `${environment.apiUrl}/auth`;
  private currentUserSubject = new BehaviorSubject<User | null>(null);
  public currentUser$ = this.currentUserSubject.asObservable();

  constructor(
    private http: HttpClient,
    private tokenService: TokenService,
    private router: Router
  ) {
    const user = localStorage.getItem('currentUser');
    if (user) {
      this.currentUserSubject.next(JSON.parse(user));
    }
  }

  register(userData: {
    email: string;
    password: string;
    name?: string;
  }): Observable<any> {
    return this.http.post(`${this.apiUrl}/register`, userData);
  }

  login(credentials: { email: string; password: string }): Observable<any> {
    return this.http.post<{ token: string }>(`${this.apiUrl}/login`, credentials).pipe(
      tap(response => {
        this.tokenService.setToken(response.token);
        this.fetchCurrentUser();
      })
    );
  }

  fetchCurrentUser(): void {
    this.http.get<User>(`${environment.apiUrl}/users/me`).subscribe({
      next: user => {
        this.currentUserSubject.next(user);
        localStorage.setItem('currentUser', JSON.stringify(user));
      },
      error: () => this.logout()
    });
  }

  logout(): void {
    this.tokenService.removeToken();
    this.currentUserSubject.next(null);
    localStorage.removeItem('currentUser');
    this.router.navigate(['/login']);
  }

  get currentUserValue(): User | null {
    return this.currentUserSubject.value;
  }

  isAuthenticated(): boolean {
    return !!this.tokenService.getToken();
  }
}

token.service.ts (令牌服务):

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class TokenService {
  private readonly TOKEN_KEY = 'auth_token';

  setToken(token: string): void {
    localStorage.setItem(this.TOKEN_KEY, token);
  }

  getToken(): string | null {
    return localStorage.getItem(this.TOKEN_KEY);
  }

  removeToken(): void {
    localStorage.removeItem(this.TOKEN_KEY);
  }

  decodeToken(token: string): any {
    try {
      const base64Url = token.split('.')[1];
      const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
      return JSON.parse(atob(base64));
    } catch (e) {
      return null;
    }
  }
}

3. HTTP拦截器

auth.interceptor.ts:

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpErrorResponse
} from '@angular/common/http';
import { Observable, catchError, throwError } from 'rxjs';
import { Router } from '@angular/router';
import { TokenService } from '../services/token.service';
import { AuthService } from '../services/auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(
    private tokenService: TokenService,
    private authService: AuthService,
    private router: Router
  ) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    const token = this.tokenService.getToken();
    let authReq = request;
    
    if (token) {
      authReq = request.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`
        }
      });
    }

    return next.handle(authReq).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401) {
          this.authService.logout();
          this.router.navigate(['/login'], { queryParams: { expired: true } });
        }
        return throwError(() => error);
      })
    );
  }
}

4. 路由守卫

auth.guard.ts:

import { Injectable } from '@angular/core';
import {
  CanActivate,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  UrlTree,
  Router
} from '@angular/router';
import { Observable } from 'rxjs';
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
  ): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
    
    if (this.authService.isAuthenticated()) {
      return true;
    }

    // 保存目标URL以便登录后重定向
    this.router.navigate(['/login'], {
      queryParams: { returnUrl: state.url }
    });
    return false;
  }
}

5. 登录组件

login.component.ts:

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { AuthService } from '../../core/services/auth.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
  loginForm: FormGroup;
  isLoading = false;
  errorMessage: string | null = null;
  returnUrl: string | null = null;
  showPassword = false;

  constructor(
    private fb: FormBuilder,
    private authService: AuthService,
    private router: Router,
    private route: ActivatedRoute
  ) {
    this.loginForm = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', Validators.required],
      rememberMe: [false]
    });
  }

  ngOnInit(): void {
    this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard';
    
    // 检查是否会话过期
    if (this.route.snapshot.queryParams['expired']) {
      this.errorMessage = '您的会话已过期,请重新登录';
    }
  }

  onSubmit(): void {
    if (this.loginForm.invalid) return;

    this.isLoading = true;
    this.errorMessage = null;

    const { email, password } = this.loginForm.value;

    this.authService.login({ email, password }).subscribe({
      next: () => {
        this.router.navigateByUrl(this.returnUrl!);
      },
      error: (err) => {
        this.errorMessage = err.error?.message || '登录失败,请检查您的凭据';
        this.isLoading = false;
      }
    });
  }

  togglePasswordVisibility(): void {
    this.showPassword = !this.showPassword;
  }
}

login.component.html:

<div class="login-container">
  <mat-card class="login-card">
    <mat-card-header>
      <mat-card-title class="text-center">欢迎回来</mat-card-title>
      <mat-card-subtitle class="text-center">请登录您的账户</mat-card-subtitle>
    </mat-card-header>

    <mat-card-content>
      <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
        <mat-form-field appearance="outline" class="full-width">
          <mat-label>电子邮箱</mat-label>
          <input matInput formControlName="email" type="email">
          <mat-icon matSuffix>mail</mat-icon>
          <mat-error *ngIf="loginForm.get('email')?.hasError('required')">
            邮箱为必填项
          </mat-error>
          <mat-error *ngIf="loginForm.get('email')?.hasError('email')">
            请输入有效的邮箱地址
          </mat-error>
        </mat-form-field>

        <mat-form-field appearance="outline" class="full-width">
          <mat-label>密码</mat-label>
          <input 
            matInput 
            [type]="showPassword ? 'text' : 'password'" 
            formControlName="password"
          >
          <button 
            type="button" 
            mat-icon-button 
            matSuffix
            (click)="togglePasswordVisibility()"
          >
            <mat-icon>{{ showPassword ? 'visibility_off' : 'visibility' }}</mat-icon>
          </button>
          <mat-error *ngIf="loginForm.get('password')?.hasError('required')">
            密码为必填项
          </mat-error>
        </mat-form-field>

        <div class="remember-forgot">
          <mat-checkbox formControlName="rememberMe">记住我</mat-checkbox>
          <a routerLink="/forgot-password">忘记密码?</a>
        </div>

        <app-error-message [message]="errorMessage"></app-error-message>

        <button 
          mat-raised-button 
          color="primary" 
          class="full-width" 
          type="submit"
          [disabled]="loginForm.invalid || isLoading"
        >
          <span *ngIf="!isLoading">登录</span>
          <mat-spinner *ngIf="isLoading" diameter="20"></mat-spinner>
        </button>
      </form>
    </mat-card-content>

    <mat-card-actions class="text-center">
      <p>还没有账户? <a routerLink="/register">立即注册</a></p>
    </mat-card-actions>
  </mat-card>
</div>

login.component.scss:

.login-container {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  padding: 1rem;
}

.login-card {
  width: 100%;
  max-width: 450px;
  padding: 2rem;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
  border-radius: 10px;
}

.full-width {
  width: 100%;
  margin-bottom: 1.5rem;
}

.remember-forgot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 1.5rem;
}

.text-center {
  text-align: center;
}

6. 注册组件

register.component.ts:

import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from '../../core/services/auth.service';
import { passwordValidator } from '../../../shared/validators/password.validator';

@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.scss']
})
export class RegisterComponent {
  registerForm: FormGroup;
  isLoading = false;
  errorMessage: string | null = null;
  successMessage: string | null = null;
  showPassword = false;
  showConfirmPassword = false;

  constructor(
    private fb: FormBuilder,
    private authService: AuthService,
    private router: Router
  ) {
    this.registerForm = this.fb.group({
      name: ['', [Validators.required, Validators.minLength(2)]],
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, passwordValidator()]],
      confirmPassword: ['', Validators.required],
      agreeTerms: [false, Validators.requiredTrue]
    }, {
      validators: this.passwordMatchValidator
    });
  }

  passwordMatchValidator(group: FormGroup) {
    const password = group.get('password')?.value;
    const confirmPassword = group.get('confirmPassword')?.value;
    return password === confirmPassword ? null : { mismatch: true };
  }

  onSubmit(): void {
    if (this.registerForm.invalid) return;

    this.isLoading = true;
    this.errorMessage = null;
    this.successMessage = null;

    const { name, email, password } = this.registerForm.value;

    this.authService.register({ name, email, password }).subscribe({
      next: () => {
        this.successMessage = '注册成功!正在跳转到登录页面...';
        setTimeout(() => {
          this.router.navigate(['/login'], {
            queryParams: { registered: true }
          });
        }, 2000);
      },
      error: (err) => {
        this.errorMessage = err.error?.message || '注册失败,请重试';
        this.isLoading = false;
      }
    });
  }

  togglePasswordVisibility(field: 'password' | 'confirmPassword'): void {
    if (field === 'password') {
      this.showPassword = !this.showPassword;
    } else {
      this.showConfirmPassword = !this.showConfirmPassword;
    }
  }
}

password.validator.ts:

import { AbstractControl, ValidatorFn } from '@angular/forms';

export function passwordValidator(): ValidatorFn {
  return (control: AbstractControl): { [key: string]: any } | null => {
    const value = control.value || '';
    
    if (!value) {
      return null;
    }
    
    const errors: any = {};
    
    // 检查长度
    if (value.length < 8) {
      errors.minLength = { requiredLength: 8 };
    }
    
    // 检查大写字母
    if (!/[A-Z]/.test(value)) {
      errors.missingUpperCase = true;
    }
    
    // 检查数字
    if (!/\d/.test(value)) {
      errors.missingNumber = true;
    }
    
    // 检查特殊字符
    if (!/[!@#$%^&*(),.?":{}|<>]/.test(value)) {
      errors.missingSpecialChar = true;
    }
    
    return Object.keys(errors).length > 0 ? { passwordRequirements: errors } : null;
  };
}

7. 错误消息组件

error-message.component.ts:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-error-message',
  template: `
    <div class="error-message" *ngIf="message">
      <mat-icon>error</mat-icon>
      <span>{{ message }}</span>
    </div>
  `,
  styles: [`
    .error-message {
      display: flex;
      align-items: center;
      color: #f44336;
      background-color: #ffebee;
      padding: 10px 15px;
      border-radius: 4px;
      margin-bottom: 20px;
      font-size: 14px;
      
      mat-icon {
        margin-right: 8px;
        font-size: 18px;
      }
    }
  `]
})
export class ErrorMessageComponent {
  @Input() message: string | null = null;
}

8. 路由配置

auth-routing.module.ts:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';

const routes: Routes = [
  { path: 'login', component: LoginComponent },
  { path: 'register', component: RegisterComponent },
  { path: 'forgot-password', loadChildren: () => import('./forgot-password/forgot-password.module').then(m => m.ForgotPasswordModule) }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class AuthRoutingModule { }

app-routing.module.ts:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './core/guards/auth.guard';

const routes: Routes = [
  { 
    path: '', 
    redirectTo: '/dashboard', 
    pathMatch: 'full' 
  },
  {
    path: 'dashboard',
    loadChildren: () => import('./modules/dashboard/dashboard.module').then(m => m.DashboardModule),
    canActivate: [AuthGuard]
  },
  {
    path: 'auth',
    loadChildren: () => import('./modules/auth/auth.module').then(m => m.AuthModule)
  },
  { 
    path: '**', 
    redirectTo: '/dashboard' 
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, { scrollPositionRestoration: 'top' })],
  exports: [RouterModule]
})
export class AppRoutingModule { }

9. 主模块配置

app.module.ts:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';

import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBarModule } from '@angular/material/snack-bar';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { AuthInterceptor } from './core/interceptors/auth.interceptor';
import { ErrorMessageComponent } from './shared/components/error-message/error-message.component';

@NgModule({
  declarations: [
    AppComponent,
    ErrorMessageComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    HttpClientModule,
    ReactiveFormsModule,
    AppRoutingModule,
    
    // Material Modules
    MatInputModule,
    MatButtonModule,
    MatCardModule,
    MatIconModule,
    MatCheckboxModule,
    MatProgressSpinnerModule,
    MatSnackBarModule
  ],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

10. 环境配置

environment.ts:

export const environment = {
  production: false,
  apiUrl: 'http://localhost:8080/api',
  appName: 'Auth Demo'
};

功能亮点说明

  1. 响应式表单验证
  2. 自定义密码验证器(长度、大小写、数字、特殊字符)
  3. 实时表单错误反馈
  4. 密码匹配验证
  5. 用户体验优化
  6. 密码可见性切换
  7. 加载状态指示器
  8. 错误和成功消息提示
  9. 记住我功能
  10. 登录后重定向
  11. 安全特性
  12. JWT自动附加到请求头
  13. Token过期处理(401错误自动跳转)
  14. 本地存储加密考虑(实际项目应使用更安全的方式)
  15. 状态管理
  16. BehaviorSubject管理用户状态
  17. 自动获取当前用户信息
  18. 本地存储持久化
  19. 路由保护
  20. 未认证用户自动重定向
  21. 登录后返回原始请求页面
  22. 模块懒加载
  23. 错误处理
  24. 统一HTTP错误拦截
  25. 友好的错误消息展示
  26. 表单验证错误提示

使用说明

  1. 安装依赖:
npm install @angular/material @angular/cdk @angular/flex-layout
  1. styles.scss中添加:
@import '@angular/material/prebuilt-themes/indigo-pink.css';
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
  1. 运行应用:
ng serve --port 4200

这个实现提供了完整的认证流程,包括:

  • 用户注册(带密码强度验证)
  • 登录/注销功能
  • JWT令牌管理
  • 路由保护
  • 响应式UI设计
  • 完善的错误处理

实际项目中,你还可以添加:

  • 密码重置功能
  • 双因素认证
  • 社交登录(Google/Facebook)
  • 用户资料管理
  • 权限控制(基于角色)