Add authentication #1

Merged
FatttSnake merged 24 commits from FatttSnake into dev 2023-10-09 10:38:31 +08:00
49 changed files with 1477 additions and 30 deletions

4
.gitignore vendored
View File

@@ -31,3 +31,7 @@ build/
### VS Code ###
.vscode/
### Custom ###
/application-config.yml
data

13
Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
FROM eclipse-temurin:17-jdk-alpine
LABEL authors="FatttSnake"
VOLUME /data
ARG EXTRACTED=target/extracted
COPY ${EXTRACTED}/dependencies/ /
COPY ${EXTRACTED}/spring-boot-loader/ /
COPY ${EXTRACTED}/snapshot-dependencies/ /
RUN true
COPY ${EXTRACTED}/application/ /
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher", "${JAVA_OPTS}"]

9
build-docker.sh Normal file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
JAR_NAME=`ls target | grep api-|grep -v original`
JAR_VERSION=${JAR_NAME%.*}
JAR_VERSION=${JAR_VERSION#*-}
mkdir target/extracted
java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted
docker build -t hub.fatweb.top/fatweb-api:latest -t hub.fatweb.top/fatweb-api:$JAR_VERSION -t hub.fatweb.top/fatweb-api:$JAR_VERSION-$(date "+%Y%m%d%H%M%S") .

12
db/schema.sql Normal file
View File

@@ -0,0 +1,12 @@
drop table if exists t_user;
create table if not exists t_user
(
id bigint not null primary key,
username varchar(20) not null comment '用户名',
password char(70) not null comment '密码',
enable int not null comment '启用',
deleted bigint not null default 0,
version int not null default 0,
constraint t_user_unique unique (username, deleted)
) comment '用户';

132
pom.xml
View File

@@ -13,9 +13,57 @@
<version>0.0.1-SNAPSHOT</version>
<name>fatweb-api</name>
<description>fatweb-api</description>
<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
<property>
<name>env</name>
<value>dev</value>
</property>
</activation>
<dependencies>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>
</dependencies>
</profile>
<profile>
<id>prod</id>
<activation>
<property>
<name>env</name>
<value>prod</value>
</property>
</activation>
<dependencies>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-ui</artifactId>
</exclusion>
<exclusion>
<groupId>org.webjars</groupId>
<artifactId>swagger-ui</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</profile>
</profiles>
<properties>
<java.version>17</java.version>
<kotlin.version>1.8.22</kotlin.version>
<build.timestamp>${maven.build.timestamp}</build.timestamp>
<maven.build.timestamp.format>yyyy-MM-dd'T'HH:mm:ss</maven.build.timestamp.format>
</properties>
<dependencies>
<dependency>
@@ -26,6 +74,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
@@ -46,9 +98,9 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -60,31 +112,80 @@
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter-test</artifactId>
<version>3.5.3.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.19</version>
</dependency>
<!--
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3.2</version>
</dependency>
-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<sourceDirs>
<source>src/main/kotlin</source>
<source>target/generated-sources/annotations</source>
</sourceDirs>
</configuration>
</execution>
</executions>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
@@ -103,5 +204,4 @@
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,11 +1,36 @@
package top.fatweb.api
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.transaction.annotation.EnableTransactionManagement
import java.io.File
import java.util.*
@SpringBootApplication
@EnableTransactionManagement
class FatWebApiApplication
fun main(args: Array<String>) {
val logger = LoggerFactory.getLogger("main")
if (!File("data").isDirectory) {
if (!File("data").mkdir()) {
logger.error("Can not create directory 'data', please try again later.")
return
}
}
if (File("application-config.yml").exists()) {
runApplication<FatWebApiApplication>(*args)
} else {
logger.warn("File application.yml cannot be found in the running path. The configuration file template 'application.example.yml' has been created in directory 'data'. Please change the configuration file content, rename it to 'application.yml' and put it in the running path, and then restart the server.")
FatWebApiApplication::class.java.getResource("/application-config-template.yml")?.readText()?.let {
File("data/application-config.example.yml").writeText(
it.replace(
"\$uuid\$", UUID.randomUUID().toString().replace("-", "")
)
)
}
}
}

View File

@@ -0,0 +1,11 @@
package top.fatweb.api.annotation
import org.springframework.core.annotation.AliasFor
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class ApiVersion(
@get:AliasFor("version") val value: Int = 1,
@get:AliasFor("value") val version: Int = 1
)

View File

@@ -0,0 +1,18 @@
package top.fatweb.api.config
import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import top.fatweb.api.filter.ExceptionFilter
@Configuration
class FilterConfig {
@Bean
fun exceptionFilterRegistrationBean(exceptionFilter: ExceptionFilter): FilterRegistrationBean<ExceptionFilter> {
val registrationBean = FilterRegistrationBean(exceptionFilter)
registrationBean.setBeanName("exceptionFilter")
registrationBean.order = -100
return registrationBean
}
}

View File

@@ -0,0 +1,20 @@
package top.fatweb.api.config
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class MybatisPlusConfig {
@Bean
fun mybatisPlusInterceptor(): MybatisPlusInterceptor {
val mybatisPlusInterceptor = MybatisPlusInterceptor()
mybatisPlusInterceptor.addInnerInterceptor(OptimisticLockerInnerInterceptor())
mybatisPlusInterceptor.addInnerInterceptor(PaginationInnerInterceptor())
return mybatisPlusInterceptor
}
}

View File

@@ -0,0 +1,42 @@
package top.fatweb.api.config
import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.annotation.PropertyAccessor
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer
import org.springframework.data.redis.serializer.StringRedisSerializer
@Configuration
class RedisConfig {
@Bean
fun redisTemplate(redisConnectionFactory: RedisConnectionFactory): RedisTemplate<*, *> {
val redisTemplate = RedisTemplate<String, Any>()
redisTemplate.connectionFactory = redisConnectionFactory
val stringRedisSerializer = StringRedisSerializer()
val objectMapper = ObjectMapper().registerModules(JavaTimeModule()).apply {
setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
activateDefaultTyping(
this.polymorphicTypeValidator, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY
)
}
val anyJackson2JsonRedisSerializer = Jackson2JsonRedisSerializer(objectMapper, Any::class.java)
// 使用StringRedisSerializer来序列化和反序列化redis的key值
redisTemplate.keySerializer = stringRedisSerializer
redisTemplate.valueSerializer = anyJackson2JsonRedisSerializer
// Hash的key也采用StringRedisSerializer的序列化方式
redisTemplate.hashKeySerializer = stringRedisSerializer
redisTemplate.hashValueSerializer = anyJackson2JsonRedisSerializer
redisTemplate.afterPropertiesSet()
return redisTemplate
}
}

View File

@@ -0,0 +1,92 @@
package top.fatweb.api.config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.*
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
import top.fatweb.api.filter.JwtAuthenticationTokenFilter
import top.fatweb.api.handler.JwtAccessDeniedHandler
import top.fatweb.api.handler.JwtAuthenticationEntryPointHandler
@Configuration
@EnableMethodSecurity
class SecurityConfig(
val jwtAuthenticationTokenFilter: JwtAuthenticationTokenFilter,
val authenticationEntryPointHandler: JwtAuthenticationEntryPointHandler,
val accessDeniedHandler: JwtAccessDeniedHandler
) {
@Bean
fun passwordEncoder() = BCryptPasswordEncoder()
@Bean
fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration): AuthenticationManager =
authenticationConfiguration.authenticationManager
@Bean
fun corsConfigurationSource(): UrlBasedCorsConfigurationSource {
val corsConfiguration = CorsConfiguration()
corsConfiguration.allowedMethods = listOf("*")
corsConfiguration.allowedHeaders = listOf("*")
corsConfiguration.maxAge = 3600L
corsConfiguration.allowedOrigins = listOf("*")
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", corsConfiguration)
return source
}
@Bean
fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain = httpSecurity
// Disable CSRF
.csrf { csrfConfigurer: CsrfConfigurer<HttpSecurity> -> csrfConfigurer.disable() }
// Do not get SecurityContent by Session
.sessionManagement { sessionManagementConfigurer: SessionManagementConfigurer<HttpSecurity?> ->
sessionManagementConfigurer.sessionCreationPolicy(
SessionCreationPolicy.STATELESS
)
}
.authorizeHttpRequests { authorizeHttpRequests: AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry ->
authorizeHttpRequests
// Allow anonymous access
.requestMatchers(
"/api/v*/login",
"/error/thrown",
"/doc.html",
"/swagger-ui/**",
"/webjars/**",
"/v3/**",
"/swagger-ui.html",
"/favicon.ico"
).anonymous()
// Authentication required
.anyRequest().authenticated()
}
.logout { logoutConfigurer: LogoutConfigurer<HttpSecurity> -> logoutConfigurer.disable() }
.exceptionHandling { exceptionHandlingConfigurer: ExceptionHandlingConfigurer<HttpSecurity?> ->
exceptionHandlingConfigurer.authenticationEntryPoint(
authenticationEntryPointHandler
)
exceptionHandlingConfigurer.accessDeniedHandler(
accessDeniedHandler
)
}
.cors { cors: CorsConfigurer<HttpSecurity?> ->
cors.configurationSource(
corsConfigurationSource()
)
}
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter::class.java).build()
}

