上篇回顾
上篇文章我们讲解了如何自定义登录页样式,以及添加了对已登录状态的显示处理;在第二篇文章中我们学习过如何使用数据库内的用户信息,即自定义用户账号及密码数据源,但这些的基础逻辑都是用户在登录页使用用户名和密码登录,有时候我们可能需要更加复杂的逻辑,如使用域控来做登录验证(正常情况下我们是拿不到域控密码的,只能借助域控做密码验证);或是登录后可能还想做一些额外操作,如记录本次登录的账号、IP、时间日志。 这个时候就需要自定义登录逻辑,本篇文章将讲解如何实现。
自定义登录逻辑
严格来讲这个属于Spring Security的范畴,但是为了系列文章的完整性还是介绍一下。
默认不做任何修改的情况下,在登录页点击登录按钮后,走的是org.springframework.security.authentication.dao.DaoAuthenticationProvider这个类的逻辑,感兴趣的可以去看一下源码,所以有些博客可能也有去覆盖这个类的做法来实现自定义登录逻辑。我们的实现方式有所不同:
在这里我们假定以下一个自定义场景来演示代码:
假设我们通过配置类或数据库来维护一个“默认密码”,用于测试环境,这样用户不需要维护密码,直接用统一的密码来登录测试环境即可。
1. 实现一个类,来实现AuthenticationProvider接口, 实现authenticate方法,并support一下UsernamePasswordAuthenticationToken这个token类型
class MyUsernamePasswordAuthenticationProvider(
private val userDetailsService: UserDetailsService,
) : AuthenticationProvider {
private val DEFAULT_PASSWORD: String? = "pwd123456" // 假设默认密码为'pwd123456'
override fun authenticate(authentication: Authentication): Authentication {
val username = authentication.name
val password = authentication.credentials.toString()
try {
// 1. 先尝试从数据库加载用户
val userDetails: UserDetails = userDetailsService.loadUserByUsername(username)
// 2. 自定义验证逻辑
if (DEFAULT_PASSWORD != null) { // 如果维护了默认密码,拿默认密码去验证
if (password != DEFAULT_PASSWORD) throw BadCredentialsException("密码错误")
} else { // 如果没维护默认密码,走数据库验证
if (!MyPasswordEncoder.matches(password,userDetails.password))
throw BadCredentialsException("密码错误")
}
// 返回认证结果
return UsernamePasswordAuthenticationToken(userDetails, password, userDetails.authorities)
} catch (e: Exception) {
throw BadCredentialsException("认证失败:${e.message}")
}
}
override fun supports(authentication: Class<*>): Boolean {
return UsernamePasswordAuthenticationToken::class.java.isAssignableFrom(authentication)
}
}
这里面的MyPasswordEncoder是需要新定义的一个单例类,因为反复会用到这个,每次都注入使用的话又很麻烦:
object MyPasswordEncoder : BCryptPasswordEncoder()
在Kotlin中,object声明的类就类似Java中的单例类,当一个类被声明为object类后,使用该类的类名既可以表示这个类,又可以直接表示这个类的唯一单例对象,
所以直接使用MyPasswordEncoder.menthod()就好比MyPasswordEncoder.instance.menthod()
这样的话,我们之前所有需要使用PasswordEncoder这个对象的地方都可以替换为这个object类,方便统一管理。
2. 注册到SecurityConfig
新注册一个Bean:
@Bean
@Primary
fun myUsernamePasswordAuthenticationProvider(userDetailsService: UserDetailsService): AuthenticationProvider {
return MyUsernamePasswordAuthenticationProvider(userDetailsService)
}
然后在拦截链1中添加:
/**
* 调用拦截链1:拦截所有oauth2相关请求,交由oauth登录处理
*/
@Bean
@Order(1)
@Throws(java.lang.Exception::class)
fun authorizationServerSecurityFilterChain(
http: HttpSecurity,
+ myUsernamePasswordAuthenticationProvider: AuthenticationProvider,
): SecurityFilterChain {
val authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer()
http
.securityMatcher(authorizationServerConfigurer.endpointsMatcher)
.with(authorizationServerConfigurer, Customizer.withDefaults())
.authorizeHttpRequests { authorize ->
authorize.anyRequest().authenticated()
}
+ .authenticationProvider(myUsernamePasswordAuthenticationProvider)
.exceptionHandling { exceptions ->
exceptions
.defaultAuthenticationEntryPointFor(
LoginUrlAuthenticationEntryPoint("/login"),
MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
}
http
.getConfigurer(OAuth2AuthorizationServerConfigurer::class.java)
.oidc(Customizer.withDefaults())
return http.build()
}
重启服务,试下效果:
这里我们输入'pwd123456'会登录成功,而输入原本维护的密码'admin'会登录失败,证明逻辑生效。
总结
本文介绍了如何自定义登录页的登录认证逻辑,希望抛砖引玉,你还可以在自定义逻辑里面实现很多复杂的需求和操作,如域控验证登录,保留登录日志,与其他协议对接等,希望能对读者有所启发。