Spring Boot + Angular 实现安全登录注册系统:全栈开发指南
引言:现代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
架构核心组件:
- 前端:Angular应用,包含登录/注册组件、认证服务和路由守卫
- 后端:Spring Boot应用,提供REST API,处理认证和用户管理
- 通信:HTTPS协议,JSON数据格式
- 认证:基于JWT(JSON Web Token)的无状态认证机制
后端实现:Spring Boot安全认证
技术栈
- Spring Security
- Spring Data JPA
- JJWT库
- H2数据库(开发环境)
- Lombok
关键代码实现
1. 用户实体类
@Entity@Data@NoArgsConstructorpublic 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工具类
@Componentpublic 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@EnableWebSecuritypublic 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); }) ); }}
系统数据流分析
登录流程
#mermaid-svg-I65XXSOpQu8ZqhAb {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-I65XXSOpQu8ZqhAb .error-icon{fill:#552222;}#mermaid-svg-I65XXSOpQu8ZqhAb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-I65XXSOpQu8ZqhAb .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-I65XXSOpQu8ZqhAb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-I65XXSOpQu8ZqhAb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-I65XXSOpQu8ZqhAb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-I65XXSOpQu8ZqhAb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-I65XXSOpQu8ZqhAb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-I65XXSOpQu8ZqhAb .marker.cross{stroke:#333333;}#mermaid-svg-I65XXSOpQu8ZqhAb svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-I65XXSOpQu8ZqhAb .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-I65XXSOpQu8ZqhAb text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-I65XXSOpQu8ZqhAb .actor-line{stroke:grey;}#mermaid-svg-I65XXSOpQu8ZqhAb .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-I65XXSOpQu8ZqhAb .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-I65XXSOpQu8ZqhAb #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-I65XXSOpQu8ZqhAb .sequenceNumber{fill:white;}#mermaid-svg-I65XXSOpQu8ZqhAb #sequencenumber{fill:#333;}#mermaid-svg-I65XXSOpQu8ZqhAb #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-I65XXSOpQu8ZqhAb .messageText{fill:#333;stroke:#333;}#mermaid-svg-I65XXSOpQu8ZqhAb .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-I65XXSOpQu8ZqhAb .labelText,#mermaid-svg-I65XXSOpQu8ZqhAb .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-I65XXSOpQu8ZqhAb .loopText,#mermaid-svg-I65XXSOpQu8ZqhAb .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-I65XXSOpQu8ZqhAb .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-I65XXSOpQu8ZqhAb .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-I65XXSOpQu8ZqhAb .noteText,#mermaid-svg-I65XXSOpQu8ZqhAb .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-I65XXSOpQu8ZqhAb .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-I65XXSOpQu8ZqhAb .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-I65XXSOpQu8ZqhAb .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-I65XXSOpQu8ZqhAb .actorPopupMenu{position:absolute;}#mermaid-svg-I65XXSOpQu8ZqhAb .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-I65XXSOpQu8ZqhAb .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-I65XXSOpQu8ZqhAb .actor-man circle,#mermaid-svg-I65XXSOpQu8ZqhAb line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-I65XXSOpQu8ZqhAb :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} User Angular SpringBoot Database 输入凭据并提交 POST /api/auth/login 查询用户 返回用户数据 生成JWT 200 OK (含JWT) 存储Token 跳转到主页 401 Unauthorized 显示错误 alt [认证成功] [认证失败] User Angular SpringBoot Database
注册流程
#mermaid-svg-co6MUmWLctTKmRUS {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-co6MUmWLctTKmRUS .error-icon{fill:#552222;}#mermaid-svg-co6MUmWLctTKmRUS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-co6MUmWLctTKmRUS .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-co6MUmWLctTKmRUS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-co6MUmWLctTKmRUS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-co6MUmWLctTKmRUS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-co6MUmWLctTKmRUS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-co6MUmWLctTKmRUS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-co6MUmWLctTKmRUS .marker.cross{stroke:#333333;}#mermaid-svg-co6MUmWLctTKmRUS svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-co6MUmWLctTKmRUS .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-co6MUmWLctTKmRUS text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-co6MUmWLctTKmRUS .actor-line{stroke:grey;}#mermaid-svg-co6MUmWLctTKmRUS .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-co6MUmWLctTKmRUS .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-co6MUmWLctTKmRUS #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-co6MUmWLctTKmRUS .sequenceNumber{fill:white;}#mermaid-svg-co6MUmWLctTKmRUS #sequencenumber{fill:#333;}#mermaid-svg-co6MUmWLctTKmRUS #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-co6MUmWLctTKmRUS .messageText{fill:#333;stroke:#333;}#mermaid-svg-co6MUmWLctTKmRUS .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-co6MUmWLctTKmRUS .labelText,#mermaid-svg-co6MUmWLctTKmRUS .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-co6MUmWLctTKmRUS .loopText,#mermaid-svg-co6MUmWLctTKmRUS .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-co6MUmWLctTKmRUS .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-co6MUmWLctTKmRUS .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-co6MUmWLctTKmRUS .noteText,#mermaid-svg-co6MUmWLctTKmRUS .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-co6MUmWLctTKmRUS .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-co6MUmWLctTKmRUS .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-co6MUmWLctTKmRUS .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-co6MUmWLctTKmRUS .actorPopupMenu{position:absolute;}#mermaid-svg-co6MUmWLctTKmRUS .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-co6MUmWLctTKmRUS .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-co6MUmWLctTKmRUS .actor-man circle,#mermaid-svg-co6MUmWLctTKmRUS line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-co6MUmWLctTKmRUS :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} User Angular SpringBoot Database 填写注册表单 POST /api/auth/register 检查邮箱唯一性 密码加密 保存用户 保存成功 200 OK 显示成功消息 400 Bad Request 显示错误 alt [邮箱可用] [邮箱已存在] User Angular SpringBoot Database
安全架构设计
#mermaid-svg-C9q21p632Xpe5eTx {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-C9q21p632Xpe5eTx .error-icon{fill:#552222;}#mermaid-svg-C9q21p632Xpe5eTx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-C9q21p632Xpe5eTx .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-C9q21p632Xpe5eTx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-C9q21p632Xpe5eTx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-C9q21p632Xpe5eTx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-C9q21p632Xpe5eTx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-C9q21p632Xpe5eTx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-C9q21p632Xpe5eTx .marker.cross{stroke:#333333;}#mermaid-svg-C9q21p632Xpe5eTx svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-C9q21p632Xpe5eTx .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-C9q21p632Xpe5eTx .cluster-label text{fill:#333;}#mermaid-svg-C9q21p632Xpe5eTx .cluster-label span{color:#333;}#mermaid-svg-C9q21p632Xpe5eTx .label text,#mermaid-svg-C9q21p632Xpe5eTx span{fill:#333;color:#333;}#mermaid-svg-C9q21p632Xpe5eTx .node rect,#mermaid-svg-C9q21p632Xpe5eTx .node circle,#mermaid-svg-C9q21p632Xpe5eTx .node ellipse,#mermaid-svg-C9q21p632Xpe5eTx .node polygon,#mermaid-svg-C9q21p632Xpe5eTx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-C9q21p632Xpe5eTx .node .label{text-align:center;}#mermaid-svg-C9q21p632Xpe5eTx .node.clickable{cursor:pointer;}#mermaid-svg-C9q21p632Xpe5eTx .arrowheadPath{fill:#333333;}#mermaid-svg-C9q21p632Xpe5eTx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-C9q21p632Xpe5eTx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-C9q21p632Xpe5eTx .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-C9q21p632Xpe5eTx .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-C9q21p632Xpe5eTx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-C9q21p632Xpe5eTx .cluster text{fill:#333;}#mermaid-svg-C9q21p632Xpe5eTx .cluster span{color:#333;}#mermaid-svg-C9q21p632Xpe5eTx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-C9q21p632Xpe5eTx :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} HTTPS Angular前端 Spring Boot后端 Spring Security JWT认证 Token生成 Token验证 密码加密 BCrypt CORS配置 CSRF防护
安全措施:
- 密码安全:BCrypt强哈希算法存储密码
- 传输安全:强制使用HTTPS
- 令牌安全:
- JWT设置合理有效期(建议2小时)
- 使用强密钥(256位以上)
- 跨域控制:严格的白名单策略
- 输入验证:前后端双重验证
部署架构
#mermaid-svg-FTM6tffSE9Hfaxwz {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-FTM6tffSE9Hfaxwz .error-icon{fill:#552222;}#mermaid-svg-FTM6tffSE9Hfaxwz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FTM6tffSE9Hfaxwz .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-FTM6tffSE9Hfaxwz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FTM6tffSE9Hfaxwz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FTM6tffSE9Hfaxwz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FTM6tffSE9Hfaxwz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FTM6tffSE9Hfaxwz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FTM6tffSE9Hfaxwz .marker.cross{stroke:#333333;}#mermaid-svg-FTM6tffSE9Hfaxwz svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FTM6tffSE9Hfaxwz .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FTM6tffSE9Hfaxwz .cluster-label text{fill:#333;}#mermaid-svg-FTM6tffSE9Hfaxwz .cluster-label span{color:#333;}#mermaid-svg-FTM6tffSE9Hfaxwz .label text,#mermaid-svg-FTM6tffSE9Hfaxwz span{fill:#333;color:#333;}#mermaid-svg-FTM6tffSE9Hfaxwz .node rect,#mermaid-svg-FTM6tffSE9Hfaxwz .node circle,#mermaid-svg-FTM6tffSE9Hfaxwz .node ellipse,#mermaid-svg-FTM6tffSE9Hfaxwz .node polygon,#mermaid-svg-FTM6tffSE9Hfaxwz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FTM6tffSE9Hfaxwz .node .label{text-align:center;}#mermaid-svg-FTM6tffSE9Hfaxwz .node.clickable{cursor:pointer;}#mermaid-svg-FTM6tffSE9Hfaxwz .arrowheadPath{fill:#333333;}#mermaid-svg-FTM6tffSE9Hfaxwz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FTM6tffSE9Hfaxwz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FTM6tffSE9Hfaxwz .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-FTM6tffSE9Hfaxwz .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-FTM6tffSE9Hfaxwz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FTM6tffSE9Hfaxwz .cluster text{fill:#333;}#mermaid-svg-FTM6tffSE9Hfaxwz .cluster span{color:#333;}#mermaid-svg-FTM6tffSE9Hfaxwz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FTM6tffSE9Hfaxwz :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} HTTPS 静态文件 API代理 用户 Nginx Angular应用 Spring Boot应用 MySQL数据库集群
部署要点:
- 使用Nginx作为反向代理和静态文件服务器
- Spring Boot应用使用内嵌Tomcat
- 数据库主从复制提高可用性
- 使用环境变量管理敏感信息
- 配置监控和日志系统
总结与扩展
我们实现了一个完整的登录注册系统,具有以下特点:
✅ 前后端分离架构
✅ JWT无状态认证
✅ 响应式表单验证
✅ 路由级权限控制
✅ 多层安全防护
扩展方向:
- 添加社交登录(OAuth2)
- 实现双因素认证
- 集成短信/邮箱验证
- 添加RBAC权限管理系统
- 实现密码重置功能
项目源码
GitHub - Spring Boot后端
GitHub - Angular前端
通过本文,你应该已经掌握了使用Spring Boot和Angular构建登录注册系统的核心知识和技能。这个架构不仅适用于登录注册功能,还可以作为任何需要用户认证的Web应用的基础。


