Spring Authorization Server 打造认证中心(四)自定义登录逻辑

145 阅读3分钟

上篇回顾

上篇文章我们讲解了如何自定义登录页样式,以及添加了对已登录状态的显示处理;在第二篇文章中我们学习过如何使用数据库内的用户信息,即自定义用户账号及密码数据源,但这些的基础逻辑都是用户在登录页使用用户名和密码登录,有时候我们可能需要更加复杂的逻辑,如使用域控来做登录验证(正常情况下我们是拿不到域控密码的,只能借助域控做密码验证);或是登录后可能还想做一些额外操作,如记录本次登录的账号、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()
    }

重启服务,试下效果:

image.png

这里我们输入'pwd123456'会登录成功,而输入原本维护的密码'admin'会登录失败,证明逻辑生效。

总结

本文介绍了如何自定义登录页的登录认证逻辑,希望抛砖引玉,你还可以在自定义逻辑里面实现很多复杂的需求和操作,如域控验证登录,保留登录日志,与其他协议对接等,希望能对读者有所启发。