View File

@@ -0,0 +1,23 @@
package top.fatweb.api.config
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Contact
import io.swagger.v3.oas.models.info.Info
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import top.fatweb.api.constant.ServerConstants
@Configuration
class SwaggerConfig {
@Bean
fun customOpenAPI(): OpenAPI? {
val contact = Contact().name("FatttSnake").url("https://fatweb.top").email("fatttsnake@fatweb.top")
return OpenAPI().info(
Info().title("FatWeb API 文档").description("FatWeb 后端 API 文档,包含各个 Controller 调用信息")
.contact(contact).version(
ServerConstants.version
)
)
}
}

View File

@@ -0,0 +1,11 @@
package top.fatweb.api.config
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
import top.fatweb.api.util.ApiResponseMappingHandlerMapping
@Configuration
class WebMvcRegistrationsConfig : WebMvcRegistrations {
override fun getRequestMappingHandlerMapping(): RequestMappingHandlerMapping = ApiResponseMappingHandlerMapping()
}

View File

@@ -0,0 +1,25 @@
package top.fatweb.api.constant
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component
import java.util.concurrent.TimeUnit
@Component
@ConfigurationProperties("app.security")
object SecurityConstants {
var headerString = "Authorization"
var tokenPrefix = "Bearer "
var jwtTtl = 2L
var jwtTtlUnit = TimeUnit.HOURS
var jwtKey = "FatWeb"
var jwtIssuer = "FatWeb"
var redisTtl = 20L
var redisTtlUnit = TimeUnit.MINUTES
}

View File

