安全编码:输入验证、SQL注入防护、XSS防护、CSRF防护

安全编码这件事,说实话,很多开发者一开始都不太当回事。

我早年带团队时,就遇到过线上数据被恶意篡改的情况。查了半天,发现是接口没做输入校验,被人钻了空子。从那以后,我对安全编码就格外上心。

今天咱们聊聊四个最基础也最关键的防护点:输入验证、SQL注入、XSS、CSRF。你想想看,只要把这四点守住,大部分Web攻击基本就挡在门外了。

核心原则:永远不要信任用户的输入。

不管是来自表单、URL参数、请求头还是Cookie,所有外部数据都要经过校验和清洗。

安全编码四大核心防护 安全编码 输入验证 SQL注入防护 XSS防护 CSRF防护 白名单校验 长度/类型检查 参数化查询 ORM框架 输出编码 CSP策略 Token验证 SameSite Cookie 四者缺一不可,构成Web应用安全的第一道防线 输入验证 → SQL注入防护 → XSS防护 → CSRF防护

1. 输入验证:第一道防线

输入验证说白了就是「把关」。用户提交的数据,你得先看看合不合规矩。

我个人习惯用白名单校验,而不是黑名单。为什么?因为黑名单永远防不全,攻击者总能找到漏网之鱼。白名单就简单了——我只允许我认可的数据格式通过。

我的经验:在Kotlin中,我通常用正则表达式配合自定义注解来做输入校验。这样代码干净,复用性也高。

// Kotlin 输入验证示例
import javax.validation.constraints.Pattern

data class UserInput(
    @field:Pattern(regexp = "^[a-zA-Z0-9_]{3,20}$")
    val username: String,

    @field:Pattern(regexp = "^[\\w.-]+@[\\w.-]+\\.\\w{2,}$")
    val email: String
)

// 服务层校验
fun validateInput(input: UserInput): Boolean {
    return try {
        // 使用 Jakarta Validation API
        val validator = Validation.buildDefaultValidatorFactory().validator
        val violations = validator.validate(input)
        violations.isEmpty()
    } catch (e: Exception) {
        false
    }
}

嗯,这里要注意:前端校验只是辅助,后端校验才是根本。我在项目中遇到过前端做了校验,后端却直接信任的情况,结果被人用Postman绕过了前端,直接攻击接口。

避坑指南:我曾经接手过一个老项目,所有接口都没有输入长度限制。结果有人传了一个10MB的字符串进来,直接把数据库连接池打爆了。从那以后,我每个字段都会加上长度校验。

2. SQL注入防护:别让用户「操纵」你的数据库

SQL注入,说白了就是用户把SQL代码当成了输入,骗过了你的程序。

举个例子,如果你这样拼接SQL:

// ❌ 错误示范:字符串拼接
val sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'"

用户输入 admin' OR '1'='1,你的SQL就变成了:

SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = ''

结果就是——人家不需要密码就能登录。是不是很可怕?

正确的做法是使用参数化查询预编译语句。在Kotlin中,如果你用Spring Data JPA或Exposed框架,它们默认就帮你做了这件事。

// ✅ 正确示范:参数化查询(Spring Data JPA)
@Query("SELECT u FROM User u WHERE u.username = :username AND u.password = :password")
fun findByUsernameAndPassword(
    @Param("username") username: String,
    @Param("password") password: String
): User?

// ✅ 使用 Exposed 框架
fun findUser(username: String, password: String): User? {
    return transaction {
        Users.selectAll()
            .where { (Users.username eq username) and (Users.password eq password) }
            .singleOrNull()
    }
}

核心原则:永远不要拼接SQL字符串。永远使用参数化查询。

我记得有一次代码审查,看到同事用字符串拼接写了一个动态排序功能。虽然当时没出问题,但我坚持让他改成了参数化方式。后来安全扫描报告出来,那个模块果然被标记为高风险——还好提前改了。

3. XSS防护:别让恶意脚本「跑」起来

XSS(跨站脚本攻击)的核心思路是:攻击者把恶意脚本注入到你的页面中,然后让其他用户执行。

