网络安全入门到实战:2026年必备的安全防护技能
面向开发者和运维人员的网络安全实战指南:OWASP Top 10防御、SQL注入/XSS/CSRF防护、API安全、认证授权(OAuth2.0/JWT)、容器安全、供应链安全、日志审计和应急响应流程。
SteveRocket
北京,中国
2 min read
安全不是”加上去”的,是”设计进去”的
大多数团队对待安全的方式是:开发完成 → 上线前做一次渗透测试 → 修几个高危漏洞 → 完事。
这种”亡羊补牢”式的安全,在 2026 年已经行不通了。攻击者不会等你”补好漏洞”再下手——根据 Verizon 数据泄露报告,攻击者从入侵到窃取数据的平均时间已经缩短到 4 小时以内。
我们团队经历了从”安全是安全团队的事”到”安全是每个人的事”的转变。这篇文章面向开发者,讲清楚你需要知道的安全知识和实战技能。
OWASP Top 10(2026)
OWASP Top 10 是 Web 应用安全的”必读清单”:
| 排名 | 风险 | 一句话解释 |
|---|---|---|
| 1 | 访问控制失效 | 用户能访问不该访问的数据 |
| 2 | 加密失效 | 敏感数据明文传输/存储 |
| 3 | 注入攻击 | 用户输入被当作代码执行 |
| 4 | 不安全的设计 | 架构层面缺少安全考虑 |
| 5 | 安全配置错误 | 默认密码、错误信息泄露 |
| 6 | 易受攻击的组件 | 使用了有漏洞的依赖 |
| 7 | 认证失效 | 登录系统可被绕过 |
| 8 | 软件和数据完整性失效 | CICD 或更新被篡改 |
| 9 | 日志和监控失效 | 被攻击了也不知道 |
| 10 | SSRF | 服务端被利用发起恶意请求 |
SQL 注入:老问题,新花样
SQL 注入已经存在 20 多年了,但至今仍在 OWASP Top 10 里。不是因为没有解决方案,而是因为开发者不够重视。
经典注入
# ❌ 危险:字符串拼接
user_id = request.GET.get('id')
query = f"SELECT * FROM users WHERE id = {user_id}"
cursor.execute(query)
# 攻击者输入:1 OR 1=1
# 实际执行:SELECT * FROM users WHERE id = 1 OR 1=1
# 结果:返回所有用户!
参数化查询
# ✅ 安全:参数化查询
cursor.execute("SELECT * FROM users WHERE id = %s", [user_id])
# Go
db.Query("SELECT * FROM users WHERE id = ?", userID)
# Java (JDBC PreparedStatement)
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setInt(1, userId);
二次注入
// 场景:用户注册时存储了恶意用户名
username := "admin' --"
// 注册时用了参数化查询(安全)
db.Exec("INSERT INTO users (username) VALUES (?)", username)
// 但后续某个功能拼接了用户名(危险!)
query := fmt.Sprintf("SELECT * FROM users WHERE username = '%s'", username)
// 变成了:SELECT * FROM users WHERE username = 'admin' --'
教训:永远不要信任数据库中存储的数据——因为数据可能是之前被”安全地”存进去的恶意内容。
ORM 不是银弹
# Django ORM 大部分情况安全
User.objects.filter(id=user_id) # ✅ 参数化
# 但原生 SQL 仍然危险
User.objects.raw(f"SELECT * FROM users WHERE id = {user_id}") # ❌
# 甚至 ORM 的一些高级功能也可能有问题
User.objects.extra(where=[f"id = {user_id}"]) # ❌
XSS(跨站脚本攻击)
三种 XSS 类型
1. 反射型 XSS
攻击者发送链接:https://site.com/search?q=<script>stealCookie()</script>
服务器把参数原样返回在页面中
用户点击 → 脚本在用户浏览器执行
2. 存储型 XSS
攻击者提交评论:<script>stealCookie()</script>
评论存储在数据库
其他用户浏览评论区 → 脚本在所有人浏览器执行
3. DOM 型 XSS
// ❌ 危险:直接操作 innerHTML
document.getElementById('content').innerHTML = userInput;
// ✅ 安全:使用 textContent
document.getElementById('content').textContent = userInput;
防御措施
// 1. 输出编码(服务端)
import { escapeHtml } from 'escape-html';
app.get('/search', (req, res) => {
const query = escapeHtml(req.query.q);
res.send(`<div>搜索:${query}</div>`);
});
// 2. Content Security Policy
// 在 HTTP 响应头中设置
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}';
style-src 'self' 'unsafe-inline';
img-src 'self' https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
// 3. HttpOnly Cookie(防止 JS 读取)
Set-Cookie: session=xxx; HttpOnly; Secure; SameSite=Strict
CSRF(跨站请求伪造)
攻击原理
1. 用户登录了 bank.com
2. 用户访问了恶意网站 evil.com
3. evil.com 包含:
<img src="https://bank.com/transfer?to=attacker&amount=10000">
4. 浏览器自动带上 bank.com 的 Cookie
5. 转账成功!
防御方案
方案一:CSRF Token(传统方案)
<!-- 表单中包含 CSRF Token -->
<form method="POST" action="/transfer">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<!-- ... -->
</form>
# 服务端验证
def transfer(request):
token = request.POST.get('csrf_token')
if token != request.session.get('csrf_token'):
raise CsrfError("CSRF token 验证失败")
# 执行业务逻辑
方案二:SameSite Cookie(现代方案)
Set-Cookie: session=xxx; SameSite=Strict
Set-Cookie: session=xxx; SameSite=Lax # 允许从外部链接 GET 请求
Set-Cookie: session=xxx; SameSite=None; Secure # 跨站但仅 HTTPS
SameSite 属性说明:
| 值 | 跨站发送 Cookie | 适用场景 |
|---|---|---|
| Strict | 完全不发送 | 银行等高安全场景 |
| Lax | GET 请求发送 | 大多数 Web 应用(推荐) |
| None | 所有请求发送 | 跨站嵌入(需要 HTTPS) |
认证与授权
JWT 的正确使用姿势
// ❌ 常见错误
// 1. 不设过期时间
// 2. 用对称密钥但密钥太弱
// 3. 把敏感信息放 payload(payload 只 base64 编码,不是加密!)
// ✅ 正确实现
type TokenManager struct {
accessSecret []byte
refreshSecret []byte
accessExpiry time.Duration // 15 分钟
refreshExpiry time.Duration // 7 天
}
func (tm *TokenManager) GenerateAccessToken(userId string) (string, error) {
claims := jwt.MapClaims{
"sub": userId,
"iat": time.Now().Unix(),
"exp": time.Now().Add(tm.accessExpiry).Unix(),
"jti": uuid.New().String(), // 唯一 ID,用于撤销
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(tm.accessSecret)
}
// Refresh Token 轮换(重要!)
func (tm *TokenManager) RefreshTokens(refreshToken string) (string, string, error) {
// 1. 验证 refresh token
// 2. 标记旧的 refresh token 为已使用(防止重放攻击)
// 3. 生成新的 access token 和 refresh token
// 4. 返回新的一对 token
}
OAuth 2.0 授权码流程
用户 → 客户端 → 授权服务器 → 用户登录确认
↓ 授权码
用户 ← 客户端 ← 授权服务器
↓ 授权码 + client_secret
客户端 → 授权服务器
↓ access_token + refresh_token
客户端 → 资源服务器(携带 access_token)
PKCE(Proof Key for Code Exchange):即使使用授权码模式,移动端和 SPA 也应该加上 PKCE 防止授权码拦截。
权限模型:RBAC 和 ABAC
// RBAC(基于角色的访问控制)
type Permission string
const (
PermReadUser Permission = "user:read"
PermWriteUser Permission = "user:write"
PermDeleteUser Permission = "user:delete"
)
var rolePermissions = map[string][]Permission{
"admin": {PermReadUser, PermWriteUser, PermDeleteUser},
"editor": {PermReadUser, PermWriteUser},
"viewer": {PermReadUser},
}
func (s *AuthService) HasPermission(userId, permission string) bool {
roles := s.getUserRoles(userId)
for _, role := range roles {
for _, perm := range rolePermissions[role] {
if string(perm) == permission {
return true
}
}
}
return false
}
API 安全
常见 API 安全风险
| 风险 | 说明 | 防御 |
|---|---|---|
| 缺乏限流 | 暴力破解、DDoS | 令牌桶/漏桶限流 |
| 过度暴露数据 | 返回不该返回的字段 | DTO + 字段级控制 |
| 批量赋值 | 用户修改了不该改的字段 | 白名单字段 |
| 不安全的数据传输 | HTTP 明文传输 | 强制 HTTPS |
| 缺乏输入验证 | 接受任意输入 | 严格验证和清洗 |
API 限流实现
// 基于 Redis 的滑动窗口限流
func rateLimit(userId string, limit int, window time.Duration) bool {
key := fmt.Sprintf("ratelimit:%s", userId)
now := time.Now().UnixNano()
windowStart := now - window.Nanoseconds()
// 移除窗口外的记录
client.ZRemRangeByScore(ctx, key, "0", strconv.FormatInt(windowStart, 10))
// 统计窗口内的请求数
count, _ := client.ZCard(ctx, key).Result()
if count >= int64(limit) {
return false // 限流
}
// 添加当前请求记录
client.ZAdd(ctx, key, &redis.Z{
Score: float64(now),
Member: strconv.FormatInt(now, 10),
})
client.Expire(ctx, key, window)
return true
}
批量赋值防护
# ❌ 危险:直接绑定整个请求体
class UserUpdateView(APIView):
def put(self, request, user_id):
user = User.objects.get(id=user_id)
for key, value in request.data.items():
setattr(user, key, value) # 用户可能修改 is_admin!
user.save()
# ✅ 安全:白名单字段
class UserUpdateView(APIView):
ALLOWED_FIELDS = {'nickname', 'avatar', 'bio', 'email'}
def put(self, request, user_id):
user = User.objects.get(id=user_id)
update_data = {
k: v for k, v in request.data.items()
if k in self.ALLOWED_FIELDS
}
for key, value in update_data.items():
setattr(user, key, value)
user.save()
容器安全
Docker 安全清单
# 1. 不要用 root 运行
USER 1000:1000
# 2. 使用特定版本标签,不要用 latest
FROM node:20.11-alpine # ✅
FROM node:latest # ❌
# 3. 最小化攻击面
RUN apk --no-cache add ca-certificates && \
rm -rf /var/cache/apk/*
# 4. 设置只读文件系统(在 docker-compose 或 K8s 中)
# docker-compose.yml
services:
app:
read_only: true
tmpfs:
- /tmp
K8s 安全上下文
apiVersion: v1
kind: Pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL # 移除所有 capabilities
# 如果需要绑定低端口
# capabilities:
# add:
# - NET_BIND_SERVICE
镜像签名和验证
# 使用 Cosign 签名镜像
cosign sign --key cosign.key myregistry/myapp:v1.0.0
# 部署时验证签名
cosign verify --key cosign.pub myregistry/myapp:v1.0.0
# 在 K8s 中使用准入控制器强制验证
# 通过 Kyverno 或 OPA Gatekeeper 策略
供应链安全
依赖管理
# npm 依赖审计
npm audit
npm audit fix
# Python 依赖检查
pip-audit
safety check
# Go 依赖检查
govulncheck ./...
# 容器镜像扫描
trivy image myapp:latest
SBOM(软件物料清单)
# 生成 SBOM
syft myapp:latest -o spdx-json > sbom.json
syft dir:./ -o cyclonedx-json > sbom.json
# 在 CI 中生成并存储
# GitLab CI 示例
generate_sbom:
script:
- syft $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -o spdx-json > sbom.json
artifacts:
paths:
- sbom.json
expire_in: 1 year
锁定依赖版本
# package-lock.json / yarn.lock — 必须提交到 Git
# go.sum — 必须提交到 Git
# requirements.txt — 锁定具体版本
Django==5.0.2 # ✅
Django>=5.0 # ❌ 太宽泛
# Pipfile.lock / poetry.lock — 必须提交到 Git
日志审计和监控
安全日志应记录什么
type SecurityLog struct {
Timestamp time.Time
EventType string // "login", "permission_denied", "data_access"
UserId string
IPAddress string
UserAgent string
Resource string // 访问的资源
Action string // 执行的操作
Result string // "success", "failure"
Details string // 附加信息
}
// 关键事件必须记录
func (s *SecurityLogger) LogLoginAttempt(userId, ip string, success bool) {
s.Log(SecurityLog{
EventType: "login",
UserId: userId,
IPAddress: ip,
Result: map[bool]string{true: "success", false: "failure"}[success],
Details: fmt.Sprintf("Login attempt from %s", ip),
})
}
func (s *SecurityLogger) LogPermissionDenied(userId, resource, action string) {
// 权限拒绝 → 可能是攻击探测 → 高优先级
s.LogWithPriority(SecurityLog{
EventType: "permission_denied",
UserId: userId,
Resource: resource,
Action: action,
Result: "failure",
}, PriorityHigh)
}
异常检测规则
# 暴力破解检测
同一IP 5分钟内 > 10次登录失败 → 告警 + 临时封禁IP
# 异常访问检测
用户凌晨3点访问管理后台 → 告警
用户从北京和纽约同时登录 → 告警(不可能)
用户1小时内访问1000条数据(平时<10条) → 告警
# 权限提升检测
普通用户突然有了管理员权限(权限表被修改) → 严重告警
应急响应流程
发现安全事件 →
1. 确认(是真的攻击吗?影响范围?)
2. 遏制(切断攻击源、隔离受影响系统)
3. 清除(移除后门、修复漏洞)
4. 恢复(从备份恢复、验证系统正常)
5. 复盘(根因分析、改进措施、更新安全策略)
应急联系人清单
每个团队应该维护一张应急联系清单:
安全事件响应团队:
- 安全负责人:张工 138xxxx xxxx(24小时)
- 运维负责人:李工 139xxxx xxxx(24小时)
- 法务联系人:王律 137xxxx xxxx(工作日)
- 公关联系人:赵总 136xxxx xxxx
外部资源:
- 云服务商应急支持热线
- 第三方安全公司
- 监管机构报告渠道
我们的实践经验
经验一:安全左移
安全要从需求阶段就开始考虑,而不是等上线前:
需求评审 → 安全需求分析(威胁建模)
↓
设计评审 → 安全架构审查
↓
开发阶段 → SAST(静态代码分析)+ 安全编码规范
↓
测试阶段 → DAST(动态扫描)+ 渗透测试
↓
上线前 → 安全配置检查 + 镜像扫描
↓
运行中 → RASP + 持续监控
经验二:默认安全
所有配置的默认值应该是安全的:
# ❌ 不安全的默认配置
debug: true
password: admin123
cors_origins: "*"
ssl: false
# ✅ 安全的默认配置
debug: false
password: ${REQUIRED_ENV_VAR} # 没有环境变量就启动失败
cors_origins: [] # 默认不允许任何跨域
ssl: true # 默认启用 HTTPS
经验三:纵深防御
不要只依赖一种安全措施:
第一层:网络防火墙 + WAF
第二层:API 网关(认证、限流)
第三层:应用层(输入验证、输出编码)
第四层:数据库(加密、审计)
第五层:监控告警
总结
安全是一个持续的过程,不是一次性的项目。核心要点:
- 安全左移:从需求阶段开始考虑安全
- 参数化查询:杜绝 SQL 注入
- 输出编码 + CSP:防范 XSS
- SameSite Cookie + CSRF Token:防范 CSRF
- JWT 短过期 + Refresh Token 轮换:安全的认证方案
- 最小权限 + 默认拒绝:API 和容器安全的基础
- 日志审计 + 异常检测:被攻击了要知道
- 应急响应流程:知道被攻击后该做什么