@@ -0,0 +1,18 @@
package top.fatweb.api.constant
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
@Component
@ConfigurationProperties("app")
object ServerConstants {
lateinit var version: String
lateinit var buildTime: String
fun buildZoneDateTime(zoneId: ZoneId = ZoneId.systemDefault()): ZonedDateTime =
LocalDateTime.parse(buildTime).atZone(ZoneId.of("UTC")).withZoneSameInstant(zoneId)
}

View File

@@ -0,0 +1,16 @@
package top.fatweb.api.controller
import io.swagger.v3.oas.annotations.Hidden
import jakarta.servlet.http.HttpServletRequest
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@Hidden
@RestController
@RequestMapping("/error")
class ExceptionController {
@RequestMapping("/thrown")
fun thrown(request: HttpServletRequest) {
throw request.getAttribute("filter.error") as RuntimeException
}
}

View File

@@ -0,0 +1,17 @@
package top.fatweb.api.controller
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
/**
* <p>
* 用户 前端控制器
* </p>
*
* @author FatttSnake
* @since 2023-10-04
*/
@RestController
@RequestMapping("/api/user")
class UserController

View File

@@ -0,0 +1,47 @@
package top.fatweb.api.controller.permission
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.servlet.http.HttpServletRequest
import jakarta.validation.Valid
import org.springframework.web.bind.annotation.*
import top.fatweb.api.annotation.ApiVersion
import top.fatweb.api.converter.UserConverter
import top.fatweb.api.entity.common.ResponseCode
import top.fatweb.api.entity.common.ResponseResult
import top.fatweb.api.param.LoginParam
import top.fatweb.api.service.permission.IAuthenticationService
import top.fatweb.api.util.WebUtil
@Tag(name = "身份认证", description = "身份认证相关接口")
@Suppress("MVCPathVariableInspection")
@RequestMapping("/api/{apiVersion}")
@ApiVersion(2)
@RestController
class AuthenticationController(val authenticationService: IAuthenticationService, val userConverter: UserConverter) {
@Operation(summary = "登录")
@PostMapping("/login")
fun login(@Valid @RequestBody loginParam: LoginParam) =
ResponseResult.success(
ResponseCode.SYSTEM_LOGIN_SUCCESS,
"Login success",
authenticationService.login(userConverter.loginParamToUser(loginParam))
)
@Operation(summary = "登出")
@PostMapping("/logout")
fun logout(request: HttpServletRequest) =
when (authenticationService.logout(WebUtil.getToken(request))) {
true -> ResponseResult.success(ResponseCode.SYSTEM_LOGOUT_SUCCESS, "Logout success", null)
false -> ResponseResult.fail(ResponseCode.SYSTEM_LOGOUT_FAILED, "Logout failed", null)
}
@Operation(summary = "更新 Token")
@GetMapping("/token")
fun renewToken(request: HttpServletRequest) =
ResponseResult.success(
ResponseCode.SYSTEM_TOKEN_RENEW_SUCCESS,
"Token renew success",
authenticationService.renewToken(WebUtil.getToken(request))
)
}

View File

@@ -0,0 +1,17 @@
package top.fatweb.api.converter
import org.springframework.stereotype.Component
import top.fatweb.api.entity.permission.User
import top.fatweb.api.param.LoginParam
@Component
object UserConverter {
fun loginParamToUser(loginParam: LoginParam): User {
val user = User().apply {
username = loginParam.username
password = loginParam.password
}
return user
}
}

View File

@@ -0,0 +1,6 @@
package top.fatweb.api.entity.common
enum class BusinessCode(val code: Int) {
SYSTEM(100),
DATABASE(200)
}

View File

@@ -0,0 +1,24 @@
package top.fatweb.api.entity.common
enum class ResponseCode(val code: Int) {
SYSTEM_OK(BusinessCode.SYSTEM, 0),
SYSTEM_LOGIN_SUCCESS(BusinessCode.SYSTEM, 20),
SYSTEM_PASSWORD_CHANGE_SUCCESS(BusinessCode.SYSTEM, 21),
SYSTEM_LOGOUT_SUCCESS(BusinessCode.SYSTEM, 22),
SYSTEM_TOKEN_RENEW_SUCCESS(BusinessCode.SYSTEM, 23),
SYSTEM_UNAUTHORIZED(BusinessCode.SYSTEM, 30),
SYSTEM_USERNAME_NOT_FOUND(BusinessCode.SYSTEM, 31),
SYSTEM_ACCESS_DENIED(BusinessCode.SYSTEM, 32),
SYSTEM_USER_DISABLE(BusinessCode.SYSTEM, 33),
SYSTEM_LOGIN_USERNAME_PASSWORD_ERROR(BusinessCode.SYSTEM, 34),
SYSTEM_OLD_PASSWORD_NOT_MATCH(BusinessCode.SYSTEM, 35),
SYSTEM_LOGOUT_FAILED(BusinessCode.SYSTEM, 36),
SYSTEM_TOKEN_ILLEGAL(BusinessCode.SYSTEM, 37),
SYSTEM_TOKEN_HAS_EXPIRED(BusinessCode.SYSTEM, 38),
SYSTEM_REQUEST_ILLEGAL(BusinessCode.SYSTEM, 40),
SYSTEM_ARGUMENT_NOT_VALID(BusinessCode.SYSTEM, 41),
SYSTEM_ERROR(BusinessCode.SYSTEM, 50),
SYSTEM_TIMEOUT(BusinessCode.SYSTEM, 51);
constructor(businessCode: BusinessCode, code: Int) : this(businessCode.code * 100 + code)
}

View File

