CTO Plus技术服务栈

网络安全入门到实战: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日志和监控失效被攻击了也不知道
10SSRF服务端被利用发起恶意请求

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完全不发送银行等高安全场景
LaxGET 请求发送大多数 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 网关(认证、限流)
第三层:应用层(输入验证、输出编码)
第四层:数据库(加密、审计)
第五层:监控告警

总结

安全是一个持续的过程,不是一次性的项目。核心要点:

  1. 安全左移:从需求阶段开始考虑安全
  2. 参数化查询:杜绝 SQL 注入
  3. 输出编码 + CSP:防范 XSS
  4. SameSite Cookie + CSRF Token:防范 CSRF
  5. JWT 短过期 + Refresh Token 轮换:安全的认证方案
  6. 最小权限 + 默认拒绝:API 和容器安全的基础
  7. 日志审计 + 异常检测:被攻击了要知道
  8. 应急响应流程:知道被攻击后该做什么

Comments

Link copied to clipboard!