本文代码仓库: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: 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. 设计模型
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
这个类存在的意义是,把一些公有的属性放一块,比如 createAt
,updateAt
// 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