最常见的场景就是评论区。用户提交一段 <script>alert('xss')</script>,如果你直接渲染到页面上,所有访问这个页面的用户都会弹窗。更严重的,可以窃取Cookie、跳转到钓鱼网站。

防护手段其实不复杂:输出编码

// Kotlin + Thymeleaf 自动转义(默认开启)
// 模板中直接输出即可,框架会自动编码
<p th:text="${comment.content}"></p>

// 手动编码(如果必须输出HTML)
import org.springframework.web.util.HtmlUtils

fun encodeForHtml(input: String): String {
    return HtmlUtils.htmlEscape(input)
}

// 使用示例
val safeContent = encodeForHtml(userInput)
// 输出:&lt;script&gt;alert('xss')&lt;/script&gt;

我的建议:除了输出编码,还可以开启内容安全策略(CSP)。在HTTP响应头中设置 Content-Security-Policy,限制脚本只能从特定来源加载。这样即使有XSS漏洞,攻击者也很难执行恶意代码。

// Spring Boot 中配置 CSP
@Configuration
class SecurityConfig : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http.headers()
            .contentSecurityPolicy("default-src 'self'; script-src 'self' https://trusted-cdn.com")
    }
}

你想想看,CSP就像给页面装了一个「门禁系统」,不是所有脚本都能随便进来。我个人非常推荐这个做法。

4. CSRF防护:防止「借刀杀人」

CSRF(跨站请求伪造)的攻击方式比较隐蔽。攻击者诱导用户访问一个恶意网站,这个网站偷偷向你的应用发送请求。因为用户已经登录了,浏览器会自动带上Cookie,服务器就以为是用户本人在操作。

举个例子:你在银行网站登录了,然后不小心点开了一个钓鱼链接。这个链接里藏了一个转账请求,你的浏览器带着你的Cookie发出去,钱就被转走了。

防护方案主要有三种:

防护方式 说明 推荐度
CSRF Token 每个表单生成唯一Token,提交时验证 ⭐⭐⭐⭐⭐
SameSite Cookie 设置Cookie的SameSite属性为Strict或Lax ⭐⭐⭐⭐
Referer/Origin验证 检查请求来源是否合法 ⭐⭐⭐

在Spring Security中,CSRF防护默认就是开启的。你只需要在表单中加上Token即可:

// Spring Security 自动生成CSRF Token
// 在Thymeleaf模板中:
<form action="/transfer" method="post">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
    <!-- 其他表单字段 -->
    <button type="submit">转账</button>
</form>

// 如果是REST API,可以通过请求头传递
// 前端在每次请求时带上 X-CSRF-TOKEN 头

避坑指南:我曾经见过一个项目,为了「方便调试」,把CSRF防护整个关掉了。结果上线第二天就被自动化工具扫描出了CSRF漏洞。记住:不要为了开发方便牺牲安全性。如果确实需要调试,可以用Postman手动处理Token,而不是直接关掉防护。

另外,SameSite Cookie 是现代浏览器的原生支持,设置起来很简单:

// Spring Boot 中配置 SameSite Cookie
@Configuration
class CookieConfig {
    @Bean
    fun cookieSerializer(): DefaultCookieSerializer {
        val serializer = DefaultCookieSerializer()
        serializer.setSameSite("Lax")  // 或者 "Strict"
        return serializer
    }
}

Lax模式允许GET请求携带Cookie,但POST不行。对于大多数场景,Lax就够用了。如果你对安全性要求极高,可以用Strict。

总结一下

安全编码不是什么高深的技术,说白了就是养成好习惯:

  • 输入验证:白名单优先,长度、类型、格式都要检查
  • SQL注入:永远用参数化查询,别碰字符串拼接
  • XSS:输出编码 + CSP策略,双保险
  • CSRF:Token验证 + SameSite Cookie,别偷懒

我个人觉得,安全编码就像系安全带——平时觉得麻烦,真出事的时候能救命。别等到线上出问题了才后悔,那时候就晚了。

最后送大家一句话:安全不是功能,而是习惯。把安全编码融入日常开发中,比事后打补丁强一百倍。

公众号:蓝海资料掘金营,微信deep3321