15、安全加固:HTTPS配置、输入验证、SQL注入防护、XSS防护、CSRF防护

说到后端安全,我见过太多「先上线再说」的项目了。

结果呢?上线第二天就被爬虫薅羊毛,第三周数据库被人拖走。

你想想看,一个API接口暴露在公网上,就像把家门钥匙挂在门口。不做安全加固,等于告诉黑客「欢迎光临」。

这一章,我们聊聊Ktor后端必须做的四道防线:HTTPS、输入验证、SQL注入、XSS和CSRF。嗯,都是实战中踩过的坑。

核心观点:安全不是功能,是习惯。每写一个接口,都要默认「输入是恶意的」。

Ktor 安全加固 HTTPS 配置 传输加密 输入验证 参数过滤 SQL 注入防护 参数化查询 XSS/CSRF 防护 输出编码+Token SSL/TLS 证书 类型/长度校验 PreparedStatement HTML 实体编码 CSRF Token 四道防线缺一不可,层层设防

一、HTTPS 配置:别让数据裸奔

我个人习惯,只要项目涉及用户登录或支付,第一件事就是配 HTTPS。

为什么?因为 HTTP 是明文传输。你在咖啡厅连公共 WiFi 发个请求,隔壁桌用 Wireshark 就能看到你的密码。

在 Ktor 里配 HTTPS 其实不复杂。你需要一个 SSL 证书,可以是自签名的(开发环境),也可以是 Let's Encrypt 的免费证书(生产环境)。

// application.conf 配置 HTTPS
ktor {
    deployment {
        port = 8443
        sslPort = 8443
    }
    security {
        ssl {
            keyStore = classpath:keystore.jks
            keyStorePassword = "changeit"
            privateKeyPassword = "changeit"
        }
    }
}

小提示:开发环境可以用 keytool 生成自签名证书。但生产环境千万别用自签的,浏览器会报不安全。我推荐用 Certbot 自动申请 Let's Encrypt 证书,免费且自动续期。

代码里怎么强制跳转 HTTPS?加一个拦截器就行:

install(HttpsRedirect) {
    // 自动将 HTTP 请求重定向到 HTTPS
    sslPort = 8443
    permanentRedirect = true
}

我曾经遇到一个坑:配了 HTTPS 但忘了关闭 HTTP 端口。结果用户还是能通过 HTTP 访问,等于白配。记得把 HTTP 端口关掉,或者强制重定向。

二、输入验证:永远不要相信用户

这是安全的第一道门。用户输入什么,你都得当恶意数据来处理。

我见过最离谱的案例:有人把 SQL 语句直接写在用户名输入框里,然后后端没做任何校验,直接把字符串拼到查询里……嗯,数据库被删了。

在 Ktor 里,我习惯用 io.ktor.server.request.receiveParameters() 获取参数后,立刻做校验:

post("/user/register") {
    val username = call.receiveParameters()["username"] ?: ""
    
    // 长度校验
    if (username.length < 3 || username.length > 20) {
        call.respond(HttpStatusCode.BadRequest, "用户名长度需在3-20之间")
        return@post
    }
    
    // 字符白名单校验(只允许字母数字和下划线)
    if (!username.matches(Regex("^[a-zA-Z0-9_]+$"))) {
        call.respond(HttpStatusCode.BadRequest, "用户名包含非法字符")
        return@post
    }
    
    // 继续处理...
}

注意:不要只在前端做校验。前端校验是给用户看的,后端校验才是保命的。用 Postman 或 curl 可以直接绕过前端,所以后端必须再验一次。

我个人习惯用 Kotlin 的 require() 或自定义验证函数,把校验逻辑抽出来复用。这样每个接口都干净很多。

三、SQL 注入防护:参数化查询是底线

SQL 注入说白了,就是攻击者把 SQL 代码混进你的输入里。

比如你写了个查询:

// 危险写法!千万别用
val query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'"

如果用户输入 admin' OR '1'='1,那查询就变成了:

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

结果呢?攻击者不需要密码就能登录。我曾经在一个老项目里看到这种代码,当时后背都凉了。

正确的做法是使用参数化查询。在 Ktor 里配合 Exposed 或 SQLDelight 框架,天然支持:

// 使用 Exposed 框架(安全写法)
transaction {
    val user = Users.selectAll()
        .where { (Users.username eq username) and (Users.password eq passwordHash) }
        .singleOrNull()
}

