ktorm + ktorm-ksp + springboot搭建RBAC后台模板 实践

2024-03-19 11:10

本文主要是介绍ktorm + ktorm-ksp + springboot搭建RBAC后台模板 实践,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文代码仓库:https://github.com/okfanger/ktorm-ksp-springboot-demo

0. 前言

使用kotlin写springboot是前几个月突然萌生的想法,起因是看到了 ktorm 官网里的一个截图:
在这里插入图片描述

没错,你可以一眼看出这款 orm 框架的特点,对我而言 这简直是太优雅了!这种写法深深吸引了我,即便我从来没有学过kotlin。为了深度体验 ktorm,我决定踩一下这个坑。

0.1. ktorm是什么?

ktorm官网:https://www.ktorm.org/zh-cn/

  1. 没有配置文件、没有 xml、没有注解、甚至没有任何第三方依赖、轻量级、简洁易用
  2. 强类型 SQL DSL,将低级 bug 暴露在编译期
  3. 灵活的查询,随心所欲地精确控制所生成的 SQL
  4. 实体序列 API,使用 filter、map、sortedBy 等序列函数进行查询,就像使用 Kotlin 中的原生集合一样方便
  5. 易扩展的设计,可以灵活编写扩展,支持更多运算符、数据类型、 SQL 函数、数据库方言等

0.2 ktorm-ksp又是什么?

用我的话讲就是:如果按照官方example,想正常使用ktorm,需要准备一个 实体类 + 一个表类,like this:

// 实体类
interface Department : Entity<Department> {companion object : Entity.Factory<Department>()val id: Intvar name: Stringvar location: String
}interface Employee : Entity<Employee> {companion object : Entity.Factory<Employee>()val id: Intvar name: Stringvar job: Stringvar manager: Employee?var hireDate: LocalDatevar salary: Longvar department: Department
}
// 表类
object Departments : Table<Department>("t_department") {val id = int("id").primaryKey().bindTo { it.id }val name = varchar("name").bindTo { it.name }val location = varchar("location").bindTo { it.location }
}object Employees : Table<Employee>("t_employee") {val id = int("id").primaryKey().bindTo { it.id }val name = varchar("name").bindTo { it.name }val job = varchar("job").bindTo { it.job }val managerId = int("manager_id").bindTo { it.manager.id }val hireDate = date("hire_date").bindTo { it.hireDate }val salary = long("salary").bindTo { it.salary }val departmentId = int("department_id").references(Departments) { it.department }
}

从而实现实体类与MySQL数据类型的映射。

ksp(Kotlin Symbol Processing)是一个开发工具,用于在Kotlin编译期间对符号进行处理和分析。它提供了一种便捷的方式来使用Kotlin语言的元编程能力,可以在编译时生成代码,从而减少运行时的开销和错误。

ktorm-ksp就是基于ksp,提供了一套代码生成工具,可以根据数据库的结构自动生成实体类和查询API,减少了手动编写和维护代码的工作量。

而如果使用 ktorm-ksp,上面的那个表类就不用去写,引用其他类的部分只需要用 注解注明即可。like this:

@Table("department")
interface Department : Entity<Department> {companion object : Entity.Factory<Department>()@PrimaryKeyval id: Intvar name: Stringvar location: String
}@Table("employee")
interface Employee : Entity<Employee> {companion object : Entity.Factory<Employee>()@PrimaryKeyval id: Intvar name: Stringvar job: Stringvar manager: Employee?var hireDate: LocalDatevar salary: Long@Column(isReferences = true)var department: Department
}

0.3 考古发现的一个比较有意思的issue

在 ktorm的官方github仓库,可以看到这条issue:https://github.com/kotlin-orm/ktorm/issues/373

在这里插入图片描述

写于 2022年2月22日,随后官方回复:
在这里插入图片描述

于是你可以在 ktorm-ksp 仓库里找到这位作者的提交记录:

在这里插入图片描述

