2486 字
12 分钟
创建一个kotlin的springboot3的项目
前言
看完你将学会使用kotlin创建一个springboot3的项目,成品项目,star
项目基础
- gradle kotlin 包管理工具
- spring web
- spring security 安全工具
- spring data jpa 数据管理
- spring validation 验证数据
- postgresql 数据库
- jjwt 生成token和校验token
- openapi 自动生成请求
项目结构
我的不一定是最好的,可以参考一下
.├── main│ ├── kotlin│ │ └── top│ │ └── inept│ │ └── blog│ │ ├── IneptBlogServerApplication.kt 启动类│ │ ├── base│ │ │ ├── ApiResponse.kt Api包装类│ │ │ ├── BaseQueryDTO.kt 基础查询DTO│ │ │ ├── PageResponse.kt Page包装类│ │ │ ├── QueryBuilder.kt 查询构建工具│ │ │ └── ValidationError.kt validation校验错误格式│ │ ├── config│ │ │ ├── MessageConfig.kt i18n配置│ │ │ ├── OpenApiI18nConfig.kt i18n配置│ │ │ └── SecurityConfig.kt 管理spring security配置│ │ ├── constant│ │ │ └── JwtClaimsConstant.kt tokenClaims字符串│ │ ├── exception│ │ │ ├── JwtInvalidException.kt 定义异常│ │ │ └── NotFoundException.kt 定义异常│ │ ├── extensions kotlin扩展函数│ │ │ ├── BaseQyeryDTOExtensions.kt│ │ │ ├── MessageExtensions.kt│ │ │ └── PageResponseExtensions.kt│ │ ├── feature 模块│ │ │ ├── article 文章│ │ │ ├── categories 分类│ │ │ ├── comment 评论│ │ │ ├── tag 标签│ │ │ └── user 用户│ │ │ ├── controller│ │ │ │ ├── AdminUserController.kt 管理员控制器│ │ │ │ ├── OpenUserController.kt 公开控制器│ │ │ │ └── UserUserController.kt 用户控制器│ │ │ ├── pojo│ │ │ │ ├── convert│ │ │ │ │ └── UserConvert.kt 转换DTO VO entity│ │ │ │ ├── dto│ │ │ │ │ ├── CreateUserDTO.kt│ │ │ │ │ ├── LoginUserDTO.kt│ │ │ │ │ ├── QueryUserDTO.kt│ │ │ │ │ ├── UpdateUserDTO.kt│ │ │ │ │ └── UpdateUserProfileDTO.kt│ │ │ │ ├── entity│ │ │ │ │ ├── User.kt spring data jpa 的 entity│ │ │ │ │ └── enums│ │ │ │ │ └── UserRole.kt│ │ │ │ ├── validated 校验相关│ │ │ │ │ ├── ValidateUserNickname.kt│ │ │ │ │ ├── ValidateUserUsername.kt│ │ │ │ │ ├── ValidatedUserEmail.kt│ │ │ │ │ └── ValidatedUserPassword.kt│ │ │ │ └── vo│ │ │ │ ├── LoginUserVO.kt│ │ │ │ ├── UserPublicVO.kt│ │ │ │ └── UserVO.kt│ │ │ ├── repository 仓库│ │ │ │ ├── UserRepository.kt│ │ │ │ └── UserSpecs.kt 查询相关(Specification)│ │ │ └── service 服务│ │ │ ├── UserService.kt│ │ │ └── impl 服务实现│ │ │ └── UserServiceImpl.kt│ │ ├── filter│ │ │ └── JwtAuthFilter.kt 判断请求头的token有效,在SecurityConfig注册│ │ ├── handler│ │ │ └── GlobalExceptionHandler.kt 全局异常拦截器│ │ ├── properties│ │ │ └── JwtProperties.kt token配置│ │ └── utils│ │ ├── DateUtil.kt│ │ ├── JwtUtil.kt│ │ ├── PasswordUtil.kt│ │ └── SecurityUtil.kt│ └── resources│ ├── application-dev.yml 开发配置│ ├── application-prod.yml 发布配置│ ├── application.yml 配置│ ├── data.sql│ ├── i18n│ │ ├── messages.properties│ │ ├── messages_en.properties│ │ ├── openapi.properties│ │ ├── openapi_en.properties│ │ ├── validation.properties│ │ └── validation_en.properties│ ├── schema.sql│ ├── static│ └── templates└── test └── kotlin └── top └── inept └── blog └── IneptBlogServerApplicationTests.kt使用idea开始创建

NOTE添加必要的依赖

