本文主要是介绍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/
- 没有配置文件、没有 xml、没有注解、甚至没有任何第三方依赖、轻量级、简洁易用
- 强类型 SQL DSL,将低级 bug 暴露在编译期
- 灵活的查询,随心所欲地精确控制所生成的 SQL
- 实体序列 API,使用 filter、map、sortedBy 等序列函数进行查询,就像使用 Kotlin 中的原生集合一样方便
- 易扩展的设计,可以灵活编写扩展,支持更多运算符、数据类型、 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. 设计模型
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
这个类存在的意义是,把一些公有的属性放一块,比如 createAt
,updateAt
// 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后台模板 实践的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!