1. 概述
在开发中,实现密码验证机制以确保用户设置强密码是一个常见任务。
本文将介绍如何在 Kotlin 中实现一个灵活、可扩展且健壮的密码验证机制。
2. 问题背景
一个强密码通常要求至少一定长度,并包含大小写字母、数字、特殊字符等组合。
乍一看,密码验证似乎很简单。一些开发者可能会使用正则表达式来处理,例如:
val pattern = "... 一个复杂的正则表达式 ...".toRegex()
if (password.length < minLength || !password.matches(pattern)) {
// 密码不合法
}
虽然这可能有效,但每当密码规则发生变化时都需要修改正则表达式,维护成本高。
因此,我们的目标是构建一个符合 Kotlin 风格、灵活、可扩展且有效的密码验证机制。
我们以如下规则为例:
const val ERR_LEN = "密码至少包含8个字符!"
const val ERR_WHITESPACE = "密码不能包含空格!"
const val ERR_DIGIT = "密码必须包含至少一个数字!"
const val ERR_UPPER = "密码必须包含至少一个大写字母!"
const val ERR_SPECIAL = "密码必须包含至少一个特殊字符,如:_%-=+#@"
此外,我们准备了如下测试用例:
val tooShort = "a1A-"
val noDigit = "_+Addabc"
val withSpace = "abcd A#1#"
val noUpper = "1234abc#"
val noSpecial = "1abcdABCD"
val okPwd = "1234abc#ABC"
接下来,我们逐步构建验证机制。
3. 创建验证函数
我们可以通过分别检查每条规则来实现灵活的验证逻辑。首先,我们构建一个验证函数:
fun validatePassword(pwd: String) = runCatching {
require(pwd.length >= 8) { ERR_LEN }
require(pwd.none { it.isWhitespace() }) { ERR_WHITESPACE }
require(pwd.any { it.isDigit() }) { ERR_DIGIT }
require(pwd.any { it.isUpperCase() }) { ERR_UPPER }
require(pwd.any { !it.isLetterOrDigit() }) { ERR_SPECIAL }
}
该函数使用 runCatching
包裹多个 require
调用,require
在条件不满足时抛出 IllegalArgumentException
,并附带错误信息。
通过 runCatching
返回的 Result
对象,我们可以判断验证是否成功,并在失败时获取错误信息。
测试代码如下:
validatePassword(tooShort).apply {
assertTrue { isFailure }
assertEquals(ERR_LEN, exceptionOrNull()?.message)
}
validatePassword(okPwd).apply {
assertTrue { isSuccess }
}
这种方式比正则表达式更易维护,也便于扩展。
4. 创建 Password 内联类
4.1 扩展 String 类的方法
我们可以将每条规则拆分成 String
的扩展函数,方便组合使用:
fun String.longerThan8() = require(length >= 8) { ERR_LEN }
fun String.withoutWhitespace() = require(none { it.isWhitespace() }) { ERR_WHITESPACE }
fun String.hasDigit() = require(any { it.isDigit() }) { ERR_DIGIT }
fun String.hasUppercase() = require(any { it.isUpperCase() }) { ERR_UPPER }
fun String.hasSpecialChar() = require(any { !it.isLetterOrDigit() }) { ERR_SPECIAL }
然后我们可以将这些方法作为函数引用组合使用:
val checks = listOf(
String::longerThan8,
String::withoutWhitespace,
String::hasDigit,
String::hasUppercase,
String::hasSpecialChar,
)
验证时使用:
runCatching { checks.forEach { it(noDigit) } }.apply {
assertTrue { isFailure }
assertEquals(ERR_DIGIT, exceptionOrNull()?.message)
}
但这种方式会污染 String
类,可能导致其他开发者误解。
4.2 使用内联类封装验证逻辑
Kotlin 的内联类可以避免污染 String
类,同时提供类型安全的封装。我们创建 Password
内联类如下:
@JvmInline
value class Password(val pwd: String) : CharSequence by pwd {
fun longerThan8Rule() = require(pwd.length >= 8) { ERR_LEN }
fun withoutWhitespaceRule() = require(pwd.none { it.isWhitespace() }) { ERR_WHITESPACE }
fun hasDigitRule() = require(pwd.any { it.isDigit() }) { ERR_DIGIT }
fun hasUppercaseRule() = require(pwd.any { it.isUpperCase() }) { ERR_UPPER }
fun hasSpecialCharRule() = require(pwd.any { !it.isLetterOrDigit() }) { ERR_SPECIAL }
infix fun checkWith(rules: List<KFunction1<Password, Unit>>) = runCatching { rules.forEach { it(this) } }
}
使用方式如下:
val rules = listOf(
Password::hasDigitRule,
Password::longerThan8Rule,
Password::withoutWhitespaceRule,
Password::hasSpecialCharRule,
Password::hasUppercaseRule
)
(Password(tooShort) checkWith rules).apply {
assertTrue { isFailure }
assertEquals(ERR_LEN, exceptionOrNull()?.message)
}
✅ 优势:不污染 String
类,支持灵活扩展规则。
❌ 缺点:只能通过 Password
类型调用。
4.3 支持管理员密码规则
我们还可以为管理员密码添加额外规则,例如:
fun Password.withoutAdminRule() = require("admin" !in pwd.lowercase()) { "管理员密码不能包含 'admin'" }
fun Password.longerThan10Rule() = require(pwd.length >= 10) { "管理员密码必须大于10个字符!" }
val adminRules = listOf(
Password::hasDigitRule,
Password::withoutWhitespaceRule,
Password::hasSpecialCharRule,
Password::hasUppercaseRule,
Password::withoutAdminRule,
Password::longerThan10Rule
)
测试:
(Password("1234adX%@") checkWith adminRules).apply {
assertTrue { isFailure }
assertEquals("管理员密码必须大于10个字符!", exceptionOrNull()?.message)
}
5. 收集所有验证错误信息
目前我们的验证逻辑在遇到第一个失败规则时就会终止。但有时我们希望返回所有验证失败的错误信息。
为此,我们可以在 Password
类中添加 validateWith
方法:
infix fun validateWith(rules: List<KFunction1<Password, Unit>>) = runCatching {
val message = rules.mapNotNull {
runCatching { it(this) }.exceptionOrNull()?.message
}.joinToString(separator = "\n")
require(message.isEmpty()) { message }
}
测试:
val onlyOneSpace = " " // 触发多个错误
(Password(onlyOneSpace) validateWith rules).apply {
assertTrue { isFailure }
assertEquals(
setOf(ERR_LEN, ERR_WHITESPACE, ERR_DIGIT, ERR_UPPER),
exceptionOrNull()?.message?.split("\n")?.toSet()
)
}
这样,我们就能一次性返回所有错误信息,提升用户体验。
6. 总结
我们首先使用 runCatching
和 require
构建了一个灵活的密码验证函数,相比正则表达式更易维护。
接着,我们将规则拆分为 String
扩展函数,提高了灵活性,但也带来了类污染问题。
为了解决这个问题,我们使用 Kotlin 的内联类 Password
封装验证逻辑,既保持了灵活性,又避免了类污染。
最后,我们进一步扩展了验证机制,使其支持返回所有验证失败的错误信息。
所有代码示例可在 GitHub 上找到。