如果你用原生的 JDBC,记得用 PreparedStatement

val stmt = connection.prepareStatement("SELECT * FROM users WHERE username = ? AND password = ?")
stmt.setString(1, username)
stmt.setString(2, passwordHash)
val rs = stmt.executeQuery()

铁律:永远不要拼接 SQL 字符串。永远。哪怕你觉得「这个字段不可能有注入」,也给我用参数化查询。这是底线。

四、XSS 防护:输出编码不能省

XSS(跨站脚本攻击)是什么?攻击者把 JavaScript 代码塞到你的页面里。

比如用户在评论框里写了 <script>alert('你被黑了')</script>,如果你直接把这个内容渲染到页面上,所有看这条评论的人都会弹窗。

更狠的,可以偷 Cookie、跳转钓鱼网站。

防护方法很简单:输出时做 HTML 实体编码。

在 Ktor 里,如果你返回 JSON 给前端,那 XSS 风险主要在前端。但如果你返回 HTML(比如用 FreeMarker 模板),那后端必须处理。

// 使用 Kotlin 内置的 HTML 编码
import kotlin.text.htmlEncode

fun sanitizeHtml(input: String): String {
    return input.htmlEncode()
}

// 在模板中使用
val safeComment = sanitizeHtml(userComment)

我个人习惯在返回 JSON 时也做一层编码,防止前端忘记处理。虽然有点过度防护,但安全这事,多一层总比少一层好。

避坑指南:我曾经以为「只允许纯文本输入」就安全了。结果用户用 Unicode 字符绕过过滤,比如用全角 代替半角 <。所以编码要放在输出环节,而不是输入环节。

五、CSRF 防护:别让用户替你发请求

CSRF(跨站请求伪造)的原理是:攻击者诱导用户点击一个链接,这个链接自动向你的服务器发请求。

比如用户登录了银行网站,然后不小心点开了一个恶意页面。那个页面自动向银行发了一个转账请求。因为浏览器会自动带上 Cookie,服务器以为是用户本人操作。

在 Ktor 里,我推荐用 CSRF 插件来防护:

install(CSRF) {
    // 只允许同源请求
    allowOrigin("https://yourdomain.com")
    
    // 检查请求头中的自定义 Token
    token {
        headerName = "X-CSRF-Token"
        cookieName = "csrf-token"
        secret = "your-secret-key"
    }
}

前端每次发 POST/PUT/DELETE 请求时,都要带上这个 Token。服务器验证 Token 是否匹配。

另一种简单做法:检查 Referer 头。但 Referer 可以被伪造,所以 Token 方案更靠谱。

注意:CSRF 防护只对「有副作用」的请求有效(POST、PUT、DELETE)。GET 请求一般不做数据修改,所以不需要防护。但如果你用 GET 做删除操作……嗯,那是设计问题,不是安全问题。

六、综合配置示例

最后,我把这些安全配置整合到一个 Ktor 模块里。你可以直接复制使用:

fun Application.configureSecurity() {
    // 1. HTTPS 强制跳转
    install(HttpsRedirect) {
        sslPort = 8443
        permanentRedirect = true
    }
    
    // 2. CSRF 防护
    install(CSRF) {
        allowOrigin("https://yourdomain.com")
        token {
            headerName = "X-CSRF-Token"
            cookieName = "csrf-token"
            secret = System.getenv("CSRF_SECRET") ?: "default-secret"
        }
    }
    
    // 3. 内容安全策略(CSP)
    install(ContentNegotiation) {
        json(Json {
            ignoreUnknownKeys = true
            isLenient = true
        })
    }
    
    // 4. 自定义输入验证拦截器
    intercept(ApplicationCallPipeline.Plugins) {
        if (call.request.contentType().toString().contains("json")) {
            // 这里可以加全局的 JSON 输入校验
        }
    }
}

说实话,安全配置看起来琐碎,但每一条都是血泪教训换来的。

我见过太多项目,上线前觉得「没事,没人会攻击我们」,结果被黑客教做人。

你想想看,一个 HTTPS 配置花 10 分钟,一个参数化查询花 2 分钟,一个 CSRF Token 花 5 分钟。这点时间,换一个安全的后端,值不值?

总结一句话:安全不是功能,是习惯。每写一行代码,都默认「输入是恶意的,输出是危险的」。养成这个习惯,你的 API 会硬气很多。

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