安全是现代Web应用开发的核心。Angular提供了强大的安全特性,但正确使用这些特性至关重要。本章将深入探讨Angular应用的安全最佳实践。
恶意用户尝试注入JavaScript代码:
<!-- 恶意输入 -->
<script>alert('XSS攻击成功')</script>
<img src="x" onerror="alert('XSS')">
// 危险:直接插入未净化的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代码,窃取用户数据,劫持会话。
// 安全:使用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>
// 危险:没有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);
}
}
// 安全:使用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);
}
}
// 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' }
}
];
通过服务器配置增强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';
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=()";
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"]
}
}
}));
// 客户端验证
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);
}
}
npm audit 检查漏洞npm outdated 检查过时依赖# 安全工具命令
# 检查安全漏洞
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 # 创建锁定文件
// 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;
// 安全事件处理服务
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提供了强大的安全特性,但最终的安全取决于开发者的实践。记住安全黄金法则: