登录注册前端详细实现 (Angular 15+)
文章标签:
用户注册表单代码html
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'
};
功能亮点说明
- 响应式表单验证
- 自定义密码验证器(长度、大小写、数字、特殊字符)
- 实时表单错误反馈
- 密码匹配验证
- 用户体验优化
- 密码可见性切换
- 加载状态指示器
- 错误和成功消息提示
- 记住我功能
- 登录后重定向
- 安全特性
- JWT自动附加到请求头
- Token过期处理(401错误自动跳转)
- 本地存储加密考虑(实际项目应使用更安全的方式)
- 状态管理
- BehaviorSubject管理用户状态
- 自动获取当前用户信息
- 本地存储持久化
- 路由保护
- 未认证用户自动重定向
- 登录后返回原始请求页面
- 模块懒加载
- 错误处理
- 统一HTTP错误拦截
- 友好的错误消息展示
- 表单验证错误提示
使用说明
- 安装依赖:
npm install @angular/material @angular/cdk @angular/flex-layout
- 在styles.scss中添加:
@import '@angular/material/prebuilt-themes/indigo-pink.css';
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
- 运行应用:
ng serve --port 4200
这个实现提供了完整的认证流程,包括:
- 用户注册(带密码强度验证)
- 登录/注销功能
- JWT令牌管理
- 路由保护
- 响应式UI设计
- 完善的错误处理
实际项目中,你还可以添加:
- 密码重置功能
- 双因素认证
- 社交登录(Google/Facebook)
- 用户资料管理
- 权限控制(基于角色)
