Add authentication #1
4
.gitignore
vendored
4
.gitignore
vendored
@@ -31,3 +31,7 @@ build/
|
|||||||
|
|
||||||
### VS Code ###
|
### VS Code ###
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
### Custom ###
|
||||||
|
/application-config.yml
|
||||||
|
data
|
||||||
|
|||||||
13
Dockerfile
Normal file
13
Dockerfile
Normal 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
9
build-docker.sh
Normal 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
12
db/schema.sql
Normal 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
132
pom.xml
@@ -13,9 +13,57 @@
|
|||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
<name>fatweb-api</name>
|
<name>fatweb-api</name>
|
||||||
<description>fatweb-api</description>
|
<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>
|
<properties>
|
||||||
<java.version>17</java.version>
|
<java.version>17</java.version>
|
||||||
<kotlin.version>1.8.22</kotlin.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>
|
</properties>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -26,6 +74,10 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.module</groupId>
|
<groupId>com.fasterxml.jackson.module</groupId>
|
||||||
<artifactId>jackson-module-kotlin</artifactId>
|
<artifactId>jackson-module-kotlin</artifactId>
|
||||||
@@ -46,9 +98,9 @@
|
|||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>com.mysql</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
<optional>true</optional>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
@@ -60,31 +112,80 @@
|
|||||||
<artifactId>spring-security-test</artifactId>
|
<artifactId>spring-security-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</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>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
|
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
|
||||||
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
|
||||||
<groupId>org.graalvm.buildtools</groupId>
|
|
||||||
<artifactId>native-maven-plugin</artifactId>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
<configuration>
|
|
||||||
<excludes>
|
|
||||||
<exclude>
|
|
||||||
<groupId>org.projectlombok</groupId>
|
|
||||||
<artifactId>lombok</artifactId>
|
|
||||||
</exclude>
|
|
||||||
</excludes>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.jetbrains.kotlin</groupId>
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
<artifactId>kotlin-maven-plugin</artifactId>
|
<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>
|
<configuration>
|
||||||
<args>
|
<args>
|
||||||
<arg>-Xjsr305=strict</arg>
|
<arg>-Xjsr305=strict</arg>
|
||||||
@@ -103,5 +204,4 @@
|
|||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -1,11 +1,36 @@
|
|||||||
package top.fatweb.api
|
package top.fatweb.api
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
import org.springframework.boot.runApplication
|
import org.springframework.boot.runApplication
|
||||||
|
import org.springframework.transaction.annotation.EnableTransactionManagement
|
||||||
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
|
@EnableTransactionManagement
|
||||||
class FatWebApiApplication
|
class FatWebApiApplication
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
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)
|
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("-", "")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/main/kotlin/top/fatweb/api/annotation/ApiVersion.kt
Normal file
11
src/main/kotlin/top/fatweb/api/annotation/ApiVersion.kt
Normal 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
|
||||||
|
)
|
||||||
18
src/main/kotlin/top/fatweb/api/config/FilterConfig.kt
Normal file
18
src/main/kotlin/top/fatweb/api/config/FilterConfig.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main/kotlin/top/fatweb/api/config/MybatisPlusConfig.kt
Normal file
20
src/main/kotlin/top/fatweb/api/config/MybatisPlusConfig.kt
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
42
src/main/kotlin/top/fatweb/api/config/RedisConfig.kt
Normal file
42
src/main/kotlin/top/fatweb/api/config/RedisConfig.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/main/kotlin/top/fatweb/api/config/SecurityConfig.kt
Normal file
92
src/main/kotlin/top/fatweb/api/config/SecurityConfig.kt
Normal 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()
|
||||||
|
}
|
||||||
23
src/main/kotlin/top/fatweb/api/config/SwaggerConfig.kt
Normal file
23
src/main/kotlin/top/fatweb/api/config/SwaggerConfig.kt
Normal 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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
25
src/main/kotlin/top/fatweb/api/constant/SecurityConstants.kt
Normal file
25
src/main/kotlin/top/fatweb/api/constant/SecurityConstants.kt
Normal 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
|
||||||
|
}
|
||||||
18
src/main/kotlin/top/fatweb/api/constant/ServerConstants.kt
Normal file
18
src/main/kotlin/top/fatweb/api/constant/ServerConstants.kt
Normal 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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/main/kotlin/top/fatweb/api/controller/UserController.kt
Normal file
17
src/main/kotlin/top/fatweb/api/controller/UserController.kt
Normal 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
|
||||||
|
|
||||||
@@ -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))
|
||||||
|
)
|
||||||
|
}
|
||||||
17
src/main/kotlin/top/fatweb/api/converter/UserConverter.kt
Normal file
17
src/main/kotlin/top/fatweb/api/converter/UserConverter.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package top.fatweb.api.entity.common
|
||||||
|
|
||||||
|
enum class BusinessCode(val code: Int) {
|
||||||
|
SYSTEM(100),
|
||||||
|
DATABASE(200)
|
||||||
|
}
|
||||||
24
src/main/kotlin/top/fatweb/api/entity/common/ResponseCode.kt
Normal file
24
src/main/kotlin/top/fatweb/api/entity/common/ResponseCode.kt
Normal 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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
54
src/main/kotlin/top/fatweb/api/entity/permission/User.kt
Normal file
54
src/main/kotlin/top/fatweb/api/entity/permission/User.kt
Normal 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}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package top.fatweb.api.exception
|
||||||
|
|
||||||
|
class TokenHasExpiredException : RuntimeException("Token has expired")
|
||||||
23
src/main/kotlin/top/fatweb/api/filter/ExceptionFilter.kt
Normal file
23
src/main/kotlin/top/fatweb/api/filter/ExceptionFilter.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt
Normal file
67
src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/main/kotlin/top/fatweb/api/mapper/UserMapper.kt
Normal file
16
src/main/kotlin/top/fatweb/api/mapper/UserMapper.kt
Normal 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>
|
||||||
17
src/main/kotlin/top/fatweb/api/param/LoginParam.kt
Normal file
17
src/main/kotlin/top/fatweb/api/param/LoginParam.kt
Normal 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
|
||||||
|
}
|
||||||
14
src/main/kotlin/top/fatweb/api/service/IUserService.kt
Normal file
14
src/main/kotlin/top/fatweb/api/service/IUserService.kt
Normal 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>
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
26
src/main/kotlin/top/fatweb/api/util/ApiVersionCondition.kt
Normal file
26
src/main/kotlin/top/fatweb/api/util/ApiVersionCondition.kt
Normal 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
|
||||||
|
}
|
||||||
67
src/main/kotlin/top/fatweb/api/util/JwtUtil.kt
Normal file
67
src/main/kotlin/top/fatweb/api/util/JwtUtil.kt
Normal 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)
|
||||||
|
}
|
||||||
183
src/main/kotlin/top/fatweb/api/util/RedisUtil.kt
Normal file
183
src/main/kotlin/top/fatweb/api/util/RedisUtil.kt
Normal 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)
|
||||||
|
}
|
||||||
16
src/main/kotlin/top/fatweb/api/util/WebUtil.kt
Normal file
16
src/main/kotlin/top/fatweb/api/util/WebUtil.kt
Normal 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))
|
||||||
|
}
|
||||||
11
src/main/kotlin/top/fatweb/api/vo/LoginVo.kt
Normal file
11
src/main/kotlin/top/fatweb/api/vo/LoginVo.kt
Normal 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
|
||||||
|
)
|
||||||
11
src/main/kotlin/top/fatweb/api/vo/TokenVo.kt
Normal file
11
src/main/kotlin/top/fatweb/api/vo/TokenVo.kt
Normal 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
|
||||||
|
)
|
||||||
43
src/main/resources/application-config-template.yml
Normal file
43
src/main/resources/application-config-template.yml
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
5
src/main/resources/mapper/UserMapper.xml
Normal file
5
src/main/resources/mapper/UserMapper.xml
Normal 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>
|
||||||
34
src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt
Normal file
34
src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt
Normal 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)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
@@ -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() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user