15、安全加固:HTTPS配置、输入验证、SQL注入防护、XSS防护、CSRF防护
说到后端安全,我见过太多「先上线再说」的项目了。
结果呢?上线第二天就被爬虫薅羊毛,第三周数据库被人拖走。
你想想看,一个API接口暴露在公网上,就像把家门钥匙挂在门口。不做安全加固,等于告诉黑客「欢迎光临」。
这一章,我们聊聊Ktor后端必须做的四道防线:HTTPS、输入验证、SQL注入、XSS和CSRF。嗯,都是实战中踩过的坑。
核心观点:安全不是功能,是习惯。每写一个接口,都要默认「输入是恶意的」。
一、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 会硬气很多。