Spring Boot + Angular 实现安全登录注册系统:全栈开发指南
引言:现代Web应用认证的重要性
在当今数字化时代,用户认证是Web应用的基石。无论是电商平台、社交媒体还是企业系统,安全的登录注册功能都至关重要。本文将手把手教你使用Spring Boot作为后端、Angular作为前端,构建一个完整的登录注册系统。
系统整体架构设计
我们的系统采用经典的前后端分离架构:
graph TDsubgraph Frontend[Angular前端]A[登录组件] -->|调用| B[认证服务]C[注册组件] -->|调用| BD[路由守卫] -->|保护| E[受保护路由]F[HTTP拦截器] -->|添加Token| G[HTTP请求]endsubgraph Backend[Spring Boot后端]H[认证控制器] -->|处理| I[注册端点]H -->|处理| J[登录端点]K[安全配置] -->|保护| L[受保护API]M[JWT工具] -->|生成/验证| N[认证]O[用户仓库] -->|数据操作| P[数据库]endFrontend -->|HTTP API调用| Backend
架构核心组件:
- 前端:Angular应用,包含登录/注册组件、认证服务和路由守卫
- 后端:Spring Boot应用,提供REST API,处理认证和用户管理
- 通信:HTTPS协议,JSON数据格式
- 认证:基于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 {@Autowiredprivate UserDetailsServiceImpl userDetailsService;@Autowiredprivate JwtUtil jwtUtil;@Overrideprotected 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();}@Beanpublic 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);}));}
}
系统数据流分析
登录流程
注册流程
安全架构设计
安全措施:
- 密码安全:BCrypt强哈希算法存储密码
- 传输安全:强制使用HTTPS
- 令牌安全:
- JWT设置合理有效期(建议2小时)
- 使用强密钥(256位以上)
- 跨域控制:严格的白名单策略
- 输入验证:前后端双重验证
部署架构
部署要点:
- 使用Nginx作为反向代理和静态文件服务器
- Spring Boot应用使用内嵌Tomcat
- 数据库主从复制提高可用性
- 使用环境变量管理敏感信息
- 配置监控和日志系统
总结与扩展
我们实现了一个完整的登录注册系统,具有以下特点:
✅ 前后端分离架构
✅ JWT无状态认证
✅ 响应式表单验证
✅ 路由级权限控制
✅ 多层安全防护
扩展方向:
- 添加社交登录(OAuth2)
- 实现双因素认证
- 集成短信/邮箱验证
- 添加RBAC权限管理系统
- 实现密码重置功能
项目源码
GitHub - Spring Boot后端
GitHub - Angular前端
通过本文,你应该已经掌握了使用Spring Boot和Angular构建登录注册系统的核心知识和技能。这个架构不仅适用于登录注册功能,还可以作为任何需要用户认证的Web应用的基础。