Spring Authorization Server 打造认证中心(二)自定义数据库表

上期回顾

上篇文章我们从零创建了一个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,分别返回UserDetailsServiceRegisteredClientRepository两个类型,那么我们只需要构造两个类来继承这两个类即可,里面实现我们的自定义逻辑,以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_idclient_secret, webUri(前端首页),令牌刷新时间等关键信息,然后将自己的组件注入进来,查到自己定义的客户端数据体后,再转换为所需要的RegisteredClient对象,有些属性我们可能没维护,这种情况要么你数据库里维护全,要么就代码里写死。

和用户那里不同的是,这里的findByClientIdfindById方法是会被高频调用的,所以我使用了缓存来返回,而不是每次都从数据库里查,可以提高效率。而且客户端信息一般也不会频繁变动,也不需要那么高的实时性。

总结

通过以上两步修改,你就可以使用数据库来存储客户端信息以及用户名和密码信息,这对于一个成熟的认证中心来讲至关重要。

从本节开始,文章重点讲解思路,不会给出完整可运行的代码,所谓授人以🐟不如授人以渔,还是希望能启发思考,学会自己做。(主要是要从项目里剥离与公司或其他模块有关的代码及工具类工作量比较大),如果读者发现哪里代码有遗漏或是不太懂,可以评论提问下,我会补充上来。

相关推荐
QING61838 分钟前
Jetpack Compose Brush API 详解 —— 新手指南
android·kotlin·android jetpack
Lovely_Ruby1 小时前
前端er Go-Frame 的学习笔记:实现 to-do 功能(一)
前端·后端
喵个咪1 小时前
初学者导引:在 Go-Kratos 中用 go-crud 实现 Ent ORM CRUD 操作
后端·go
计算机毕设匠心工作室1 小时前
【python大数据毕设实战】全国健康老龄化数据分析系统、Hadoop、计算机毕业设计、包括数据爬取、数据分析、数据可视化、机器学习
后端·python
v***87041 小时前
Spring Boot实现多数据源连接和切换
spring boot·后端·oracle
哈哈哈笑什么1 小时前
企业级追踪业务数据变动的通用组件
后端
毕设源码-郭学长1 小时前
【开题答辩全过程】以 高校教室管理系统为例,包含答辩的问题和答案
java·spring boot
稚辉君.MCA_P8_Java1 小时前
Gemini永久会员 go数组中最大异或值
数据结构·后端·算法·golang·哈希算法
Moe4881 小时前
Spring Boot启动魔法:SpringApplication.run()源码全流程拆解
java·后端·面试