@@ -0,0 +1,29 @@
package top.fatweb.api.entity.common
import io.swagger.v3.oas.annotations.media.Schema
import java.io.Serializable
class ResponseResult<T> private constructor(
@Schema(description = "响应码", defaultValue = "200")
val code: Int,
@Schema(description = "是否调用成功")
val success: Boolean,
@Schema(description = "信息")
val msg: String,
@Schema(description = "数据")
val data: T?
) : Serializable {
companion object {
fun <T> build(code: ResponseCode, success: Boolean, msg: String, data: T?) =
ResponseResult(code.code, success, msg, data)
fun <T> success(code: ResponseCode = ResponseCode.SYSTEM_OK, msg: String = "success", data: T? = null) =
build(code, true, msg, data)
fun <T> fail(code: ResponseCode = ResponseCode.SYSTEM_ERROR, msg: String = "fail", data: T? = null) =
build(code, false, msg, data)
}
}

View File

@@ -0,0 +1,44 @@
package top.fatweb.api.entity.permission
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonTypeInfo
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
class LoginUser() : UserDetails {
lateinit var user: User
@JsonIgnore
private var authorities: List<GrantedAuthority>? = null
constructor(user: User) : this() {
this.user = user
}
@JsonIgnore
override fun getAuthorities(): List<GrantedAuthority> {
authorities?.let { return it }
authorities = emptyList()
return authorities as List<GrantedAuthority>
}
@JsonIgnore
override fun getPassword(): String? = user.password
@JsonIgnore
override fun getUsername(): String? = user.username
@JsonIgnore
override fun isAccountNonExpired(): Boolean = true
@JsonIgnore
override fun isAccountNonLocked(): Boolean = true
@JsonIgnore
override fun isCredentialsNonExpired(): Boolean = true
@JsonIgnore
override fun isEnabled(): Boolean = user.enable == 1
}

View File

@@ -0,0 +1,54 @@
package top.fatweb.api.entity.permission
import com.baomidou.mybatisplus.annotation.*
import java.io.Serializable
/**
* <p>
* 用户
* </p>
*
* @author FatttSnake
* @since 2023-10-04
*/
@TableName("t_user")
class User() : Serializable {
constructor(username: String, password: String, enable: Boolean = true) : this() {
this.username = username
this.password = password
this.enable = if (enable) 1 else 0
}
@TableId("id")
var id: Long? = null
/**
* 用户名
*/
@TableField("username")
var username: String? = null
/**
* 密码
*/
@TableField("password")
var password: String? = null
/**
* 启用
*/
@TableField("enable")
var enable: Int? = null
@TableField("deleted")
@TableLogic
var deleted: Long? = null
@TableField("version")
@Version
var version: Int? = null
override fun toString(): String {
return "User{id=$id, username=$username, password=$password, enable=$enable, deleted=$deleted, version=$version}"
}
}

View File

@@ -0,0 +1,3 @@
package top.fatweb.api.exception
class TokenHasExpiredException : RuntimeException("Token has expired")

View File

@@ -0,0 +1,23 @@
package top.fatweb.api.filter
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import org.springframework.stereotype.Component
@Component
class ExceptionFilter : Filter {
override fun doFilter(
servletRequest: ServletRequest?,
servletResponse: ServletResponse?,
filterChain: FilterChain?
) {
try {
filterChain!!.doFilter(servletRequest, servletResponse)
} catch (e: Exception) {
servletRequest?.setAttribute("filter.error", e)
servletRequest?.getRequestDispatcher("/error/thrown")?.forward(servletRequest, servletResponse)
}
}
}

View File

@@ -0,0 +1,46 @@
package top.fatweb.api.filter
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
import org.springframework.util.StringUtils
import org.springframework.web.filter.OncePerRequestFilter
import top.fatweb.api.constant.SecurityConstants
import top.fatweb.api.entity.permission.LoginUser
import top.fatweb.api.exception.TokenHasExpiredException
import top.fatweb.api.util.JwtUtil
import top.fatweb.api.util.RedisUtil
import top.fatweb.api.util.WebUtil
@Component
class JwtAuthenticationTokenFilter(private val redisUtil: RedisUtil) : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val tokenWithPrefix = request.getHeader(SecurityConstants.headerString)
if (!StringUtils.hasText(tokenWithPrefix) || "/error/thrown" == request.servletPath) {
filterChain.doFilter(request, response)
return
}
val token = WebUtil.getToken(tokenWithPrefix)
JwtUtil.parseJwt(token)
val redisKey = "${SecurityConstants.jwtIssuer}_login:" + token
val loginUser = redisUtil.getObject<LoginUser>(redisKey)
loginUser ?: let { throw TokenHasExpiredException() }
redisUtil.setExpire(redisKey, SecurityConstants.redisTtl, SecurityConstants.redisTtlUnit)
val authenticationToken = UsernamePasswordAuthenticationToken(loginUser, null, loginUser.authorities)
SecurityContextHolder.getContext().authentication = authenticationToken
filterChain.doFilter(request, response)
}
}

View File