1.管理好配置
和我一样在resources目录下创建yml格式的配置吧
创建开发配置
top: inept: datasource: ddl-auto: update host: 127.0.0.1 port: 5432 database: inept_blog username: user password: 123456 server: port: 8080 jwt: secretKey: 82MjWAO+36AA+XMmx6dCuLZd8Kaa2sL+KxzDVY5Mvm+xXo5ZcUQe5uZpnv+lhhdgTMjgJpfevyxok0NlXCwb8A== ttlHours: 168 tokenName: token
logging: level: org: springframework: web: servlet: DispatcherServlet: DEBUG validation: DEBUG hibernate: validator: DEBUG创建发布配置
top: inept: datasource: ddl-auto: validate host: 127.0.0.1 port: 5432 database: inept_blog username: user password: 123456 server: port: 8080 jwt: secretKey: U7lIBboDJW24Ak1ROncsOyuVNuugnZriJWZBY9ULB//4EdZU5AeGCRlVAdXMKxinBynktrcuQgGVCM4Fm54tBg== #openssl rand -base64 64 ttlHours: 168 #7day tokenName: token创建主配置
spring: application: name: inept-test profiles: active: dev datasource: url: jdbc:postgresql://${top.inept.datasource.host}:${top.inept.datasource.port}/${top.inept.datasource.database} username: ${top.inept.datasource.username} password: ${top.inept.datasource.password}
jpa: hibernate: ddl-auto: ${top.inept.datasource.ddl-auto} show-sql: true properties: hibernate: format_sql: true database-platform: org.hibernate.dialect.PostgreSQLDialect database: postgresql
messages: basename: i18n/messages,i18n/validation,i18n/openapi encoding: UTF-8 fallback-to-system-locale: false
server: port: ${top.inept.server.port}
top: inept: jwt: secretKey: ${top.inept.jwt.secretKey} ttlHours: ${top.inept.jwt.ttlHours} tokenName: ${top.inept.jwt.tokenName}详细讲解配置
我们只需要修改开发配置和发布配置
CAUTION不过再次之前我们需要把
application-dev.yml添加到.gitignore中,只需要在行末添加
top: inept: datasource: ddl-auto: update host: 127.0.0.1 #数据库ip或者域名 port: 5432 #数据库端口 database: inept_blog #数据库名称 username: user #数据库账号 password: 123456 #数据库密码 server: port: 8080 #springboot web 开放端口 jwt: secretKey: 82MjWAO+36AA+XMmx6dCuLZd8Kaa2sL+KxzDVY5Mvm+xXo5ZcUQe5uZpnv+lhhdgTMjgJpfevyxok0NlXCwb8A== #jwt的secretKey使用openssl rand -base64 64随机生成 ttlHours: 168 #过期时间 tokenName: token
logging: level: org: springframework: web: servlet: DispatcherServlet: DEBUG validation: DEBUG hibernate: validator: DEBUG2.添加i18n相关
在项目结构中可以看到在resources下有个i18n文件夹里面就是关于i18n内容,按照下面创建多个i18n文件
├── messages.properties├── messages_en.properties├── openapi.properties├── openapi_en.properties├── validation.properties└── validation_en.properties默认配置是中文的,大概内容为
openapi.query.page=页数openapi.query.size=大小
openapi.response.code=失败成功数字....我来说明messages openapi validation这些文件有什么用
messages
通常在服务层使用较为通用,例如message.user.username_or_password_error=用户名或者密码错误
openapi
专为为openapi使用,通常在DTO VO类使用
data class UserVO( @Schema(description = "openapi.user.id") val id: Long,
@Schema(description = "openapi.user.nickname") //openapi.user.nickname=昵称 val nickname: String,
@Schema(description = "openapi.user.username") //openapi.user.username=用户名 val username: String,
@Schema(description = "openapi.user.email") val email: String?,)validation
和名字一样在validation使用,例如校验用户提示的错误
@Size(min = 2, max = 16, message = "valid.user.nickname")annotation class ValidateUserNickname( val message: String = "valid.common.unknown_error", val groups: Array<KClass<*>> = [], val payload: Array<KClass<out Payload>> = [])valid.user.nickname=错误的昵称格式,长度应为2-16valid.user.username=错误的用户名格式,长度应为6-16valid.user.password=至少一个字母,至少一个数字,长度应为6-20valid.user.email=邮箱校验错误....3.创建最基础的
你可以直接在我的成品项目中复制代码,在开头有提到
1.创建JwtProperties
创建这个发布我们在业务代码方便获取到在配置文件写的内容,JwtProperties获取的是关于Jwt的
@ConfigurationProperties(prefix = "top.inept.jwt")data class JwtProperties @ConstructorBinding constructor( val secretKey: String, val ttlHours: Long, val tokenName: String,)2.创建配置类
1.MessageConfig
用于解析i18n内容的
@Configurationclass MessageConfig : WebMvcConfigurer { @Bean fun messageAccessor(messageSource: MessageSource): MessageSourceAccessor { // 这个 accessor 会自动从 LocaleContextHolder 取 locale return MessageSourceAccessor(messageSource) }}创建配套的拓展函数
// 单参数,无 argsoperator fun MessageSourceAccessor.get(code: String): String = try { this.getMessage(code) } catch (ex: NoSuchMessageException) { code }
// 多参数,可传 argsoperator fun MessageSourceAccessor.get(code: String, vararg args: Any): String = try { this.getMessage(code, args) } catch (ex: NoSuchMessageException) { code }2.OpenApiI18nConfig
由于openapi不会自动解析我们的i18n内容我需要自己手动解析
@Configurationclass OpenApiI18nConfig(private val messages: MessageSourceAccessor) { @Bean fun i18nCustomizer(): OpenApiCustomizer { return OpenApiCustomizer { openApi: OpenAPI -> openApi.components?.schemas?.forEach { (_, schema) -> translateSchema(schema, messages) } } }
private fun translateSchema(schema: Schema<*>, source: MessageSourceAccessor) { schema.description?.takeIf { it.startsWith("openapi.") }?.also { schema.description = source[it] } schema.properties?.values?.forEach { prop -> prop?.let { p -> p.description?.takeIf { it.startsWith("openapi.") }?.also { p.description = source[it] } } } }}3.一些基础的东西
ApiResponse
一个Api包装类,并且之类已经用到openapi的i18n注解了
data class ApiResponse<T>( @Schema(description = "openapi.response.code") val code: Int = 0,
@Schema(description = "openapi.response.msg") //openapi.response.msg=消息 val msg: String? = null,
@Schema(description = "openapi.response.data") val data: T? = null) : Serializable {
companion object { const val SUCCESS = 0 const val FAILURE = 1
/** * 只返回成功,不带数据 */ fun <T> success(data: T): ApiResponse<T> = ApiResponse(code = SUCCESS, data = data, msg = "success")
/** * 只返回错误信息 */ fun <T> error(msg: String): ApiResponse<T> = ApiResponse(code = FAILURE, msg = msg)
fun <T> error(msg: String, data: T): ApiResponse<T> = ApiResponse(code = FAILURE, msg = msg, data = data) }}ValidationError
自定义validation返回的错误消息格式,在下面的全局拦截器有提到
data class ValidationError(val field: String, val message: String)4.创建全局的异常拦截器
@RestControllerAdviceclass GlobalExceptionHandler( private val messages: MessageSourceAccessor) { private val logger = LoggerFactory.getLogger(this::class.java)
@ExceptionHandler fun exceptionHandler(ex: Exception): ApiResponse<String> { logger.error(ex.message, ex)
return ApiResponse.error(ex.message ?: messages["message.common.unknown_error"]) //message.common.unknown_error=未知错误 }
@ExceptionHandler fun exceptionHandler(ex: HttpMessageNotReadableException): ApiResponse<String> { logger.error(ex.message, ex)
//json解析时缺少字段报错 if (ex.cause is MissingKotlinParameterException) { return ApiResponse.error(messages["message.common.missing_json_field", ex.message ?: "Null"]) }
return ApiResponse.error(ex.message ?: messages["message.common.unknown_error"]) }
/** * Validated的验证错误 */ @ExceptionHandler(MethodArgumentNotValidException::class) fun exceptionHandler(ex: MethodArgumentNotValidException): ApiResponse<List<ValidationError>> { val errors = ex.bindingResult.fieldErrors.map { fe -> ValidationError( field = fe.field, message = messages[fe.defaultMessage ?: "message.common.illegal_parameters"], ) }
return ApiResponse.error(messages["message.common.illegal_parameters"], errors) }}4.在启动类中添加需要的注释
下面这个是默认的启动类,可能你创建项目名字不同和我的不太一样
@SpringBootApplication@EnableJpaAuditing //JPA 中启用审计@EnableConfigurationProperties(JwtProperties::class) //给刚刚创建的JwtProperties添加上class IneptBlogServerApplication
fun main(args: Array<String>) { runApplication<IneptBlogServerApplication>(*args)}5.配置认证过滤器
1.创建JwtUtil
由于我这里的JwtUtil没有和UserRole解耦,你们最好看一下成品项目
package top.inept.blog.utils
import io.jsonwebtoken.*import org.springframework.context.support.MessageSourceAccessorimport org.springframework.stereotype.Componentimport top.inept.blog.constant.JwtClaimsConstantimport top.inept.blog.exception.JwtInvalidExceptionimport top.inept.blog.extensions.getimport top.inept.blog.feature.user.pojo.entity.enums.UserRoleimport java.util.*import javax.crypto.spec.SecretKeySpec
@Componentclass JwtUtil(private val messages: MessageSourceAccessor) { private fun createJWT( secretKey: String, ttlHours: Long, claims: Map<String, Any> ): String { val now = Date(System.currentTimeMillis()) val hmacKey = SecretKeySpec(Base64.getDecoder().decode(secretKey), "HmacSHA512")
return Jwts.builder() .claims(claims) .issuedAt(now) .expiration(DateUtil.addHoursToDate(now, ttlHours)) .signWith(hmacKey) .compact() }
fun createJWT( secretKey: String, ttlHours: Long, id: Long, username: String, role: UserRole, ): String { val payload = HashMap<String, Any>() payload.put(JwtClaimsConstant.ID, id) payload.put(JwtClaimsConstant.USERNAME, username) payload.put(JwtClaimsConstant.ROLE, role.toString())
return createJWT(secretKey, ttlHours, payload) }
fun parseJWT( secretKey: String, token: String ): Jws<Claims> { val hmacKey = SecretKeySpec(Base64.getDecoder().decode(secretKey), "HmacSHA512")
return try { Jwts.parser() .verifyWith(hmacKey) .build() .parseSignedClaims(token) } catch (e: ExpiredJwtException) { throw JwtInvalidException(messages["message.jwt.expired"]) } catch (e: JwtException) { throw JwtInvalidException(messages["message.jwt.invalid"]) } }
fun getIdFromClaims(claims: Claims): Long? { val raw = claims[JwtClaimsConstant.ID]
return when (raw) { is Number -> raw.toLong() is String -> raw.toLongOrNull() else -> null } }
fun getUsernameFromClaims(claims: Claims): String? { val raw = claims[JwtClaimsConstant.USERNAME] return raw as? String }
fun getRoleFromClaims(claims: Claims): UserRole? { val raw = claims[JwtClaimsConstant.ROLE] if (raw !is String) return null
return try { UserRole.valueOf(raw) } catch (e: IllegalArgumentException) { null } }}class JwtInvalidException(message: String) : RuntimeException(message)object DateUtil { fun addHoursToDate(date: Date, hours: Long): Date { val instant = date.toInstant() val newInstant = instant.plus(Duration.ofHours(hours)) return Date.from(newInstant) }}2.创建JwtAuthFilter
@Component class JwtAuthFilter( private val jwtProperties: JwtProperties, private val jwtUtil: JwtUtil,) : OncePerRequestFilter() {
override fun doFilterInternal( request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain ) { //判断有没有token val authHeader = request.getHeader("Authorization") if (authHeader == null || !authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response) return }
//获取token val token = authHeader.substring(7)
//解析token val claims = jwtUtil.parseJWT(secretKey = jwtProperties.secretKey, token)
//获取用户名与role val username = jwtUtil.getUsernameFromClaims(claims.payload) val role = jwtUtil.getRoleFromClaims(claims.payload)
if (username == null || role == null) { filterChain.doFilter(request, response) return }
// 如果上下文中没有认证信息,才进行设置 if (SecurityContextHolder.getContext().authentication == null) {
val authToken = UsernamePasswordAuthenticationToken( username, null, listOf<GrantedAuthority>(role) ) authToken.details = WebAuthenticationDetailsSource().buildDetails(request)
// 将认证对象设置到 SecurityContext 中,表示当前用户已认证 SecurityContextHolder.getContext().authentication = authToken }
filterChain.doFilter(request, response) }}3.创建SecurityConfig配置类
@Configuration@EnableWebSecurityclass SecurityConfig( private val jwtAuthFilter: JwtAuthFilter,) { @Bean fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http.invoke { // 禁用 CSRF,因为我们使用无状态的 JWT csrf { disable() }
authorizeHttpRequests { // 定义访问权限 authorize("/open/**", permitAll) authorize("/user/**", hasAuthority(UserRole.USER.authority)) // /user/** 路径需要 USER 角色 authorize("/admin/**", hasAuthority(UserRole.ADMIN.authority)) // /admin/** 路径需要 ADMIN 角色
//TODO 发布时关闭 authorize("/swagger-ui/**", permitAll) authorize("/v3/api-docs/**", permitAll)
authorize(anyRequest, authenticated) // 其他所有请求都需要认证 }
// 设置 Session 管理策略为 STATELESS (无状态) sessionManagement { sessionCreationPolicy = SessionCreationPolicy.STATELESS }
addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter::class.java) } return http.build() }} 创建一个kotlin的springboot3的项目
https://blog.inept.top/posts/kotlin-springboot3/start-kotlin-springboot3-01/