Angular安全指南

安全是现代Web应用开发的核心。Angular提供了强大的安全特性,但正确使用这些特性至关重要。本章将深入探讨Angular应用的安全最佳实践。

安全第一原则: 永远不要信任用户输入,永远验证所有数据,最小权限原则。

1. 跨站脚本攻击 (XSS) 防护

XSS攻击示例

恶意用户尝试注入JavaScript代码:

<!-- 恶意输入 -->
<script>alert('XSS攻击成功')</script>
<img src="x" onerror="alert('XSS')">

严重 不安全的HTML插入

// 危险:直接插入未净化的HTML
import { Component } from '@angular/core';

@Component({
  selector: 'app-unsafe',
  template: `
    <div [innerHTML]="userInput"></div>
  `
})
export class UnsafeComponent {
  userInput = '<script>alert("XSS")</script><img src="x" onerror="alert("XSS")">';
}

风险: 攻击者可以执行任意JavaScript代码,窃取用户数据,劫持会话。

安全 使用DomSanitizer

// 安全:使用Angular内置的DOM净化器
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

@Component({
  selector: 'app-safe',
  template: `
    <div [innerHTML]="safeHtml"></div>
  `
})
export class SafeComponent {
  safeHtml: SafeHtml;

  constructor(private sanitizer: DomSanitizer) {
    const userInput = '<span style="color: blue">安全的内容</span>';
    this.safeHtml = this.sanitizer.bypassSecurityTrustHtml(userInput);
  }
}

// 或者使用管道
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';

@Pipe({ name: 'safeHtml' })
export class SafeHtmlPipe implements PipeTransform {
  constructor(private sanitizer: DomSanitizer) {}

  transform(html: string) {
    return this.sanitizer.bypassSecurityTrustHtml(html);
  }
}

// 在模板中使用
// <div [innerHTML]="userInput | safeHtml"></div>
XSS攻击防御流程
用户输入
输入验证
DOM净化
安全输出

2. 跨站请求伪造 (CSRF) 防护

高风险 缺少CSRF保护的API调用

// 危险:没有CSRF保护的HTTP请求
import { HttpClient } from '@angular/common/http';

@Injectable()
export class UnsafeApiService {
  constructor(private http: HttpClient) {}

  transferMoney(data: any) {
    // 攻击者可以构造恶意页面发起此请求
    return this.http.post('/api/transfer', data);
  }
}

安全 使用CSRF Token防护

// 安全:使用HttpClient的CSRF保护
import { HttpClient, HttpHeaders } from '@angular/common/http';

@Injectable()
export class SafeApiService {
  constructor(private http: HttpClient) {}

  transferMoney(data: any) {
    // HttpClient会自动处理CSRF token(需要服务器支持)
    return this.http.post('/api/transfer', data, {
      withCredentials: true  // 重要:启用凭据发送
    });
  }
}

// 服务器端配合(Node.js Express示例)
// app.use(cookieParser());
// app.use(csrf({ cookie: true }));

// Angular自动从cookie中获取CSRF token
// 并添加到X-XSRF-TOKEN请求头中

// 手动添加CSRF token的拦截器
import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http';
import { Injectable } from '@angular/core';

@Injectable()
export class CsrfInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    // 从cookie或localStorage获取token
    const csrfToken = this.getCsrfToken();

    if (csrfToken && this.requiresCsrf(req)) {
      const cloned = req.clone({
        headers: req.headers.set('X-CSRF-Token', csrfToken)
      });
      return next.handle(cloned);
    }

    return next.handle(req);
  }

  private getCsrfToken(): string | null {
    // 从cookie中获取
    const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/);
    return match ? decodeURIComponent(match[1]) : null;
  }

  private requiresCsrf(req: HttpRequest<any>): boolean {
    // 仅对修改数据的请求应用CSRF保护
    return ['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method);
  }
}

CSRF防护检查清单