@@ -0,0 +1,67 @@
package top.fatweb.api.handler
import com.auth0.jwt.exceptions.JWTDecodeException
import com.auth0.jwt.exceptions.SignatureVerificationException
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.InsufficientAuthenticationException
import org.springframework.security.authentication.InternalAuthenticationServiceException
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
import top.fatweb.api.entity.common.ResponseCode
import top.fatweb.api.entity.common.ResponseResult
import top.fatweb.api.exception.TokenHasExpiredException
@RestControllerAdvice
class ExceptionHandler {
private val log: Logger = LoggerFactory.getLogger(this::class.java)
@ExceptionHandler(value = [Exception::class])
fun exceptionHandler(e: Exception): ResponseResult<*> {
return when (e) {
is InsufficientAuthenticationException -> {
log.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.SYSTEM_UNAUTHORIZED, e.localizedMessage, null)
}
is HttpMessageNotReadableException -> {
log.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.SYSTEM_REQUEST_ILLEGAL, e.localizedMessage.split(":")[0], null)
}
is MethodArgumentNotValidException -> {
log.debug(e.localizedMessage, e)
val errorMessage = e.allErrors.map { error -> error.defaultMessage }.joinToString(". ")
ResponseResult.fail(ResponseCode.SYSTEM_ARGUMENT_NOT_VALID, errorMessage, null)
}
is InternalAuthenticationServiceException -> {
log.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.SYSTEM_USERNAME_NOT_FOUND, e.localizedMessage, null)
}
is BadCredentialsException -> {
log.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.SYSTEM_LOGIN_USERNAME_PASSWORD_ERROR, e.localizedMessage, null)
}
is SignatureVerificationException, is JWTDecodeException -> {
log.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.SYSTEM_TOKEN_ILLEGAL, "Token illegal", null)
}
is TokenHasExpiredException -> {
log.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.SYSTEM_TOKEN_HAS_EXPIRED, e.localizedMessage, null)
}
else -> {
log.error(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.SYSTEM_ERROR, data = null)
}
}
}
}

View File

@@ -0,0 +1,19 @@
package top.fatweb.api.handler
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.web.access.AccessDeniedHandler
import org.springframework.stereotype.Component
@Component
class JwtAccessDeniedHandler : AccessDeniedHandler {
override fun handle(
request: HttpServletRequest?,
response: HttpServletResponse?,
accessDeniedException: AccessDeniedException?
) {
request?.setAttribute("filter.error", accessDeniedException)
request?.getRequestDispatcher("/error/thrown")?.forward(request, response)
}
}

View File

@@ -0,0 +1,19 @@
package top.fatweb.api.handler
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.AuthenticationEntryPoint
import org.springframework.stereotype.Component
@Component
class JwtAuthenticationEntryPointHandler : AuthenticationEntryPoint {
override fun commence(
request: HttpServletRequest?,
response: HttpServletResponse?,
authException: AuthenticationException?
) {
request?.setAttribute("filter.error", authException)
request?.getRequestDispatcher("/error/thrown")?.forward(request, response)
}
}

View File

@@ -0,0 +1,16 @@
package top.fatweb.api.mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper
import org.apache.ibatis.annotations.Mapper
import top.fatweb.api.entity.permission.User
/**
* <p>
* 用户 Mapper 接口
* </p>
*
* @author FatttSnake
* @since 2023-10-04
*/
@Mapper
interface UserMapper : BaseMapper<User>

View File

@@ -0,0 +1,17 @@
package top.fatweb.api.param
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import java.io.Serializable
@Schema(description = "登录请求参数")
class LoginParam : Serializable {
@Schema(description = "用户名", example = "test", required = true)
@NotBlank(message = "Username can not be blank")
val username: String? = null
@Schema(description = "密码", example = "test123456", required = true)
@NotBlank(message = "Password can not be blank")
val password: String? = null
}

View File

@@ -0,0 +1,14 @@
package top.fatweb.api.service
import com.baomidou.mybatisplus.extension.service.IService
import top.fatweb.api.entity.permission.User
/**
* <p>
* 用户 服务类
* </p>
*
* @author FatttSnake
* @since 2023-10-04
*/
interface IUserService : IService<User>

View File

@@ -0,0 +1,18 @@
package top.fatweb.api.service.impl
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
import org.springframework.stereotype.Service
import top.fatweb.api.entity.permission.User
import top.fatweb.api.mapper.UserMapper
import top.fatweb.api.service.IUserService
/**
* <p>
* 用户 服务实现类
* </p>
*
* @author FatttSnake
* @since 2023-10-04
*/
@Service
class UserServiceImpl : ServiceImpl<UserMapper, User>(), IUserService

View File

@@ -0,0 +1,13 @@
package top.fatweb.api.service.permission
import top.fatweb.api.entity.permission.User
import top.fatweb.api.vo.LoginVo
import top.fatweb.api.vo.TokenVo
interface IAuthenticationService {
fun login(user: User): LoginVo
fun logout(token: String): Boolean
fun renewToken(token: String): TokenVo
}

View File

@@ -0,0 +1,65 @@
package top.fatweb.api.service.permission.impl
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.stereotype.Service
import top.fatweb.api.constant.SecurityConstants
import top.fatweb.api.entity.permission.LoginUser
import top.fatweb.api.entity.permission.User
import top.fatweb.api.service.permission.IAuthenticationService
import top.fatweb.api.util.JwtUtil
import top.fatweb.api.util.RedisUtil
import top.fatweb.api.util.WebUtil
import top.fatweb.api.vo.LoginVo
import top.fatweb.api.vo.TokenVo
@Service
class AuthenticationServiceImpl(
private val authenticationManager: AuthenticationManager,
private val redisUtil: RedisUtil
) : IAuthenticationService {
override fun login(user: User): LoginVo {
val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(user.username, user.password)
val authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken)
authentication ?: let {
throw RuntimeException("Login failed")
}
val loginUser = authentication.principal as LoginUser
loginUser.user.password = ""
val userId = loginUser.user.id.toString()
val jwt = JwtUtil.createJwt(userId)
jwt ?: let {
throw RuntimeException("Login failed")
}
val redisKey = "${SecurityConstants.jwtIssuer}_login:" + jwt
redisUtil.setObject(redisKey, loginUser, SecurityConstants.redisTtl, SecurityConstants.redisTtlUnit)
return LoginVo(jwt)
}
override fun logout(token: String): Boolean =
redisUtil.delObject("${SecurityConstants.jwtIssuer}_login:" + token)
override fun renewToken(token: String): TokenVo {
val oldRedisKey = "${SecurityConstants.jwtIssuer}_login:" + token
redisUtil.delObject(oldRedisKey)
val jwt = JwtUtil.createJwt(WebUtil.getLoginUserId().toString())
jwt ?: let {
throw RuntimeException("Login failed")
}
val redisKey = "${SecurityConstants.jwtIssuer}_login:" + jwt
redisUtil.setObject(
redisKey,
WebUtil.getLoginUser(),
SecurityConstants.redisTtl,
SecurityConstants.redisTtlUnit
)
return TokenVo(jwt)
}
}

