前置知识: Kotlin

Kotlin与安全

00:00
3 min Intermediate 2026/6/14

Kotlin安全编程

概述

安全编程是指在编写代码时主动防范各种安全威胁,包括空指针异常、数据泄露、注入攻击、密码存储不安全等。Kotlin 在语言层面提供了空安全机制,大幅减少了空指针异常。但在实际开发中,还有许多其他安全问题需要关注,比如输入验证、密码处理、加密解密、网络安全等。

本文涵盖 Kotlin 开发中常见的安全问题和最佳实践,帮助你在编写代码时避免常见的安全漏洞。

基础概念

  • 空安全(Null Safety):Kotlin 的类型系统区分可空和不可空类型,在编译期防止空指针异常
  • 输入验证(Input Validation):永远不要信任用户输入,所有外部数据都要验证
  • 加密(Encryption):用算法将明文转换为密文,保护敏感数据
  • 哈希(Hashing)转换,用于密码存储,不可逆
  • 注入攻击(Injection):攻击者通过输入恶意数据来执行非预期操作

快速上手

空安全基础

// Kotlin 的空安全是第一道防线
fun process(value: String?) {
    // 安全调用:如果 value 为 null,整个表达式返回 null
    val length = value?.length

    // Elvis 运算符:提供默认值
    val safeLength = value?.length ?: 0

    // let 模式:只在非空时执行
    value?.let {
        println("值是: $it")
    }

    // requireNotNull:如果为 null 则抛出异常
    val nonNull = requireNotNull(value) { "值不能为空" }
}

密码哈希

import org.mindrot.jbcrypt.BCrypt

class PasswordService {
    // 哈希密码:使用 BCrypt 自动加盐
    fun hashPassword(password: String): String {
        return BCrypt.hashpw(password, BCrypt.gensalt())
    }

    // 验证密码
    fun checkPassword(password: String, hash: String): Boolean {
        return BCrypt.checkpw(password, hash)
    }
}

fun main() {
    val service = PasswordService()
    val hash = service.hashPassword("mySecret123")
    println("哈希值: $hash")
    println("验证正确密码: ${service.checkPassword("mySecret123", hash)}")  // true
    println("验证错误密码: ${service.checkPassword("wrongPassword", hash)}")  // false
}

详细用法

输入验证

永远不要信任用户输入,所有外部数据都要验证

// 验证工具类
object Validator {
    // 验证邮箱格式
    fun isValidEmail(email: String): Boolean {
        val regex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
        return regex.matches(email)
    }

    // 验证密码强度
    fun isValidPassword(password: String): Boolean {
        if (password.length < 8) return false
        if (!password.any { it.isDigit() }) return false
        if (!password.any { it.isLetter() }) return false
        return true
    }

    // 验证用户名(只允许字母数字下划线)
    fun isValidUsername(name: String): Boolean {
        val regex = Regex("^[a-zA-Z0-9_]{3,20}$")
        return regex.matches(name)
    }
}

// 在业务逻辑中使用
fun registerUser(username: String, email: String, password: String): Result<User> {
    // 逐项验证
    if (!Validator.isValidUsername(username)) {
        return Result.failure(IllegalArgumentException("用户名格式不合法"))
    }
    if (!Validator.isValidEmail(email)) {
        return Result.failure(IllegalArgumentException("邮箱格式不合法"))
    }
    if (!Validator.isValidPassword(password)) {
        return Result.failure(IllegalArgumentException("密码强度不足"))
    }
    return Result.success(User(username, email))
}

SQL 注入防护

import org.jetbrains.exposed.sql.*

// 错误做法:字符串拼接,容易被 SQL 注入
fun findUserUnsafe(name: String): List<User> {
    // 危险!攻击者可以输入 "'; DROP TABLE users; --"
    val query = "SELECT * FROM users WHERE name = '$name'"
    return emptyList()  // 不要这样做
}

// 正确做法:使用参数化查询
fun findUserSafe(name: String): List<String> {
    return transaction {
        // Exposed 自动参数化,防止 SQL 注入
        Users.select { Users.name eq name }
            .map { it[Users.name] }
    }
}

// 使用原生 JDBC 时的参数化查询
fun findUserWithJdbc(name: String, connection: java.sql.Connection): List<String> {
    val results = mutableListOf<String>()
    // 使用 PreparedStatement,参数自动转义
    val stmt = connection.prepareStatement("SELECT * FROM users WHERE name = ?")
    stmt.setString(1, name)  // 参数化,安全
    val rs = stmt.executeQuery()
    while (rs.next()) {
        results.add(rs.getString("name"))
    }
    return results
}

数据加密

import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
import java.util.Base64

class AesEncryptor {
    // 生成 AES 密钥
    fun generateKey(): SecretKey {
        val keyGen = KeyGenerator.getInstance("AES")
        keyGen.init(256)  // 256 位密钥
        return keyGen.generateKey()
    }

    // 加密
    fun encrypt(plainText: String, key: SecretKey): String {
        val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
        cipher.init(Cipher.ENCRYPT_MODE, key)
        val iv = cipher.iv  // 获取初始化向量
        val encrypted = cipher.doFinal(plainText.toByteArray())
        // 将 IV 和密文一起编码
        val combined = iv + encrypted
        return Base64.getEncoder().encodeToString(combined)
    }

