SmsCodeAuthenticationProvider.kt
package io.github.lishangbu.avalon.auth.authentication
import io.github.lishangbu.avalon.auth.service.VerificationCodeService
import io.github.lishangbu.avalon.oauth2.authorizationserver.authentication.SmsAuthenticationToken
import io.github.lishangbu.avalon.oauth2.authorizationserver.exception.InvalidCaptchaException
import io.github.lishangbu.avalon.oauth2.common.core.AuthorizationGrantTypeSupport
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.stereotype.Component
/**
* 短信验证码认证提供者
*
* 使用 Redis 中的短信验证码进行认证
*
* @author lishangbu
* @since 2026/3/13
*/
@Component
class SmsCodeAuthenticationProvider(
/** 验证码服务 */
private val verificationCodeService: VerificationCodeService,
/** 用户详情服务 */
private val userDetailsService: UserDetailsService,
) : AuthenticationProvider {
/** 校验短信验证码并构造已认证令牌 */
@Throws(AuthenticationException::class)
override fun authenticate(authentication: Authentication): Authentication {
val smsAuthenticationToken = authentication as SmsAuthenticationToken
val normalizedPhone = normalizePhone(smsAuthenticationToken.principal)
val normalizedCode = resolveRequiredText(smsAuthenticationToken.credentials)
verificationCodeService.verifyCode(
normalizedPhone,
normalizedCode,
AuthorizationGrantTypeSupport.SMS.value,
)
val userDetails: UserDetails = userDetailsService.loadUserByUsername(normalizedPhone)
val authenticated =
SmsAuthenticationToken(userDetails, null, userDetails.authorities).also {
it.details = authentication.details
}
return authenticated
}
/** 判断当前提供者是否支持短信验证码令牌 */
override fun supports(authentication: Class<*>): Boolean = SmsAuthenticationToken::class.java.isAssignableFrom(authentication)
/** 解析必填文本 */
private fun resolveRequiredText(value: Any?): String =
value
?.toString()
?.trim()
?.takeIf { it.isNotEmpty() }
?: throw InvalidCaptchaException("短信验证码不能为空")
/** 规范化手机 */
private fun normalizePhone(phone: Any?): String = resolveRequiredText(phone)
}