View File

@@ -0,0 +1,19 @@
package top.fatweb.api.service.permission.impl
import com.baomidou.mybatisplus.extension.kotlin.KtQueryWrapper
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.stereotype.Service
import top.fatweb.api.entity.permission.LoginUser
import top.fatweb.api.entity.permission.User
import top.fatweb.api.service.IUserService
@Service
class UserDetailsServiceImpl(val userService: IUserService) : UserDetailsService {
override fun loadUserByUsername(username: String?): UserDetails {
val user = userService.getOne(KtQueryWrapper(User()).eq(User::username, username))
user ?: let { throw Exception("Username not found") }
return LoginUser(user)
}
}

View File

@@ -0,0 +1,31 @@
package top.fatweb.api.util
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.servlet.mvc.condition.RequestCondition
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
import top.fatweb.api.annotation.ApiVersion
import java.lang.reflect.Method
class ApiResponseMappingHandlerMapping : RequestMappingHandlerMapping() {
private val versionFlag = "{apiVersion}"
private fun createCondition(clazz: Class<*>): RequestCondition<ApiVersionCondition>? {
val classRequestMapping = clazz.getAnnotation(RequestMapping::class.java)
classRequestMapping ?: let { return null }
val mappingUrlBuilder = StringBuilder()
if (classRequestMapping.value.isNotEmpty()) {
mappingUrlBuilder.append(classRequestMapping.value[0])
}
val mappingUrl = mappingUrlBuilder.toString()
if (!mappingUrl.contains(versionFlag)) {
return null
}
val apiVersion = clazz.getAnnotation(ApiVersion::class.java)
return if (apiVersion == null) ApiVersionCondition(1) else ApiVersionCondition(apiVersion.version)
}
override fun getCustomMethodCondition(method: Method): RequestCondition<*>? = createCondition(method.javaClass)
override fun getCustomTypeCondition(handlerType: Class<*>): RequestCondition<*>? = createCondition(handlerType)
}

View File

@@ -0,0 +1,26 @@
package top.fatweb.api.util
import jakarta.servlet.http.HttpServletRequest
import org.springframework.web.servlet.mvc.condition.RequestCondition
import java.util.regex.Pattern
class ApiVersionCondition(private val apiVersion: Int) : RequestCondition<ApiVersionCondition> {
private val versionPrefixPattern: Pattern = Pattern.compile(".*v(\\d+).*")
override fun combine(other: ApiVersionCondition): ApiVersionCondition = ApiVersionCondition(other.apiVersion)
override fun getMatchingCondition(request: HttpServletRequest): ApiVersionCondition? {
val matcher = versionPrefixPattern.matcher(request.requestURI)
if (matcher.find()) {
val version = matcher.group(1).toInt()
if (version >= this.apiVersion) {
return this
}
}
return null
}
override fun compareTo(other: ApiVersionCondition, request: HttpServletRequest): Int =
other.apiVersion - this.apiVersion
}

View File

@@ -0,0 +1,67 @@
package top.fatweb.api.util
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.interfaces.DecodedJWT
import top.fatweb.api.constant.SecurityConstants
import java.util.*
import java.util.concurrent.TimeUnit
import javax.crypto.spec.SecretKeySpec
object JwtUtil {
private fun getUUID() = UUID.randomUUID().toString().replace("-", "")
/**
* 生成加密后的秘钥 secretKey
*
* @return 密钥
*/
private fun generalKey(): SecretKeySpec {
val encodeKey = Base64.getDecoder().decode(SecurityConstants.jwtKey)
return SecretKeySpec(encodeKey, 0, encodeKey.size, "AES")
}
private fun algorithm(): Algorithm = Algorithm.HMAC256(generalKey().toString())
/**
* 创建 token
*
* @param subject token 中存放的数据json格式)
* @param ttl token 生存时间
* @param timeUnit ttl 时间单位
* @param uuid 唯一 ID
* @return jwt 串
*/
fun createJwt(
subject: String,
ttl: Long = SecurityConstants.jwtTtl,
timeUnit: TimeUnit = SecurityConstants.jwtTtlUnit,
uuid: String = getUUID()
): String? {
val nowMillis = System.currentTimeMillis()
val nowDate = Date(nowMillis)
val unitTtl = (ttl * when (timeUnit) {
TimeUnit.DAYS -> 24 * 60 * 60 * 1000
TimeUnit.HOURS -> 60 * 60 * 1000
TimeUnit.MINUTES -> 60 * 1000
TimeUnit.SECONDS -> 1000
TimeUnit.MILLISECONDS -> 1
TimeUnit.NANOSECONDS -> 1 / 1000
TimeUnit.MICROSECONDS -> 1 / 1000 / 1000
})
val expMillis = nowMillis + unitTtl
val expDate = Date(expMillis)
return JWT.create().withJWTId(uuid).withSubject(subject).withIssuer(SecurityConstants.jwtIssuer)
.withIssuedAt(nowDate).withExpiresAt(expDate).sign(algorithm())
}
/**
* 解析 jwt
*
* @param jwt jwt 串
* @return 解析内容
*/
fun parseJwt(jwt: String): DecodedJWT =
JWT.require(algorithm()).build().verify(jwt)
}