    // 解密
    fun decrypt(encryptedText: String, key: SecretKey): String {
        val combined = Base64.getDecoder().decode(encryptedText)
        // 提取 IV(前 16 字节)
        val iv = combined.copyOfRange(0, 16)
        // 提取密文
        val encrypted = combined.copyOfRange(16, combined.size)
        val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
        cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
        return String(cipher.doFinal(encrypted))
    }
}

fun main() {
    val encryptor = AesEncryptor()
    val key = encryptor.generateKey()
    val encrypted = encryptor.encrypt("敏感数据", key)
    println("加密后: $encrypted")
    val decrypted = encryptor.decrypt(encrypted, key)
    println("解密后: $decrypted")
}

日志安全

不要在日志输出敏感信息

object SafeLogger {
    // 敏感字段列表
    private val sensitiveFields = setOf("password", "token", "secret", "creditCard", "ssn")

    // 脱敏处理
    fun sanitize(message: String): String {
        var result = message
        for (field in sensitiveFields) {
            // 替换敏感字段值为 ***
            val regex = Regex("""(?i)($field["\s:=]+)(\S+)""")
            result = result.replace(regex) { "${it.groupValues[1]}***" }
        }
        return result
    }

    fun info(message: String) {
        println("[INFO] ${sanitize(message)}")
    }
}

fun main() {
    // 安全的日志输出
    SafeLogger.info("User login: password=secret123")
    // 输出: [INFO] User login: password=***
}

常见场景

API Token 安全处理

class TokenManager {
    // 不要把 Token 存在普通变量中
    private var token: String? = null

    // 设置 Token 后清除日志中的痕迹
    fun setToken(newToken: String) {
        // 先清零旧 Token 的内存
        token?.let { old ->
            val chars = old.toCharArray()
            chars.fill('\u0000')
        }
        token = newToken
    }

    // 使用后尽快清除
    fun useToken(action: (String) -> Unit) {
        token?.let { action(it) }
    }

    // 清除 Token
    fun clearToken() {
        token = null
    }
}

HTTPS 与证书验证

import io.ktor.client.*
import io.ktor.client.engine.cio.*

// 正确做法:始终使用 HTTPS
val secureClient = HttpClient(CIO) {
    // 不要禁用证书验证
    // 错误做法:trustAllCertificates = true
}

// 如果必须使用自签名证书(仅限开发环境)
fun createDevClient(): HttpClient {
    // 生产环境绝对不能这样做
    return HttpClient(CIO) {
        // 仅在开发环境使用
    }
}

文件路径安全

import java.io.File

class SafeFileAccess(private val baseDir: String) {
    // 防止路径遍历攻击
    fun safeRead(filePath: String): String? {
        val base = File(baseDir).canonicalPath
        val target = File(baseDir, filePath).canonicalPath

        // 检查目标路径是否在基础目录内
        if (!target.startsWith(base)) {
            println("非法路径访问: $filePath")
            return null
        }

        return File(target).takeIf { it.exists() }?.readText()
    }
}

fun main() {
    val accessor = SafeFileAccess("/app/data")
    // 正常访问
    accessor.safeRead("config.json")
    // 路径遍历攻击会被阻止
    accessor.safeRead("../../etc/passwd")  // 返回 null
}

注意事项

  • 不要硬编码密钥加密密钥、API Token 等不要写在代码中,使用环境变量或密钥管理服务
  • 不要自己实现加密算法:使用成熟的标准算法(如 AES、RSA),不要自己发明加密方式
  • 不要在日志打印敏感数据密码Token身份证号等不能出现在日志
  • 不要忽略 HTTPS:所有网络通信都应使用 HTTPS,不要为了方便降级HTTP
  • 不要使用 MD5/SHA1 存储密码:这些算法已被证明安全,使用 BCrypt、Argon2 等专用哈希算法
  • Kotlin 空安全不是万能的:Kotlin 的空安全只在编译检查,与 Java 互操作时仍可能出现空指针

进阶用法

安全的随机数生成

import java.security.SecureRandom

object SecureRandomUtil {
    private val secureRandom = SecureRandom()

    // 生成安全的随机字符串
    fun randomString(length: Int): String {
        val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
        return (1..length)
            .map { chars[secureRandom.nextInt(chars.length)] }
            .joinToString("")
    }

    // 生成安全的随机整数
    fun randomInt(min: Int, max: Int): Int {
        return secureRandom.nextInt(max - min + 1) + min
    }
}

// 不要使用 java.util.Random 生成安全相关的随机数
// 它是伪随机数,可被预测

JWT Token 处理

import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import java.util.Date

class JwtService(private val secret: String) {
    private val algorithm = Algorithm.HMAC256(secret)

    // 生成 Token
    fun createToken(userId: String): String {
        return JWT.create()
            .withClaim("userId", userId)
            .withIssuedAt(Date())
            .withExpiresAt(Date(System.currentTimeMillis() + 3600000))  // 1 小时过期
            .sign(algorithm)
    }

    // 验证 Token
    fun verifyToken(token: String): String? {
        return try {
            val verifier = JWT.require(algorithm).build()
            val decoded = verifier.verify(token)
            decoded.getClaim("userId").asString()
        } catch (e: Exception) {
            null  // Token 无效或过期
        }
    }
}

安全的序列化配置

import kotlinx.serialization.json.Json

// 安全的 JSON 配置
val safeJson = Json {
    ignoreUnknownKeys = true
    // 不允许特殊字符,防止 JSON 注入
    isLenient = false
    // 严格模式,防止意外解析
    allowSpecialFloatingPointValues = false
    // 不允许注释,防止解析歧义
    allowComments = false
}

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式