对所有状态修改的请求使用CSRF Token
使用SameSite Cookie属性
验证请求来源(Referer/Origin头)
使用自定义请求头(Angular的HttpClient自动处理)
对重要操作要求重新认证

3. 认证与授权

认证威胁
  • 会话劫持
  • 凭证泄露
  • 暴力破解
  • 会话固定
授权威胁
  • 权限提升
  • 越权访问
  • 信息泄露
  • API滥用
防护措施
  • JWT Token
  • OAuth 2.0
  • 路由守卫
  • HTTP拦截器

安全 JWT认证最佳实践

// JWT认证服务
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { jwtDecode } from 'jwt-decode';

@Injectable({ providedIn: 'root' })
export class AuthService {
  private tokenKey = 'access_token';

  constructor(private http: HttpClient, private router: Router) {}

  login(credentials: any) {
    return this.http.post('/api/auth/login', credentials).pipe(
      tap((response: any) => {
        // 安全存储token
        this.setToken(response.access_token);
        this.setRefreshToken(response.refresh_token);
      })
    );
  }

  private setToken(token: string) {
    // 使用httpOnly cookie更安全,这里演示localStorage方式
    localStorage.setItem(this.tokenKey, token);

    // 或者使用更安全的存储方式
    // 使用Angular的BrowserStorage或服务端session
  }

  logout() {
    // 清除所有认证信息
    localStorage.removeItem(this.tokenKey);
    localStorage.removeItem('refresh_token');
    sessionStorage.clear();

    // 通知服务器撤销token
    this.http.post('/api/auth/logout', {}).subscribe();

    // 重定向到登录页
    this.router.navigate(['/login']);
  }

  getToken(): string | null {
    const token = localStorage.getItem(this.tokenKey);

    if (token && this.isTokenExpired(token)) {
      this.refreshToken();
      return null;
    }

    return token;
  }

  private isTokenExpired(token: string): boolean {
    try {
      const decoded: any = jwtDecode(token);
      return decoded.exp * 1000 < Date.now();
    } catch {
      return true;
    }
  }

  private refreshToken() {
    const refreshToken = localStorage.getItem('refresh_token');

    if (!refreshToken) {
      this.logout();
      return;
    }

    this.http.post('/api/auth/refresh', { refresh_token: refreshToken })
      .subscribe({
        next: (response: any) => {
          this.setToken(response.access_token);
        },
        error: () => {
          this.logout();
        }
      });
  }

  // 检查用户权限
  hasPermission(requiredPermission: string): boolean {
    const token = this.getToken();
    if (!token) return false;

    try {
      const decoded: any = jwtDecode(token);
      const userPermissions = decoded.permissions || [];
      return userPermissions.includes(requiredPermission);
    } catch {
      return false;
    }
  }
}

// 路由守卫
import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot): boolean {
    if (this.authService.isAuthenticated()) {
      // 检查路由所需的权限
      const requiredPermission = route.data['permission'];

      if (requiredPermission && !this.authService.hasPermission(requiredPermission)) {
        this.router.navigate(['/unauthorized']);
        return false;
      }

      return true;
    }

    // 记录尝试访问的URL以便登录后重定向
    this.router.navigate(['/login'], {
      queryParams: { returnUrl: this.router.url }
    });

    return false;
  }
}

// 在路由中使用
const routes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    data: { permission: 'admin.access' }
  }
];

4. 安全HTTP头配置

服务器安全头配置

通过服务器配置增强Angular应用的安全性:

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';
Nginx配置
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin";
add_header Content-Security-Policy "default-src 'self'";
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()";
Express配置
const helmet = require('helmet');
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "https://api.example.com"]
    }
  }
}));

5. 输入验证与输出编码

安全 完整的输入验证链

// 客户端验证
import { FormControl, Validators } from '@angular/forms';
import { AbstractControl, ValidatorFn } from '@angular/forms';