View File

@@ -0,0 +1,183 @@
package top.fatweb.api.util
import org.springframework.data.redis.core.BoundSetOperations
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.stereotype.Component
import java.util.concurrent.TimeUnit
@Suppress("UNCHECKED_CAST")
@Component
class RedisUtil(private val redisTemplate: RedisTemplate<String, Any>) {
/**
* 设置有效时间
*
* @param key 缓存的键
* @param timeout 超时时间
* @param timeUnit 时间颗粒度
* @return true=设置成功false=设置失败
*/
fun setExpire(key: String, timeout: Long, timeUnit: TimeUnit = TimeUnit.SECONDS) =
redisTemplate.expire(key, timeout, timeUnit)
/**
* 获取有效时间
*
* @param key 缓存的键
* @return 有效时间
*/
fun getExpire(key: String, timeUnit: TimeUnit = TimeUnit.SECONDS) = redisTemplate.getExpire(key, timeUnit)
/**
* 判断 key 是否存在
*
* @param key 缓存的键
* @return true=存在; false=不存在
*/
fun hasKey(key: String) = redisTemplate.hasKey(key)
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
fun keys(pattern: String): Set<String> = redisTemplate.keys(pattern)
/**
* 缓存基本的对象Integer、String、实体类等
*
* @param key 缓存的键
* @param value 缓存的值
*/
fun setObject(key: String, value: Any) = redisTemplate.opsForValue().set(key, value)
/**
* 缓存基本的对象Integer、String、实体类等
*
* @param key 缓存的键
* @param value 缓存的值
* @param timeout 超时时间
* @param timeUnit 时间颗粒度
*/
fun setObject(key: String, value: Any, timeout: Long, timeUnit: TimeUnit = TimeUnit.SECONDS) =
redisTemplate.opsForValue().set(key, value, timeout, timeUnit)
/**
* 获得缓存的基本对象
*
* @param key 缓存的键
* @return 缓存的值
*/
fun <T> getObject(key: String) = redisTemplate.opsForValue().get(key) as? T
/**
* 删除单个对象
*
* @param key 缓存的键
* @return true=删除成功false=删除失败
*/
fun delObject(key: String) = redisTemplate.delete(key)
/**
* 删除对象集合
*
* @param collection 键集合
* @return 删除个数
*/
fun delObject(collection: Collection<String>) = redisTemplate.delete(collection)
/**
* 缓存 List 数据
*
* @param key 缓存的键
* @param dataList 缓存的 List 数据
* @return 缓存的个数
*/
fun setList(key: String, dataList: List<Any>) = redisTemplate.opsForList().rightPushAll(key, dataList)
/**
* 获得缓存的 List 数据
*
* @param key 缓存的键
* @return 缓存的键对应的 List 数据
*/
fun <T> getList(key: String): List<T>? = redisTemplate.opsForList().range(key, 0, -1) as? List<T>
/**
* 缓存 Set 数据
*
* @param key 缓存的键
* @param dataSet 缓存的 Set 数据
* @return 缓存数据的对象
*/
fun setSet(key: String, dataSet: Set<Any>): BoundSetOperations<String, Any> {
val boundSetOps: BoundSetOperations<String, Any> = redisTemplate.boundSetOps(key)
for (data in dataSet) {
boundSetOps.add(data)
}
return boundSetOps
}
/**
* 获得缓存的 Set 数据
*
* @param key 缓存的键
* @return 缓存的键对应的 Set 数据
*/
fun <T> getSet(key: String): Set<T>? = redisTemplate.opsForSet().members(key) as? Set<T>
/**
* 缓存 Map 数据
*
* @param key 缓存的键
* @param dataMap 缓存的 Map 数据
*/
fun setMap(key: String, dataMap: Map<String, Any>) = redisTemplate.opsForHash<String, Any>().putAll(key, dataMap)
/**
* 获得缓存的 Map 数据
*
* @param key 缓存的键
* @return 缓存的键对应的 Map 数据
*/
fun <T> getMap(key: String): Map<String, T>? =
redisTemplate.opsForHash<String, Any>().entries(key) as? Map<String, T>
/**
* 往 Hash 中存入数据
*
* @param key Redis 键
* @param hKey Hash 键
* @param value 值
*/
fun setMapValue(key: String, hKey: String, value: Any) =
redisTemplate.opsForHash<String, Any>().put(key, hKey, value)
/**
* 获取 Hash 中的数据
*
* @param key Redis 键
* @param hKey Hash 键
* @return Hash 中的对象
*/
fun <T> getMapValue(key: String, hKey: String) = redisTemplate.opsForHash<String, T>().get(key, hKey)
/**
* 删除 Hash 中的数据
*
* @param key Redis 键
* @param hKey Hash 键
*/
fun delMapValue(key: String, hKey: String) = redisTemplate.opsForHash<String, Any>().delete(key, hKey)
/**
* 获取多个 Hash 中的数据
*
* @param key Redis 键
* @param hKeys Hash 键集合
* @return Hash 对象集合
*/
fun <T> getMultiMapValue(key: String, hKeys: Collection<String>): List<T> =
redisTemplate.opsForHash<String, T>().multiGet(key, hKeys)
}

