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

相关文章

SpringMVC获取请求参数的方法

《SpringMVC获取请求参数的方法》:本文主要介绍SpringMVC获取请求参数的方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下... 目录1、通过ServletAPI获取2、通过控制器方法的形参获取请求参数3、@RequestParam4、@

SpringBoot应用中出现的Full GC问题的场景与解决

《SpringBoot应用中出现的FullGC问题的场景与解决》这篇文章主要为大家详细介绍了SpringBoot应用中出现的FullGC问题的场景与解决方法,文中的示例代码讲解详细,感兴趣的小伙伴可... 目录Full GC的原理与触发条件原理触发条件对Spring Boot应用的影响示例代码优化建议结论F

springboot项目中常用的工具类和api详解

《springboot项目中常用的工具类和api详解》在SpringBoot项目中,开发者通常会依赖一些工具类和API来简化开发、提高效率,以下是一些常用的工具类及其典型应用场景,涵盖Spring原生... 目录1. Spring Framework 自带工具类(1) StringUtils(2) Coll

Python 中的 with open文件操作的最佳实践

《Python中的withopen文件操作的最佳实践》在Python中,withopen()提供了一个简洁而安全的方式来处理文件操作,它不仅能确保文件在操作完成后自动关闭,还能处理文件操作中的异... 目录什么是 with open()?为什么使用 with open()?使用 with open() 进行

SpringBoot条件注解核心作用与使用场景详解

《SpringBoot条件注解核心作用与使用场景详解》SpringBoot的条件注解为开发者提供了强大的动态配置能力,理解其原理和适用场景是构建灵活、可扩展应用的关键,本文将系统梳理所有常用的条件注... 目录引言一、条件注解的核心机制二、SpringBoot内置条件注解详解1、@ConditionalOn

通过Spring层面进行事务回滚的实现

《通过Spring层面进行事务回滚的实现》本文主要介绍了通过Spring层面进行事务回滚的实现,包括声明式事务和编程式事务,具有一定的参考价值,感兴趣的可以了解一下... 目录声明式事务回滚:1. 基础注解配置2. 指定回滚异常类型3. ​不回滚特殊场景编程式事务回滚:1. ​使用 TransactionT

Spring LDAP目录服务的使用示例

《SpringLDAP目录服务的使用示例》本文主要介绍了SpringLDAP目录服务的使用示例... 目录引言一、Spring LDAP基础二、LdapTemplate详解三、LDAP对象映射四、基本LDAP操作4.1 查询操作4.2 添加操作4.3 修改操作4.4 删除操作五、认证与授权六、高级特性与最佳

Spring Shell 命令行实现交互式Shell应用开发

《SpringShell命令行实现交互式Shell应用开发》本文主要介绍了SpringShell命令行实现交互式Shell应用开发,能够帮助开发者快速构建功能丰富的命令行应用程序,具有一定的参考价... 目录引言一、Spring Shell概述二、创建命令类三、命令参数处理四、命令分组与帮助系统五、自定义S

SpringSecurity JWT基于令牌的无状态认证实现

《SpringSecurityJWT基于令牌的无状态认证实现》SpringSecurity中实现基于JWT的无状态认证是一种常见的做法,本文就来介绍一下SpringSecurityJWT基于令牌的无... 目录引言一、JWT基本原理与结构二、Spring Security JWT依赖配置三、JWT令牌生成与

Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码

《Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码》:本文主要介绍Java中日期时间转换的多种方法,包括将Date转换为LocalD... 目录一、Date转LocalDateTime二、Date转LocalDate三、LocalDateTim