export class CustomValidators {
  // 自定义验证器
  static noSpecialChars(): ValidatorFn {
    return (control: AbstractControl): { [key: string]: any } | null => {
      const hasSpecialChars = /[<>'"/`]/.test(control.value);
      return hasSpecialChars ? { 'specialChars': true } : null;
    };
  }

  static safeEmail(): ValidatorFn {
    return (control: AbstractControl): { [key: string]: any } | null => {
      const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
      return emailRegex.test(control.value) ? null : { 'invalidEmail': true };
    };
  }
}

// 在组件中使用
@Component({
  selector: 'app-registration',
  template: `
    <form [formGroup]="registerForm">
      <input formControlName="username">
      <div *ngIf="registerForm.get('username')?.errors?.specialChars">
        用户名不能包含特殊字符
      </div>
    </form>
  `
})
export class RegistrationComponent {
  registerForm = new FormGroup({
    username: new FormControl('', [
      Validators.required,
      Validators.minLength(3),
      Validators.maxLength(50),
      CustomValidators.noSpecialChars()  // 自定义验证
    ]),
    email: new FormControl('', [
      Validators.required,
      CustomValidators.safeEmail()
    ]),
    password: new FormControl('', [
      Validators.required,
      Validators.minLength(8),
      Validators.pattern('^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$')
    ])
  });

  onSubmit() {
    if (this.registerForm.valid) {
      // 发送到服务器前再次清理
      const cleanedData = this.sanitizeData(this.registerForm.value);
      this.authService.register(cleanedData).subscribe();
    }
  }

