引言:为什么“自己造轮子”的认证方式存在风险?
\n\n想象一下:您正在用Kotlin编写应用程序——无论是Android客户端还是基于Ktor/Spring Boot的后端。您快速搭建了一个登录表单,将密码保存在SharedPreferences中,并庆幸一切运行正常。但一周后,您收到一条消息:“您的应用程序被入侵,数据已泄露”。熟悉吗?不幸的是,这是经典案例。
\n\nKotlin中的安全认证不仅仅是“验证密码”。它是一个完整的生态系统:哈希、令牌、防请求伪造、安全存储机密信息。在本文中,我们将探讨如何避免常见错误并构建可靠的登录系统。开始吧!
\n\n1. 密码哈希:bcrypt及其同类算法
\n\n安全的第一条规则:切勿以明文形式存储密码。即使您使用HTTPS,数据库也可能被入侵。您的任务是仅存储哈希值。
\n\n为什么MD5和SHA是邪恶的?
\n这些算法速度很快。而快速的哈希正是黑客的梦想:他们可以每秒尝试数十亿种组合。请改用bcrypt、scrypt或Argon2。它们故意设计得较慢,并且能够抵御GPU攻击。
\n\n使用jbcrypt库的Kotlin示例:
import org.mindrot.jbcrypt.BCrypt\n\nfun hashPassword(password: String): String {\n val salt = BCrypt.gensalt(12) // cost factor 12 — 最优\n return BCrypt.hashpw(password, salt)\n}\n\nfun checkPassword(password: String, hashed: String): Boolean {\n return BCrypt.checkpw(password, hashed)\n}\n\n重要提示:盐值会自动生成并嵌入哈希中。您无需单独存储它。
\n\n2. JWT:轻量级且安全的认证
\n\nJSON Web Token (JWT) 是在各方之间传输声明(claims)的标准。在Kotlin中,您可以使用java-jwt (Auth0) 或jsonwebtoken (jjwt) 库。
在Kotlin中生成JWT
\n\nimport io.jsonwebtoken.Jwts\nimport io.jsonwebtoken.SignatureAlgorithm\nimport io.jsonwebtoken.security.Keys\nimport java.util.Date\n\nfun generateToken(userId: String, secretKey: String): String {\n val key = Keys.hmacShaKeyFor(secretKey.toByteArray())\n return Jwts.builder()\n .setSubject(userId)\n .setIssuedAt(Date())\n .setExpiration(Date(System.currentTimeMillis() + 3600_000)) // 1小时\n .signWith(key, SignatureAlgorithm.HS256)\n .compact()\n}\n\n验证令牌
\n\nfun validateToken(token: String, secretKey: String): String? {\n return try {\n val key = Keys.hmacShaKeyFor(secretKey.toByteArray())\n val claims = Jwts.parserBuilder()\n .setSigningKey(key)\n .build()\n .parseClaimsJws(token)\n .body\n claims.subject // 返回userId\n } catch (e: Exception) {\n null // 令牌无效或已过期\n }\n}\n\n建议:不要将密钥存储在代码中。请使用环境变量或Vault。
\n\n3. Android上的安全令牌存储
\n\n如果您正在用Kotlin编写移动应用程序,访问令牌不能存储在SharedPreferences中——这是一个开放文件,其他应用程序或具有root权限的攻击者可能读取它。
\n\n请使用EncryptedSharedPreferences(来自AndroidX Security)或Keystore:
\n\nimport androidx.security.crypto.EncryptedSharedPreferences\nimport androidx.security.crypto.MasterKey\n\nfun saveToken(context: Context, token: String) {\n val masterKey = MasterKey.Builder(context)\n .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n .build()\n\n val sharedPreferences = EncryptedSharedPreferences.create(\n context,\n "secure_prefs",\n masterKey,\n