Spring Boot + Angular 实现安全登录注册系统:全栈开发指南

发布于:2025-08-10 ⋅ 阅读:(54) ⋅ 点赞:(0)

引言:现代Web应用认证的重要性

在当今数字化时代,用户认证是Web应用的基石。无论是电商平台、社交媒体还是企业系统,安全的登录注册功能都至关重要。本文将手把手教你使用Spring Boot作为后端、Angular作为前端,构建一个完整的登录注册系统。

系统整体架构设计

我们的系统采用经典的前后端分离架构:

graph TD
    subgraph Frontend[Angular前端]
        A[登录组件] -->|调用| B[认证服务]
        C[注册组件] -->|调用| B
        D[路由守卫] -->|保护| E[受保护路由]
        F[HTTP拦截器] -->|添加Token| G[HTTP请求]
    end
    
    subgraph Backend[Spring Boot后端]
        H[认证控制器] -->|处理| I[注册端点]
        H -->|处理| J[登录端点]
        K[安全配置] -->|保护| L[受保护API]
        M[JWT工具] -->|生成/验证| N[认证]
        O[用户仓库] -->|数据操作| P[数据库]
    end
    
    Frontend -->|HTTP API调用| Backend

架构核心组件:

  1. 前端:Angular应用,包含登录/注册组件、认证服务和路由守卫
  2. 后端:Spring Boot应用,提供REST API,处理认证和用户管理
  3. 通信:HTTPS协议,JSON数据格式
  4. 认证:基于JWT(JSON Web Token)的无状态认证机制

后端实现:Spring Boot安全认证

技术栈

  • Spring Security
  • Spring Data JPA
  • JJWT库
  • H2数据库(开发环境)
  • Lombok

关键代码实现

1. 用户实体类
@Entity
@Data
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true)
    private String email;
    
    private String password;
    private String name;
    private String role = "USER";
    private LocalDateTime createdAt = LocalDateTime.now();
}
2. JWT工具类
@Component
public class JwtUtil {
    private final String SECRET_KEY = "your-strong-secret-key-here";
    private final long EXPIRATION_MS = 10 * 60 * 60 * 1000; // 10小时
    
    public String generateToken(UserDetails userDetails) {
        return Jwts.builder()
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }
    
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}
3. 安全配置
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/api/auth/**", "/h2-console/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .addFilter(new JwtAuthenticationFilter(authenticationManager(), jwtUtil))
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        
        // 允许H2控制台的帧访问
        http.headers().frameOptions().disable();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
4. 认证控制器
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    @PostMapping("/register")
    public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest request) {
        if (userRepository.existsByEmail(request.getEmail())) {
            return ResponseEntity.badRequest().body(
                Map.of("message", "Email already exists")
            );
        }
        
        User user = new User();
        user.setEmail(request.getEmail());
        user.setName(request.getName());
        user.setPassword(passwordEncoder.encode(request.getPassword()));
        userRepository.save(user);
        
        return ResponseEntity.ok(Map.of("message", "User registered successfully"));
    }
    
    @PostMapping("/login")
    public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) {
        try {
            Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    request.getEmail(),
                    request.getPassword()
                )
            );
            
            UserDetails userDetails = (UserDetails) authentication.getPrincipal();
            String jwt = jwtUtil.generateToken(userDetails);
            
            return ResponseEntity.ok(new AuthResponse(jwt));
        } catch (BadCredentialsException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
                Map.of("message", "Invalid credentials")
            );
        }
    }
}

前端实现:Angular认证系统

项目结构

src/
├── app/
│   ├── core/
│   │   ├── guards/          # 路由守卫
│   │   ├── interceptors/     # HTTP拦截器
│   │   └── services/         # 核心服务
│   ├── modules/
│   │   ├── auth/             # 认证模块
│   │   └── dashboard/        # 主应用模块
│   ├── shared/               # 共享模块
│   ├── app-routing.module.ts # 路由配置
│   └── app.module.ts         # 主模块

关键组件实现

1. 认证服务
@Injectable({ providedIn: 'root' })
export class AuthService {
  private readonly 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));
    }
  }

  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']);
  }
}
2. 登录组件
@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]
    });
  }

  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 || '/dashboard');
      },
      error: (err) => {
        this.errorMessage = '登录失败,请检查您的凭据';
        this.isLoading = false;
      }
    });
  }
}
3. 登录组件模板
<div class="login-container">
  <mat-card class="login-card">
    <mat-card-header>
      <mat-card-title>欢迎回来</mat-card-title>
      <mat-card-subtitle>请登录您的账户</mat-card-subtitle>
    </mat-card-header>

    <mat-card-content>
      <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
        <mat-form-field appearance="outline">
          <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-form-field>

        <mat-form-field appearance="outline">
          <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-form-field>

        <button 
          mat-raised-button 
          color="primary" 
          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>
</div>
4. HTTP拦截器
@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);
      })
    );
  }
}

系统数据流分析

登录流程

User Angular SpringBoot Database 输入凭据并提交 POST /api/auth/login 查询用户 返回用户数据 生成JWT 200 OK (含JWT) 存储Token 跳转到主页 401 Unauthorized 显示错误 alt [认证成功] [认证失败] User Angular SpringBoot Database

注册流程

User Angular SpringBoot Database 填写注册表单 POST /api/auth/register 检查邮箱唯一性 密码加密 保存用户 保存成功 200 OK 显示成功消息 400 Bad Request 显示错误 alt [邮箱可用] [邮箱已存在] User Angular SpringBoot Database

安全架构设计

HTTPS
Angular前端
Spring Boot后端
Spring Security
JWT认证
Token生成
Token验证
密码加密
BCrypt
CORS配置
CSRF防护

安全措施:

  1. 密码安全:BCrypt强哈希算法存储密码
  2. 传输安全:强制使用HTTPS
  3. 令牌安全
    • JWT设置合理有效期(建议2小时)
    • 使用强密钥(256位以上)
  4. 跨域控制:严格的白名单策略
  5. 输入验证:前后端双重验证

部署架构

HTTPS
静态文件
API代理
用户
Nginx
Angular应用
Spring Boot应用
MySQL数据库集群

部署要点:

  1. 使用Nginx作为反向代理和静态文件服务器
  2. Spring Boot应用使用内嵌Tomcat
  3. 数据库主从复制提高可用性
  4. 使用环境变量管理敏感信息
  5. 配置监控和日志系统

总结与扩展

我们实现了一个完整的登录注册系统,具有以下特点:

✅ 前后端分离架构
✅ JWT无状态认证
✅ 响应式表单验证
✅ 路由级权限控制
✅ 多层安全防护

扩展方向:

  1. 添加社交登录(OAuth2)
  2. 实现双因素认证
  3. 集成短信/邮箱验证
  4. 添加RBAC权限管理系统
  5. 实现密码重置功能

项目源码
GitHub - Spring Boot后端
GitHub - Angular前端

通过本文,你应该已经掌握了使用Spring Boot和Angular构建登录注册系统的核心知识和技能。这个架构不仅适用于登录注册功能,还可以作为任何需要用户认证的Web应用的基础。


网站公告

今日签到

点亮在社区的每一天
去签到