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: Int
    var name: String
    var location: String
}

interface Employee : Entity<Employee> {
    companion object : Entity.Factory<Employee>()
    val id: Int
    var name: String
    var job: String
    var manager: Employee?
    var hireDate: LocalDate
    var salary: Long
    var 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>()
    @PrimaryKey
    val id: Int
    var name: String
    var location: String
}

@Table("employee")
interface Employee : Entity<Employee> {
    companion object : Entity.Factory<Employee>()
    @PrimaryKey
    val id: Int
    var name: String
    var job: String
    var manager: Employee?
    var hireDate: LocalDate
    var 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 Boot
    implementation("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 + Ktorm
    implementation("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-KSP
    implementation("org.ktorm:ktorm-ksp-api:1.0.0-RC3")
    ksp("org.ktorm:ktorm-ksp-compiler:1.0.0-RC3")

    // jwt
    implementation("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.example

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
open class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

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

// com.example.common.BizCode
package com.example.common

enum 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.common

import 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.common

data 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.exception

import 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.common


fun <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_perm
        foreign key (permissionId) references sys_permission (id),
    constraint fk_role_perm_role
        foreign 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_role
        foreign key (roleId) references sys_role (id),
    constraint fk_user_role_user
        foreign 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.config

import 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 {

    @Autowired
    lateinit var dataSource: DataSource

    @Bean
    fun database(): Database {
        return Database.connectWithSpringSupport(dataSource)
    }
}

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

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/ktorm-ksp-springboot-demo
    username: root
    password:

4.2 配置 Jackson

// com.example.config.JacksonConfiguration
package com.example.config

import 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()
    }

    @Bean
    fun 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:ss
                setDateFormat(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.base

import org.ktorm.entity.Entity
import java.time.LocalDateTime

interface BaseEntity<T : Entity<T>> : Entity<T> {
    var createAt: LocalDateTime
    var updateAt: LocalDateTime
}

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

// com.example.model.base.BaseJavaEntity
package com.example.model.base

import java.time.LocalDateTime

abstract class BaseJavaEntity {
    abstract var createAt: LocalDateTime;
    abstract var updateAt: LocalDateTime;
}

4.4 SysUser

// com.example.model.sys.SysUser
package com.example.model.sys

import 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> {
    @PrimaryKey
    var id: Long

    var uid: String
    var password: String

    companion object : Entity.Factory<SysUser>()
}

4.5 SysRole

// com.example.model.sys.SysRole
package com.example.model.sys

import 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> {
    @PrimaryKey
    var id: Long
    var name: String

    companion object : Entity.Factory<SysRole>()
}

4.6 SysPermission

// com.example.model.sys.SysPermission
package com.example.model.sys

import 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> {
    @PrimaryKey
    var id: Long
    var name: String

    companion object : Entity.Factory<SysPermission>()
}

4.7 SysRolePermission

// com.example.model.sys.SysRolePermssion
package com.example.model.sys

import 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> {
    @PrimaryKey
    var id: Long
    var roleId: Long
    var permissionId: Long

    companion object : Entity.Factory<SysRolePermission>()
}

4.8 SysUserRole

// com.example.model.sys.SysUserRole
package com.example.model.sys

import 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> {
    @PrimaryKey
    var id: Long
    var userId: Long
    var roleId: Long

    companion object : Entity.Factory<SysUserRole>()
}

4.9 RoleVO & PermissionVO

// com.example.model.sys.PermissionVO
package com.example.model.sys

data class PermissionVO(
    val id: Long,
    val name: String
)
// com.example.model.sys.RoleVO
package com.example.model.sys

data class RoleVO(
    val id: Long,
    val name: String,
    val permissions: List<PermissionVO>
)

4.10 UserVO

// com.example.model.sys.UserVO
package com.example.model.sys

import com.example.common.Need
import com.example.model.base.BaseJavaEntity
import java.time.LocalDateTime

data 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.sys

data class UserLoginRequest(
    val uid: String,
    val password: String
)
// com.example.model.sys.TokenDTO
package com.example.model.sys

data class TokenDTO(val token: String) {
}

5. DAO层

5.1 封装BaseDAO

package com.example.dao

import 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.Resource

abstract class BaseDAO<E : Entity<E>, T : Table<E>>(private val tableObject: T) {
    @Resource
    protected lateinit var database: Database

    open 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.sys

import 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.common

import 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.util

import 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.common

import 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.common

import com.example.model.sys.UserVO

object 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.aop

import 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 {
    @Autowired
    private lateinit var jwtUtil: JwtUtil

    @Autowired
    private lateinit var sysUserDAO: SysUserDAO;
    @Pointcut("@annotation(com.example.common.Need)")
    fun permissionCheck() {
    }
    @Around("permissionCheck()")
    fun around(joinPoint: ProceedingJoinPoint): Any? {
        // 获取当前请求的用户id
        val 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.controller

import 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 {

    @Autowired
    private lateinit var sysUserDAO: SysUserDAO

    @Autowired
    private 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

okfang616

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值