不禁感叹,这位大佬前辈的执行力杠杠的,要是我可能就半路弃坑了。
事实上,我是从ktorm官网的某个条目里找到ksp的,但是在笔者写这篇博文的时候,目录上已经搜不到ksp的页面了,不清楚具体的原因。。。(可能是我幻视了?

1. 准备环境

1.1 一个 gradle

我知道大家都习惯用maven了,但是如果你的项目中需要用到ktorm-ksp,你必须用gradle作为依赖管理工具(我也不清楚为什么
当然,下载gradle也是有技巧的,我推荐你去 腾讯云镜像站 下载 gradle的release版本:https://mirrors.cloud.tencent.com/gradle/

注意:不管下载哪个版本,都选那个带 all的!!!(因为如果不是all的版本,idea加载的时候还是会去重复下载TUT

以 8.6 版本为例:
在这里插入图片描述

下载完毕后解压到某个目录,然后在idea里配置:

在这里插入图片描述

1.2 配置 gradle 镜像

和maven配置aliyun镜像一样,只是略微的区别。
首先你要在 用户主目录 下找到 .gradle 目录(如果没有就新建一个),然后在里面新建一个 init.gradle 文件,并填充以下代码:

allprojects {buildscript {repositories {maven { url 'https://maven.aliyun.com/repository/public/' }maven { url 'https://maven.aliyun.com/repository/google/' }}}repositories {maven { url 'https://maven.aliyun.com/repository/public/' }maven { url 'https://maven.aliyun.com/repository/google/' }}println "${it.name}: Aliyun maven mirror injected"
}

保存即可。

1.3 JDK >= 8

本文的demo里 没有用到 SpringBoot 3.x,8就够用。

1.4 MySQL >= 8.0

不必多说

2. 先搭一个 SpringBoot 初始框架(kotlin版

2.1 新建项目

首先我们打开idea,新建项目,选择 gradle,选择 kotlin

在这里插入图片描述

2.2 依赖项(已包含之后所有需要的依赖

// build.gradle.kts
plugins {id("org.springframework.boot") version "2.7.4"id("io.spring.dependency-management") version "1.1.4"id("com.google.devtools.ksp") version "1.9.0-1.0.13"kotlin("jvm") version "1.9.21"kotlin("plugin.spring") version "1.9.21"
}group = "org.example"
version = "1.0-SNAPSHOT"repositories {mavenCentral()
}dependencies {// Spring Bootimplementation("org.springframework.boot:spring-boot-starter-web")implementation("com.fasterxml.jackson.module:jackson-module-kotlin")implementation("org.jetbrains.kotlin:kotlin-reflect")implementation("org.springframework.boot:spring-boot-starter-aop")compileOnly("org.projectlombok:lombok")annotationProcessor("org.projectlombok:lombok")developmentOnly("org.springframework.boot:spring-boot-devtools")testImplementation("org.springframework.boot:spring-boot-starter-test")testImplementation("org.jetbrains.kotlin:kotlin-test")// JDBC + Ktormimplementation("org.ktorm:ktorm-core:3.6.0")implementation("org.springframework.boot:spring-boot-starter-jdbc")implementation("com.fasterxml.jackson.module:jackson-module-kotlin")implementation("com.mysql:mysql-connector-j:8.3.0")implementation("org.ktorm:ktorm-core:3.6.0")implementation("org.ktorm:ktorm-jackson:3.6.0")implementation("org.ktorm:ktorm-support-mysql:3.6.0")// Ktorm-KSPimplementation("org.ktorm:ktorm-ksp-api:1.0.0-RC3")ksp("org.ktorm:ktorm-ksp-compiler:1.0.0-RC3")// jwtimplementation("com.auth0:java-jwt:3.18.1")
}tasks.test {useJUnitPlatform()
}
kotlin {jvmToolchain(8)
}

此处有概率ksp("org.ktorm:ktorm-ksp-compiler:1.0.0-RC3")这个包加载不出来(比如我
我的解决方案是去 mvnrepository.com 下载jar包,然后通过 gradle的本地引过去。
如果你不幸遇到了这样的事情,你可以参考我的配置:

// 把 下载好的jar包放在 src/main/resources/lib/ 下
...
implementation(files("src/main/resources/lib/ktorm-ksp-api-1.0.0-RC3.jar"))
...

2.3 启动类

// com.example.Application.kt
package com.exampleimport org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication@SpringBootApplication
open class Applicationfun main(args: Array<String>) {runApplication<Application>(*args)
}

2.5 自定义异常类和错误枚举

// com.example.common.BizCode
package com.example.commonenum class BizCode(val code: Int, val msg: String) {SUCCESS(200, "成功"),USER_NOT_FOUND(40101, "用户不存在"),PWD_WRONG(40102, "密码错误"),USER_EXIST(40103, "用户已存在"),NO_AUTH(40104, "未登录"),ACCESS_DENIED(40301, "权限不足"),SYSTEM_ERROR(50000, "系统错误"),
}
// com.example.common.BizException
package com.example.commonimport com.example.common.BizCode
class BizException(val code: Int, val msg: String) : RuntimeException(msg) {constructor(bizCode: BizCode) : this(bizCode.code, bizCode.msg)
}

2.4 封装统一返回类

// com.example.common.ApiRes
package com.example.commondata class ApiRes<T>(val success: Boolean,val data: T? = null,val msg: String,val code: Int
) {companion object {fun <T> ok(data: T): ApiRes<T> {return ApiRes(true, data, BizCode.SUCCESS.msg, BizCode.SUCCESS.code)}fun <T> error(bizCode: BizCode): ApiRes<T> {return ApiRes(false, null, bizCode.msg, bizCode.code)}fun <T> error(code: Int, msg: String): ApiRes<T> {return ApiRes(false, null, msg, code)}}
}

2.5 全局异常处理

// com.example.exception.GlobalExceptionHandler
package com.example.exceptionimport com.example.common.ApiRes
import com.example.common.BizCode
import com.example.common.BizException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseBody
import org.springframework.web.bind.annotation.RestControllerAdvice@RestControllerAdvice
@ResponseBody
class GlobalExceptionHandler {@ExceptionHandler(BizException::class)fun handleBizException(ex: BizException): ApiRes<Nothing> {return ApiRes.error(ex.code, ex.msg)}@ExceptionHandler(RuntimeException::class)fun handleRuntimeException(ex: RuntimeException): ApiRes<Nothing> {ex.printStackTrace()return ApiRes.error(BizCode.SYSTEM_ERROR.code, ex.message ?: BizCode.SYSTEM_ERROR.msg)}
}

2.6 BizThrow 扩展函数

// com.example.common.BizThrow
package com.example.commonfun <T> T.okk(): ApiRes<T> {return ApiRes.ok(this)
}
fun Boolean.thenThrow(exception: BizException) {if (this) {throw exception}
}fun Boolean.thenThrow(bizCode: BizCode) {return this.thenThrow(BizException(bizCode))
}fun Boolean.failThenThrow(bizCode: BizCode) {if (!this) {throw BizException(bizCode)}
}fun justThrow(bizCode: BizCode) {throw BizException(bizCode)
}inline fun <T> tryGetOrThrow(block: () -> T, bizCode: BizCode): T {return try {block()} catch (e: Exception) {throw BizException(bizCode)}
}inline fun <T> tryOrThrow(block: () -> T, bizCode: BizCode) {try {block()} catch (e: Exception) {throw BizException(bizCode)}
}inline fun <T> tryOrElse(block: () -> T, default: T): T {return try {block()} catch (e: Exception) {default}
}inline fun <T> tryGetOrElse(block: () -> T, default: T): T {return try {block()} catch (e: Exception) {default}
}

3. 设计模型

sys_user id bigint uid varchar password varchar createAt datetime updateAt datetime sys_user_role id bigint userId bigint roleId bigint createAt datetime updateAt datetime sys_role id bigint name varchar createAt datetime updateAt datetime sys_role_permission id bigint roleId bigint permissionId bigint createAt datetime updateAt datetime sys_permission id bigint name varchar createAt datetime updateAt datetime 1 1 1 1
create database if not exists `ktorm-ksp-springboot-demo`;
create table sys_user
(id       bigint auto_increment comment 'id'primary key,uid      varchar(64)                        not null comment '用户唯一id',password varchar(255)                       not null comment '密码',createAt datetime default CURRENT_TIMESTAMP null comment '创建时间',updateAt datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间'
)comment '用户表';INSERT INTO sys_user (id, uid, password, createAt, updateAt)
VALUES (1, 'okfang', '12345', '2024-02-20 23:51:30', '2024-02-20 23:51:38');create table sys_role
(id       bigint auto_increment comment 'id'primary key,name     varchar(64)                        not null comment '角色名',createAt datetime default CURRENT_TIMESTAMP null comment '创建时间',updateAt datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间'
)comment '角色表';INSERT INTO sys_role (id, name, createAt, updateAt)
VALUES (1, 'admin', '2024-02-20 23:51:43', '2024-02-20 23:51:43');create table sys_permission
(id       bigint auto_increment comment 'id'primary key,name     varchar(64)                        not null comment '权限名',createAt datetime default CURRENT_TIMESTAMP null comment '创建时间',updateAt datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间'
)comment '权限表';INSERT INTO sys_permission (id, name, createAt, updateAt)
VALUES (1, 'auth:info', '2024-02-20 23:52:37', '2024-02-20 23:52:37');
create table sys_role_permission
(id           bigint auto_increment comment 'id'primary key,roleId       bigint                             not null comment '角色id',permissionId bigint                             not null comment '权限id',createAt     datetime default CURRENT_TIMESTAMP null comment '创建时间',updateAt     datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',constraint fk_role_perm_permforeign key (permissionId) references sys_permission (id),constraint fk_role_perm_roleforeign key (roleId) references sys_role (id)
)comment '角色权限关联表';INSERT INTO sys_role_permission (id, roleId, permissionId, createAt, updateAt)
VALUES (2, 1, 1, '2024-02-20 23:52:42', '2024-02-20 23:52:42');
create table sys_user_role
(id       bigint auto_increment comment 'id'primary key,userId   bigint                             not null comment '用户id',roleId   bigint                             not null comment '角色id',createAt datetime default CURRENT_TIMESTAMP null comment '创建时间',updateAt datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',constraint fk_user_role_roleforeign key (roleId) references sys_role (id),constraint fk_user_role_userforeign key (userId) references sys_user (id)
)comment '用户角色关联表';INSERT INTO sys_user_role (id, userId, roleId, createAt, updateAt)
VALUES (1, 1, 1, '2024-02-20 23:51:49', '2024-02-20 23:51:49');

4. ktorm配置与实体类

参考文档:https://www.ktorm.org/zh-cn/quick-start.html

4.1 配置 DataSource

// com.example.config.KtormConfiguration
package com.example.configimport org.ktorm.database.Database
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import javax.sql.DataSource@Configuration
class KtormConfiguration {@Autowiredlateinit var dataSource: DataSource@Beanfun database(): Database {return Database.connectWithSpringSupport(dataSource)}
}

对应的,你需要在 application.yml 里添加:

spring:datasource:url: jdbc:mysql://localhost:3306/ktorm-ksp-springboot-demousername: rootpassword:

4.2 配置 Jackson

// com.example.config.JacksonConfiguration
package com.example.configimport com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.Module
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer
import org.ktorm.jackson.KtormModule
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
import java.text.SimpleDateFormat@Configuration
class JacksonConfiguration {fun ktormModule(): Module {return KtormModule()}@Beanfun jacksonObjectMapper(builder: Jackson2ObjectMapperBuilder): ObjectMapper {// Long 转 String 精度问题val module = SimpleModule().apply {addSerializer(Long::class.javaObjectType, ToStringSerializer.instance)addSerializer(Long::class.javaPrimitiveType, ToStringSerializer.instance)}return builder.createXmlMapper(false).build<ObjectMapper>().apply {//对象的所有字段全部列入setSerializationInclusion(JsonInclude.Include.ALWAYS)//取消默认转换timestamps形式configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)//忽略空Bean转json的错误configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)//所有的日期格式都统一为以下的样式,即yyyy-MM-dd HH:mm:sssetDateFormat(SimpleDateFormat("yyyy-MM-dd HH:mm:ss"))//忽略 在json字符串中存在,但是在java对象中不存在对应属性的情况。防止错误configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)registerModule(module)registerModule(ktormModule())}}
}

4.3 封装 BaseEntity

这个类存在的意义是,把一些公有的属性放一块,比如 createAtupdateAt

// com.example.model.base.BaseEntity
package com.example.model.baseimport org.ktorm.entity.Entity
import java.time.LocalDateTimeinterface BaseEntity<T : Entity<T>> : Entity<T> {var createAt: LocalDateTimevar updateAt: LocalDateTime
}

下面这个是给 DTO 类、VO类用的

// com.example.model.base.BaseJavaEntity
package com.example.model.baseimport java.time.LocalDateTimeabstract class BaseJavaEntity {abstract var createAt: LocalDateTime;abstract var updateAt: LocalDateTime;
}

4.4 SysUser

// com.example.model.sys.SysUser
package com.example.model.sysimport com.example.model.base.BaseEntity
import org.ktorm.entity.Entity
import org.ktorm.ksp.api.PrimaryKey
import org.ktorm.ksp.api.Table@Table("sys_user")
interface SysUser : BaseEntity<SysUser> {@PrimaryKeyvar id: Longvar uid: Stringvar password: Stringcompanion object : Entity.Factory<SysUser>()
}

4.5 SysRole

// com.example.model.sys.SysRole
package com.example.model.sysimport com.example.model.base.BaseEntity
import org.ktorm.entity.Entity
import org.ktorm.ksp.api.PrimaryKey
import org.ktorm.ksp.api.Table@Table("sys_role")
interface SysRole : BaseEntity<SysRole> {@PrimaryKeyvar id: Longvar name: Stringcompanion object : Entity.Factory<SysRole>()
}

4.6 SysPermission

// com.example.model.sys.SysPermission
package com.example.model.sysimport com.example.model.base.BaseEntity
import org.ktorm.entity.Entity
import org.ktorm.ksp.api.PrimaryKey
import org.ktorm.ksp.api.Table@Table("sys_permission")
interface SysPermission : BaseEntity<SysPermission> {@PrimaryKeyvar id: Longvar name: Stringcompanion object : Entity.Factory<SysPermission>()
}

4.7 SysRolePermission

// com.example.model.sys.SysRolePermssion
package com.example.model.sysimport com.example.model.base.BaseEntity
import org.ktorm.entity.Entity
import org.ktorm.ksp.api.PrimaryKey
import org.ktorm.ksp.api.Table@Table("sys_role_permission")
interface SysRolePermission : BaseEntity<SysRolePermission> {@PrimaryKeyvar id: Longvar roleId: Longvar permissionId: Longcompanion object : Entity.Factory<SysRolePermission>()
}

4.8 SysUserRole

// com.example.model.sys.SysUserRole
package com.example.model.sysimport com.example.model.base.BaseEntity
import org.ktorm.entity.Entity
import org.ktorm.ksp.api.PrimaryKey
import org.ktorm.ksp.api.Table@Table("sys_user_role")
interface SysUserRole : BaseEntity<SysUserRole> {@PrimaryKeyvar id: Longvar userId: Longvar roleId: Longcompanion object : Entity.Factory<SysUserRole>()
}

4.9 RoleVO & PermissionVO

// com.example.model.sys.PermissionVO
package com.example.model.sysdata class PermissionVO(val id: Long,val name: String
)
// com.example.model.sys.RoleVO
package com.example.model.sysdata class RoleVO(val id: Long,val name: String,val permissions: List<PermissionVO>
)

4.10 UserVO

// com.example.model.sys.UserVO
package com.example.model.sysimport com.example.common.Need
import com.example.model.base.BaseJavaEntity
import java.time.LocalDateTimedata class UserVO(val id: Long,val uid: String,var roles: List<RoleVO>,override var createAt: LocalDateTime,override var updateAt: LocalDateTime,
) : BaseJavaEntity() {fun hasPermission(need: Need): Boolean {with(need) {if (allOf.isNotEmpty()) {for (it in allOf) {if (!hasPermission(it))return false}return true} else if (anyOf.isNotEmpty()) {for (it in anyOf) {if (hasPermission(it))return true}return false} else {return false}}}private fun hasPermission(permissionName: String): Boolean {return roles.any { roleIt ->roleIt.permissions.any { it.name == permissionName }}}
}fun SysUser.toVO(roles: List<RoleVO> = emptyList()): UserVO {return UserVO(this.id, this.uid, roles, this.createAt, this.updateAt)
}

4.11 LoginRequest & TokenDTO

// com.example.model.sys.UserLoginRequest
package com.example.model.sysdata class UserLoginRequest(val uid: String,val password: String
)
// com.example.model.sys.TokenDTO
package com.example.model.sysdata class TokenDTO(val token: String) {
}

5. DAO层

5.1 封装BaseDAO

package com.example.daoimport org.ktorm.database.Database
import org.ktorm.dsl.QuerySource
import org.ktorm.dsl.from
import org.ktorm.entity.*
import org.ktorm.schema.ColumnDeclaring
import org.ktorm.schema.Table
import javax.annotation.Resourceabstract class BaseDAO<E : Entity<E>, T : Table<E>>(private val tableObject: T) {@Resourceprotected lateinit var database: Databaseopen fun add(entity: E): Int {return database.sequenceOf(tableObject).add(entity)}open fun update(entity: E): Int {return database.sequenceOf(tableObject).update(entity)}open fun deleteIf(predicate: (T) -> ColumnDeclaring<Boolean>): Int {return database.sequenceOf(tableObject).removeIf(predicate)}open fun allMatched(predicate: (T) -> ColumnDeclaring<Boolean>): Boolean {return database.sequenceOf(tableObject).all(predicate)}open fun anyMatched(predicate: (T) -> ColumnDeclaring<Boolean>): Boolean {return database.sequenceOf(tableObject).any(predicate)}open fun noneMatched(predicate: (T) -> ColumnDeclaring<Boolean>): Boolean {return database.sequenceOf(tableObject).none(predicate)}open fun count(): Int {return database.sequenceOf(tableObject).count()}open fun count(predicate: (T) -> ColumnDeclaring<Boolean>): Int {return database.sequenceOf(tableObject).count(predicate)}open fun findOne(predicate: (T) -> ColumnDeclaring<Boolean>): E? {return database.sequenceOf(tableObject).find(predicate)}open fun findList(predicate: (T) -> ColumnDeclaring<Boolean>): List<E> {return database.sequenceOf(tableObject).filter(predicate).toList()}open fun findAll(): List<E> {return database.sequenceOf(tableObject).toList()}open fun getDSL(): QuerySource {return database.from(tableObject)}open fun getSequence(): EntitySequence<E, T> {return database.sequenceOf(tableObject)}}

5.2 SysUserDAO

package com.example.dao.sysimport com.example.dao.base.BaseDAO
import com.example.model.sys.*
import org.ktorm.dsl.*
import org.springframework.stereotype.Repository@Repository
class SysUserDAO : BaseDAO<SysUser, SysUsers>(SysUsers) {fun getRoleVOsByUserIds(ids: List<Long>): List<RoleVO> {return getRolesByUserIds(ids).let { roleList ->val roleIds = roleList.map { it.id }val rolePermissionsMap = getRolePermissionsMapByRoleIds(roleIds)roleList.map { role ->val permissions = rolePermissionsMap[role.id]?.map { permission ->PermissionVO(permission.id, permission.name)} ?: emptyList()RoleVO(role.id, role.name, permissions)}}}private fun getRolesByUserIds(ids: List<Long>): List<SysRole> {return database.from(SysUserRoles).leftJoin(SysRoles, SysUserRoles.roleId eq SysRoles.id).select(SysRoles.columns).where { SysUserRoles.userId inList ids }.map { SysRoles.createEntity(it) }}private fun getRolePermissionsMapByRoleIds(roleIds: List<Long>): Map<Long, List<SysPermission>> {return database.from(SysRolePermissions).leftJoin(SysPermissions, SysRolePermissions.permissionId eq SysPermissions.id).selectDistinct(SysRolePermissions.roleId, SysPermissions.name, SysPermissions.id).where { SysRolePermissions.roleId inList roleIds }.map { it[SysRolePermissions.roleId] to SysPermissions.createEntity(it) }.toList().map { (it.first ?: 0L) to it.second }.let { pairs ->pairs.groupBy({ it.first }, { it.second })}}
}

6. JWT

6.1 Jwt Properties

// com.example.common.JwtProperties
package com.example.commonimport org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component@Component
@ConfigurationProperties(prefix = "jwt")
data class JwtProperties(var header: String? = null,var tokenHead: String? = null,var secret: String? = null
)

对应 application.yml的配置:

jwt:header: "Authorization"tokenHead: "Bearer "secret: GoodMorning

6.2 JwtUtil

// com.example.util.JwtUtil
package com.example.utilimport com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.interfaces.Claim
import com.example.common.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import javax.servlet.http.HttpServletRequest@Component
class JwtUtil @Autowired constructor(val jwtProperties: JwtProperties) {fun createToken(payload: Map<String, Any>): String {return JWT.create().withPayload(payload).sign(Algorithm.HMAC512(jwtProperties.secret))}fun validateToken(authToken: String?): Boolean {return tryGetOrElse({JWT.require(Algorithm.HMAC512(jwtProperties.secret)).build().verify(authToken)true}, false)}fun parseToken(token: String): Map<String?, Claim?>? {return token.replace(jwtProperties.tokenHead!!, "").trim().let {validateToken(it).failThenThrow(BizCode.NO_AUTH)tryGetOrThrow({ JWT.decode(it).claims }, BizCode.NO_AUTH)}}fun parseToken(httpRequest: HttpServletRequest): Map<String?, Claim?>? {return tryGetOrThrow({parseToken(httpRequest.getHeader(jwtProperties.header!!))}, BizCode.NO_AUTH)}
}

7. 自定义权限注解 + ThreadLocal + AOP

7.1 自定义权限注解 @Need

// com.example.common.Need
package com.example.commonimport org.springframework.web.bind.annotation.RequestMapping
import java.lang.annotation.Inherited@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
@Inherited
annotation class Need(val anyOf: Array<String> = [],val allOf: Array<String> = []
)

7.2 AuthContextHolder

// com.example.common.AuthContextHolder
package com.example.commonimport com.example.model.sys.UserVOobject AuthContextHolder {private val authLocalThread = ThreadLocal<UserVO>()fun getUser(): UserVO {return authLocalThread.get()}fun setUser(sysUserInfo: UserVO) {authLocalThread.set(sysUserInfo)}fun clear() {authLocalThread.remove()}
}

7.3 AuthAop

// com.example.aop.AuthAop
package com.example.aopimport com.example.common.*
import com.example.dao.sys.SysUserDAO
import com.example.model.sys.SysUser
import com.example.model.sys.toVO
import com.example.util.JwtUtil
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Pointcut
import org.ktorm.dsl.eq
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes@Aspect
@Component
class AuthAop {@Autowiredprivate lateinit var jwtUtil: JwtUtil@Autowiredprivate lateinit var sysUserDAO: SysUserDAO;@Pointcut("@annotation(com.example.common.Need)")fun permissionCheck() {}@Around("permissionCheck()")fun around(joinPoint: ProceedingJoinPoint): Any? {// 获取当前请求的用户idval userId = (RequestContextHolder.getRequestAttributes() as ServletRequestAttributes).request.let { jwtUtil.parseToken(it) }.let { it?.get("id")?.asLong()!! }// 获取当前请求的用户信息val user = sysUserDAO.findOne { it.id eq userId }.let {it ?: justThrow(BizCode.USER_NOT_FOUND)it as SysUser}// 获得当前方法上的 @Need 注解val need = joinPoint.target.javaClass.getMethod(joinPoint.signature.name).getAnnotation(Need::class.java)// 获取当前请求的用户权限val roleVOs = sysUserDAO.getRoleVOsByUserIds(listOf(user.id))val userVo = user.toVO(roleVOs)userVo.hasPermission(need).failThenThrow(BizCode.ACCESS_DENIED)try {AuthContextHolder.setUser(userVo)return joinPoint.proceed()} finally {// 清除当前请求的用户信息AuthContextHolder.clear()}}}

8. AuthController

// com.example.controller.AuthController
package com.example.controllerimport com.example.common.*
import com.example.dao.sys.SysUserDAO
import com.example.model.sys.TokenDTO
import com.example.model.sys.UserLoginRequest
import com.example.model.sys.UserVO
import com.example.util.JwtUtil
import org.ktorm.dsl.eq
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.*@RequestMapping("/auth")
@RestController
class AuthController {@Autowiredprivate lateinit var sysUserDAO: SysUserDAO@Autowiredprivate lateinit var jwtUtil: JwtUtil@PostMapping(value = ["/login"])fun login(@RequestBody body: UserLoginRequest): ApiRes<TokenDTO> {return sysUserDAO.findOne { it.uid eq body.uid }.let {// 如果用户不存在则抛出用户不存在异常it ?: justThrow(BizCode.USER_NOT_FOUND)// 如果密码不匹配则抛出密码错误异常(it!!.password != body.password).thenThrow(BizCode.PWD_WRONG)// 创建token并返回jwtUtil.createToken(buildMap {put("uid", it.uid)put("id", it.id)}).let { token -> TokenDTO(token).okk() }}}@GetMapping("/info")@Need(allOf = ["auth:info"])fun info(): ApiRes<UserVO> {return AuthContextHolder.getUser().okk()}
}

9. 效果测试

9.1 登录

9.1.1 成功

在这里插入图片描述

9.1.2 密码错误

在这里插入图片描述

9.1.3 用户不存在

在这里插入图片描述

9.2 用户信息查看

9.2.1 未登录(未携带Token

在这里插入图片描述

9.2.2 成功(带Token

在这里插入图片描述
在这里插入图片描述

10. 代码

https://github.com/okfanger/ktorm-ksp-springboot-demo

这篇关于ktorm + ktorm-ksp + springboot搭建RBAC后台模板 实践的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/825788

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2