  private sanitizeData(data: any): any {
    // 深度清理数据
    return Object.keys(data).reduce((acc, key) => {
      if (typeof data[key] === 'string') {
        // 移除危险字符
        acc[key] = data[key]
          .replace(/[<>'"/`]/g, '')
          .trim();
      } else {
        acc[key] = data[key];
      }
      return acc;
    }, {} as any);
  }
}

// 输出编码
export class OutputEncoder {
  static encodeHtml(text: string): string {
    const map: { [key: string]: string } = {
      '&': '&',
      '<': '<',
      '>': '>',
      '"': '"',
      "'": ''',
      '/': '/',
      '`': '`'
    };
    return text.replace(/[&<>"'\/`]/g, match => map[match]);
  }

  static encodeUrl(text: string): string {
    return encodeURIComponent(text);
  }
}

6. 依赖安全与供应链安全

依赖安全检查清单

定期运行 npm audit 检查漏洞
使用 npm outdated 检查过时依赖
启用GitHub的Dependabot自动安全更新
使用锁定文件(package-lock.json)
验证依赖包的完整性
审查第三方库的代码质量
最小化依赖数量
# 安全工具命令
# 检查安全漏洞
npm audit
npm audit fix  # 自动修复
npm audit fix --force  # 强制修复(可能破坏兼容性)

# 检查过时依赖
npm outdated
npx npm-check-updates  # 更详细的检查

# 使用Snyk安全扫描
npx snyk test
npx snyk monitor

# 检查许可证
npx license-checker --summary
npx npm-license-crawler --json licenses.json

# 包完整性验证
npm ci  # 清洁安装,使用锁定文件
npm shrinkwrap  # 创建锁定文件
供应链威胁
  • 恶意包注入
  • 依赖混淆攻击
  • 劫持合法包
  • 许可证违规
防护措施
  • 使用私有的npm仓库
  • 启用包签名验证
  • 设置CI/CD安全扫描
  • 定期更新依赖

7. 传输层安全 (TLS/SSL)

必须实施 HTTPS强制配置

// Angular中的HTTP客户端强制HTTPS
import { HttpClient } from '@angular/common/http';

@Injectable()
export class SecureApiService {
  constructor(private http: HttpClient) {}

  private ensureHttps(url: string): string {
    if (!url.startsWith('https://')) {
      // 在生产环境中强制HTTPS
      if (environment.production) {
        console.warn('非HTTPS请求被阻止:', url);
        throw new Error('仅允许HTTPS请求');
      }
    }
    return url;
  }

  getSecureData() {
    const apiUrl = this.ensureHttps('/api/data');
    return this.http.get(apiUrl);
  }
}

// 开发环境启用HTTPS
// angular.json
{
  "projects": {
    "my-app": {
      "architect": {
        "serve": {
          "options": {
            "ssl": true,
            "sslKey": "./ssl/localhost.key",
            "sslCert": "./ssl/localhost.crt"
          }
        }
      }
    }
  }
}

// 服务器重定向(Nginx)
server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$server_name$request_uri;
}

// HSTS头配置
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

8. 安全审计与监控

安全审计工具

OWASP ZAP(主动安全扫描)
Burp Suite(渗透测试)
SonarQube(代码质量分析)
Snyk(依赖漏洞扫描)
Lighthouse(安全评分)
Chrome DevTools安全面板
实时监控
  • 异常登录检测
  • API调用频率监控
  • 错误日志分析
  • 用户行为分析
告警机制
  • 安全事件通知
  • 漏洞发现提醒
  • 异常流量告警
  • 数据泄露监测
合规性
GDPR CCPA PCI DSS HIPAA

9. 应急响应与恢复

必须准备 安全应急计划

// 安全事件处理服务
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class SecurityIncidentService {
  constructor(private http: HttpClient) {}

  // 报告安全事件
  reportIncident(type: string, details: any) {
    const incident = {
      type,
      details,
      timestamp: new Date().toISOString(),
      userAgent: navigator.userAgent,
      url: window.location.href
    };

    // 发送到安全事件管理系统
    return this.http.post('/api/security/incidents', incident);
  }

  // 处理令牌泄露
  handleTokenLeak() {
    // 立即清除本地存储
    localStorage.clear();
    sessionStorage.clear();

    // 通知服务器撤销所有令牌
    this.http.post('/api/auth/revoke-all', {}).subscribe();

    // 强制用户重新登录
    window.location.href = '/login?security=incident';
  }

  // 数据泄露响应
  handleDataBreach(affectedData: string[]) {
    // 记录泄露事件
    this.reportIncident('data_breach', { affectedData });

    // 通知用户(根据法律要求)
    if (environment.production) {
      this.notifyAffectedUsers(affectedData);
    }
  }

  // 定期安全检查
  performSecurityCheck() {
    // 检查localStorage中是否有敏感信息
    this.checkLocalStorageSecurity();

    // 检查是否运行在HTTPS下
    if (environment.production && !window.location.protocol.startsWith('https')) {
      this.reportIncident('insecure_protocol', { protocol: window.location.protocol });
    }

    // 检查是否存在恶意扩展
    this.checkBrowserExtensions();
  }

  private checkLocalStorageSecurity() {
    const sensitiveKeys = ['token', 'password', 'secret', 'private'];

    Object.keys(localStorage).forEach(key => {
      if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
        console.warn('发现可能敏感的数据存储在localStorage:', key);
      }
    });
  }
}
安全成熟度模型
基础级

HTTPS、输入验证、CSRF防护

标准级

CSP、安全头、依赖扫描

高级级

实时监控、威胁建模、自动化测试

企业级

零信任架构、安全DevOps、合规认证

安全资源
安全测试清单
  • 定期进行: 渗透测试、漏洞扫描、代码审查
  • 监控指标: 安全事件数量、平均修复时间、测试覆盖率
  • 更新频率: 依赖更新、安全补丁、策略调整
  • 团队培训: 安全意识、应急演练、技能提升

总结:安全是持续的过程

Angular提供了强大的安全特性,但最终的安全取决于开发者的实践。记住安全黄金法则:

  1. 最小权限: 只授予必要的权限
  2. 深度防御: 多层安全防护
  3. 默认安全: 默认拒绝,明确允许
  4. 持续监控: 实时检测和响应
  5. 安全设计: 从开始就考虑安全