View File

@@ -0,0 +1,16 @@
package top.fatweb.api.util
import jakarta.servlet.http.HttpServletRequest
import org.springframework.security.core.context.SecurityContextHolder
import top.fatweb.api.constant.SecurityConstants
import top.fatweb.api.entity.permission.LoginUser
object WebUtil {
fun getLoginUser() = SecurityContextHolder.getContext().authentication.principal as LoginUser
fun getLoginUserId() = getLoginUser().user.id
fun getToken(tokenWithPrefix: String) = tokenWithPrefix.removePrefix(SecurityConstants.tokenPrefix)
fun getToken(request: HttpServletRequest) = getToken(request.getHeader(SecurityConstants.headerString))
}

View File

@@ -0,0 +1,11 @@
package top.fatweb.api.vo
import io.swagger.v3.oas.annotations.media.Schema
@Schema(description = "登录返回参数")
data class LoginVo(
@Schema(
description = "Token",
example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkYTllYjFkYmVmZDQ0OWRkOThlOGNjNzZlNzZkMDgyNSIsInN1YiI6IjE3MDk5ODYwNTg2Nzk5NzU5MzgiLCJpc3MiOiJGYXRXZWIiLCJpYXQiOjE2OTY1MjgxMTcsImV4cCI6MTY5NjUzNTMxN30.U2ZsyrGk7NbsP-DJfdz9xgWSfect5r2iKQnlEsscAA8"
) val token: String
)

View File

@@ -0,0 +1,11 @@
package top.fatweb.api.vo
import io.swagger.v3.oas.annotations.media.Schema
@Schema(description = "Token")
data class TokenVo(
@Schema(
description = "Token",
example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkYTllYjFkYmVmZDQ0OWRkOThlOGNjNzZlNzZkMDgyNSIsInN1YiI6IjE3MDk5ODYwNTg2Nzk5NzU5MzgiLCJpc3MiOiJGYXRXZWIiLCJpYXQiOjE2OTY1MjgxMTcsImV4cCI6MTY5NjUzNTMxN30.U2ZsyrGk7NbsP-DJfdz9xgWSfect5r2iKQnlEsscAA8"
) val token: String
)

View File

@@ -0,0 +1,43 @@
app:
security:
# header-string: "Authorization" # The key of head to get token
# token-prefix: "Bearer " # Token prefix
# jwt-ttl: 2 # The life of token
# jwt-ttl-unit: hours # Unit of life of token [nanoseconds, microseconds, milliseconds, seconds, minutes, hours, days]
jwt-key: $uuid$ # Key to generate token (Only numbers and letters allow)
# jwt-issuer: FatWeb # Token issuer
# redis-ttl: 20 # The life of token in redis
# redis-ttl-unit: minutes # Unit of life of token in redis [nanoseconds, microseconds, milliseconds, seconds, minutes, hours, days]
server:
# port: 8080 # Server port
spring:
datasource:
url: jdbc:mysql://localhost # MySQL url
username: root # MySQL username
password: root # MySQL password
druid:
# initial-size: 20 # Initial number of connections
# min-idle: 20 # Minimum number of connection pools
# max-active: 20 # Maximum number of connection pools
# max-wait: 3000 # Maximum connection timeout
# min-evictable-idle-time-millis: 300000 # Minimum time for a connection to live in the pool
# max-evictable-idle-time-millis: 900000 # Maximum time for a connection to live in the pool
# break-after-acquire-failure: true # Terminate the retry when the server fails to reconnect for the specified number of times
# connection-error-retry-attempts: 5 # Number of failed reconnections
data:
redis:
# database: 0 # Redis database (default: 0)
# host: localhost # Redis host (default: localhost)
# port: 6379 # Redis port (default: 6379)
# password: # Password of redis
# connect-timeout: 3000 # Redis connect timeout
# lettuce:
# pool:
# min-idle: 0
# max-idle: 8
# max-active: 8
# max-wait: -1ms

View File

@@ -1 +1,18 @@
app:
version: @project.version@
build-time: @build.timestamp@
spring:
profiles:
active: config
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted
logic-not-delete-value: 0
logic-delete-value: id
id-type: assign_id
type-aliases-package: top.fatweb.api.entity

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.fatweb.api.mapper.UserMapper">
</mapper>

View File

@@ -0,0 +1,34 @@
package top.fatweb.api
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.test.context.junit.jupiter.SpringExtension
import top.fatweb.api.constant.SecurityConstants
import top.fatweb.api.util.JwtUtil
@ExtendWith(SpringExtension::class)
class FatWebApiApplicationTests {
@Test
fun removePrefixTest() {
assertEquals("12312", "Bearer 12312".removePrefix(SecurityConstants.tokenPrefix))
}
@Test
fun jwtTest() {
val jwt = JwtUtil.createJwt("User")
assertEquals("User", jwt?.let { JwtUtil.parseJwt(it).subject })
}
/*
@Test
fun addUser(@Autowired userService: IUserService, @Autowired passwordEncoder: PasswordEncoder) {
val username = "admin"
val rawPassword = "admin"
val encodedPassword = passwordEncoder.encode(rawPassword)
val user = User(username, encodedPassword)
userService.save(user)
}
*/
}

View File

@@ -1,13 +0,0 @@
package top.fatweb.api
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class FatwebApiApplicationTests {
@Test
fun contextLoads() {
}
}