上期回顾
上篇文章我们从零创建了一个Spring Authorization Server项目,并验证使用了一下授权码模式的基本流程。但是当时我们使用的是官方的最简示例,登录用户和客户端信息均为内存(代码)写死,本期我们将使用数据库表来优化我们的客户端源和用户密码源。
使用自带表
Sping Authorization Server 为我们准备了自带表,位于他的jar包中:

要使用这些表,需要使用JDBC数据库对象。
在上一节中我们有两个关键Bean定义如下:
kotlin
// 用于检索用户进行身份验证
@Bean
fun userDetailsService(): UserDetailsService {
val userDetails = User.withUsername("admin")
.password(passwordEncoder().encode("admin"))
.roles("admin")
.build()
return InMemoryUserDetailsManager(userDetails)
}
//用于管理客户端
@Bean
fun registeredClientRepository(): RegisteredClientRepository {
......
return InMemoryRegisteredClientRepository(oidcClient)
}
可以看到我们返回的都是InMemoryXXX,也就是基于内存的存储方式,以客户端信息举例,我们需要改成:
kotlin
//用于管理客户端
@Bean
fun registeredClientRepository(jdbcTemplate: JdbcTemplate): RegisteredClientRepository {
// 方式3:自定义数据库查询
return JdbcRegisteredClientRepository(jdbcTemplate);
}
然后将oauth2-registered-client-schema.sql数据库脚本在你的数据库内执行就行了。
同理如果用户、密码、角色也想用这种方式管理,需要改动Bean:
kotlin
@Bean
fun userDetailsService(jdbcTemplate: JdbcTemplate): UserDetailsService {
val jdbcDao = JdbcDaoImpl()
jdbcDao.setJdbcTemplate(jdbcTemplate);
return jdbcDao;
}
具体的表我没找到,猜测是属于spring security的范畴,可以参考其他文章看一下。
自定义数据库表
以上为Spring Authorization Server官方为我们提供的方法,但是实际上JDBC已经是比较原始的做法了,现在我们基本上都是使用Mybatis或是Mybatis-Plus来做ORM,那么该如何使用我们自定义的库表呢。观察到上面两个Bean,分别返回UserDetailsService和RegisteredClientRepository两个类型,那么我们只需要构造两个类来继承这两个类即可,里面实现我们的自定义逻辑,以UserDetailsService举例:
kotlin
/**
* 自定义实现UserDetailsService逻辑
*/
class MyUserDetailsService(private val userService: IUserService) : UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails? {
return userService.listUsers(mapOf("account" to username))
.apply { if (isFail()) throw UsernameNotFoundException(msg) }
.data?.getOrNull(0)?.toUserDetails()
}
}
fun UserDTO.toUserDetails(): UserDetails {
return User.withUsername(account)
.password(password)
.apply { roleId?.split(",")?.let { roles(*it.toTypedArray()) } }
.build()
}
代码解析:这里我们定义了一个MyUserDetailsService 来继承UserDetailsService,继承后编辑器会自动提示要override一下loadUserByUsername方法,这个方法的意思是根据传入username(对应登录页的账号)来返回一个UserDetails,我使用一个自定义的userService 通过数据库查询来返回了一个用户实体(基于mybatis-plus),类型为UserDTO,然后通过扩展函数将UserDTO转换为所需的UserDetails,就实现了这个功能。
读者可以参考上面的逻辑实现自己的版本,比较简单。
接下来再介绍下RegisteredClientRepository的自定义方法:
kotlin
/**
* 自定义实现RegisteredClientRepository逻辑
*/
class MyRegisteredClientRepository : RegisteredClientRepository {
private val clientCache by lazy { SpringUtil.getBean(ClientCache::class.java) }
override fun save(registeredClient: RegisteredClient) {
TODO("Do not need to save via this way")
}
override fun findById(id: String): RegisteredClient? {
return clientCache.getById(id.toLong())?.toRegisteredClient()
}
override fun findByClientId(clientId: String): RegisteredClient? {
return clientCache.getByClientId(clientId)?.toRegisteredClient()
}
}
internal fun CcxiClient.toRegisteredClient(): RegisteredClient {
return RegisteredClient.withId(id.toString())
.clientId(clientId)
.clientSecret(BCryptPasswordEncoder().encode(clientSecret))
.clientAuthenticationMethods {
it.addAll(
setOf(
ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
ClientAuthenticationMethod.CLIENT_SECRET_POST,
ClientAuthenticationMethod.CLIENT_SECRET_JWT
)
)
}
.authorizationGrantTypes { methods ->
if (!authorizedGrantTypes.isNullOrBlank()) {
methods.addAll(authorizedGrantTypes!!.split(",").map { AuthorizationGrantType(it) }.toSet())
}
}
.redirectUri(webUri)
// .postLogoutRedirectUri("http://127.0.0.1:8080")
.scopes { it.addAll(setOf(OidcScopes.OPENID, OidcScopes.PROFILE)) }
.tokenSettings(
TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofSeconds(accessTokenValidity!!)) // 设置访问令牌有效期
.refreshTokenTimeToLive(Duration.ofDays(refreshTokenValidity!!)) // 设置刷新令牌有效期
//.accessTokenFormat(OAuth2TokenFormat.REFERENCE) // 这个设置是开启不透明token
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) // 使用透明token
.build()
)
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
.build()
}
其实和上面自定义用户是一样的思路,首先我的数据库内有自己定义的一张clients表,存储了各个业务系统的client_id,client_secret, webUri(前端首页),令牌刷新时间等关键信息,然后将自己的组件注入进来,查到自己定义的客户端数据体后,再转换为所需要的RegisteredClient对象,有些属性我们可能没维护,这种情况要么你数据库里维护全,要么就代码里写死。
和用户那里不同的是,这里的findByClientId或findById方法是会被高频调用的,所以我使用了缓存来返回,而不是每次都从数据库里查,可以提高效率。而且客户端信息一般也不会频繁变动,也不需要那么高的实时性。
总结
通过以上两步修改,你就可以使用数据库来存储客户端信息以及用户名和密码信息,这对于一个成熟的认证中心来讲至关重要。
从本节开始,文章重点讲解思路,不会给出完整可运行的代码,所谓授人以🐟不如授人以渔,还是希望能启发思考,学会自己做。(主要是要从项目里剥离与公司或其他模块有关的代码及工具类工作量比较大),如果读者发现哪里代码有遗漏或是不太懂,可以评论提问下,我会补充上来。