init
This commit is contained in:
parent
6a0b7409a6
commit
f6d25af9b9
12
.gitignore
12
.gitignore
@ -40,14 +40,4 @@ application-local.yml
|
||||
rebel.xml
|
||||
.flattened-pom.xml
|
||||
.DS_Store
|
||||
/.idea/.gitignore
|
||||
/logs/backend-java.log
|
||||
/.idea/compiler.xml
|
||||
/.idea/encodings.xml
|
||||
/.idea/jarRepositories.xml
|
||||
/.idea/misc.xml
|
||||
/.idea/modules.xml
|
||||
/.idea/inspectionProfiles/Project_Default.xml
|
||||
/.idea/vcs.xml
|
||||
/xianyu-core/xianyu-core.iml
|
||||
/xianyu-server/xianyu-server.iml
|
||||
logs/
|
||||
|
||||
19
pom.xml
19
pom.xml
@ -15,6 +15,7 @@
|
||||
<description>Xianyu Management System</description>
|
||||
<modules>
|
||||
<module>xianyu-api</module>
|
||||
<module>xianyu-common</module>
|
||||
<module>xianyu-core</module>
|
||||
<module>xianyu-server</module>
|
||||
<module>xianyu-goofish</module>
|
||||
@ -31,6 +32,7 @@
|
||||
<playwright.version>1.49.0</playwright.version>
|
||||
<sqlite-jdbc.version>3.44.1.0</sqlite-jdbc.version>
|
||||
<hibernate.version>6.4.1.Final</hibernate.version>
|
||||
<jjwt.version>0.12.3</jjwt.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
@ -98,6 +100,23 @@
|
||||
<version>${zxing.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT 相关依赖 -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@ -12,9 +12,9 @@
|
||||
<artifactId>xianyu-api</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>25</maven.compiler.source>
|
||||
<maven.compiler.target>25</maven.compiler.target>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>top.biwin</groupId>
|
||||
<artifactId>xianyu-freedom</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>xianyu-common</artifactId>
|
||||
|
||||
@ -26,6 +26,28 @@
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Security -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@ -2,14 +2,22 @@ package top.biwin.xinayu.server;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* Xianyu Freedom 应用启动类
|
||||
*
|
||||
* 配置说明:
|
||||
* - @EntityScan: 扫描 xianyu-core 模块中的实体类(AdminUser 等)
|
||||
* - @EnableJpaRepositories: 启用 JPA Repository 支持
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-20 23:51
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EntityScan(basePackages = "top.biwin.xianyu.core.entity")
|
||||
@EnableJpaRepositories(basePackages = "top.biwin.xinayu.server.repository")
|
||||
public class XianyuFreedomApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(XianyuFreedomApplication.class, args);
|
||||
|
||||
@ -0,0 +1,137 @@
|
||||
package top.biwin.xinayu.server.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import top.biwin.xinayu.server.security.JwtAuthenticationFilter;
|
||||
|
||||
/**
|
||||
* Spring Security 配置类
|
||||
* 配置认证和授权相关的核心组件
|
||||
*
|
||||
* 配置内容:
|
||||
* 1. SecurityFilterChain - 定义安全过滤器链
|
||||
* 2. PasswordEncoder - BCrypt 密码编码器(强度 12)
|
||||
* 3. AuthenticationManager - 认证管理器
|
||||
* 4. DaoAuthenticationProvider - DAO 认证提供者
|
||||
*
|
||||
* 安全策略:
|
||||
* - 无状态会话管理(JWT 场景)
|
||||
* - 禁用 CSRF(REST API 场景)
|
||||
* - 白名单:/auth/** 接口
|
||||
* - 其他所有接口需要认证
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Autowired
|
||||
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
@Autowired
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
/**
|
||||
* 配置安全过滤器链
|
||||
*
|
||||
* 配置项:
|
||||
* 1. 禁用 CSRF(JWT 无状态认证不需要)
|
||||
* 2. 配置授权规则:
|
||||
* - /auth/** 接口允许匿名访问(登录、刷新令牌)
|
||||
* - 其他所有接口需要认证
|
||||
* 3. 配置无状态会话管理
|
||||
* 4. 添加 JWT 认证过滤器(在 UsernamePasswordAuthenticationFilter 之前)
|
||||
*
|
||||
* @param http HttpSecurity 对象
|
||||
* @return SecurityFilterChain
|
||||
* @throws Exception 配置异常
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// 禁用 CSRF(JWT 场景下不需要)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
|
||||
// 配置授权规则
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// 白名单:允许 /auth/** 接口匿名访问
|
||||
.requestMatchers("/auth/**").permitAll()
|
||||
|
||||
// 其他所有接口都需要认证
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
|
||||
// 配置无状态会话管理(JWT 无状态认证)
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
)
|
||||
|
||||
// 添加 JWT 认证过滤器(在 UsernamePasswordAuthenticationFilter 之前执行)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码编码器 Bean
|
||||
* 使用 BCrypt 算法,强度为 12(2^12 = 4096 次哈希)
|
||||
*
|
||||
* BCrypt 优势:
|
||||
* - 自动加盐
|
||||
* - 计算密集,防暴力破解
|
||||
* - 业界标准
|
||||
*
|
||||
* @return PasswordEncoder
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder(12);
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证管理器 Bean
|
||||
* Spring Security 用于处理认证请求
|
||||
*
|
||||
* @param authenticationConfiguration 认证配置
|
||||
* @return AuthenticationManager
|
||||
* @throws Exception 配置异常
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(
|
||||
AuthenticationConfiguration authenticationConfiguration) throws Exception {
|
||||
return authenticationConfiguration.getAuthenticationManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* DAO 认证提供者 Bean
|
||||
* 连接 UserDetailsService 和 PasswordEncoder
|
||||
*
|
||||
* 工作流程:
|
||||
* 1. 从 UserDetailsService 加载用户信息
|
||||
* 2. 使用 PasswordEncoder 验证密码
|
||||
*
|
||||
* @return DaoAuthenticationProvider
|
||||
*/
|
||||
@Bean
|
||||
public DaoAuthenticationProvider authenticationProvider() {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||
authProvider.setUserDetailsService(userDetailsService);
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
return authProvider;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
package top.biwin.xinayu.server.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import top.biwin.xinayu.server.dto.LoginRequest;
|
||||
import top.biwin.xinayu.server.dto.LoginResponse;
|
||||
import top.biwin.xinayu.server.dto.RefreshRequest;
|
||||
import top.biwin.xinayu.server.dto.RefreshResponse;
|
||||
import top.biwin.xinayu.server.service.AuthService;
|
||||
|
||||
/**
|
||||
* 认证控制器
|
||||
* 提供登录、刷新令牌等认证相关的 REST API
|
||||
*
|
||||
* 接口列表:
|
||||
* - POST /auth/login - 用户登录
|
||||
* - POST /auth/refresh - 刷新访问令牌
|
||||
*
|
||||
* 注意:这些接口在 SecurityConfig 中已配置为白名单,无需认证即可访问
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
public class AuthController {
|
||||
|
||||
@Autowired
|
||||
private AuthService authService;
|
||||
|
||||
/**
|
||||
* 用户登录接口
|
||||
*
|
||||
* 请求示例:
|
||||
* POST /auth/login
|
||||
* {
|
||||
* "username": "admin",
|
||||
* "password": "password123"
|
||||
* }
|
||||
*
|
||||
* 响应示例:
|
||||
* {
|
||||
* "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
* "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
* "expiresIn": 900,
|
||||
* "tokenType": "Bearer"
|
||||
* }
|
||||
*
|
||||
* 错误响应:
|
||||
* - 401 Unauthorized: 用户名或密码错误
|
||||
* - 500 Internal Server Error: 服务器内部错误
|
||||
*
|
||||
* @param request 登录请求(包含 username 和 password)
|
||||
* @return ResponseEntity<LoginResponse> 登录响应
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
|
||||
LoginResponse response = authService.login(request.getUsername(), request.getPassword());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新访问令牌接口
|
||||
*
|
||||
* 请求示例:
|
||||
* POST /auth/refresh
|
||||
* {
|
||||
* "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
* }
|
||||
*
|
||||
* 响应示例:
|
||||
* {
|
||||
* "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
* "expiresIn": 900,
|
||||
* "tokenType": "Bearer"
|
||||
* }
|
||||
*
|
||||
* 错误响应:
|
||||
* - 401 Unauthorized: 刷新令牌无效或已过期
|
||||
* - 500 Internal Server Error: 服务器内部错误
|
||||
*
|
||||
* @param request 刷新令牌请求(包含 refreshToken)
|
||||
* @return ResponseEntity<RefreshResponse> 刷新令牌响应
|
||||
*/
|
||||
@PostMapping("/refresh")
|
||||
public ResponseEntity<RefreshResponse> refresh(@RequestBody RefreshRequest request) {
|
||||
RefreshResponse response = authService.refresh(request.getRefreshToken());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package top.biwin.xinayu.server.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 登录请求 DTO
|
||||
* 用于接收客户端的登录请求参数
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
@Data
|
||||
public class LoginRequest {
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
private String password;
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package top.biwin.xinayu.server.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 登录响应 DTO
|
||||
* 返回给客户端的登录成功响应
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginResponse {
|
||||
/**
|
||||
* 访问令牌(短期有效,用于访问受保护资源)
|
||||
*/
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* 刷新令牌(长期有效,用于获取新的访问令牌)
|
||||
*/
|
||||
private String refreshToken;
|
||||
|
||||
/**
|
||||
* 访问令牌过期时间(秒)
|
||||
*/
|
||||
private Long expiresIn;
|
||||
|
||||
/**
|
||||
* 令牌类型(固定为 "Bearer")
|
||||
*/
|
||||
private String tokenType = "Bearer";
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package top.biwin.xinayu.server.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 刷新令牌请求 DTO
|
||||
* 用于接收客户端的刷新令牌请求参数
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
@Data
|
||||
public class RefreshRequest {
|
||||
/**
|
||||
* 刷新令牌(必填)
|
||||
*/
|
||||
private String refreshToken;
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package top.biwin.xinayu.server.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 刷新令牌响应 DTO
|
||||
* 返回给客户端的刷新令牌成功响应
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RefreshResponse {
|
||||
/**
|
||||
* 新的访问令牌
|
||||
*/
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* 访问令牌过期时间(秒)
|
||||
*/
|
||||
private Long expiresIn;
|
||||
|
||||
/**
|
||||
* 令牌类型(固定为 "Bearer")
|
||||
*/
|
||||
private String tokenType = "Bearer";
|
||||
}
|
||||
@ -0,0 +1,128 @@
|
||||
package top.biwin.xinayu.server.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
* 统一处理认证授权相关的异常,返回友好的 JSON 错误响应
|
||||
*
|
||||
* 处理的异常类型:
|
||||
* - BadCredentialsException: 认证失败(用户名或密码错误)
|
||||
* - UsernameNotFoundException: 用户不存在
|
||||
* - RuntimeException: 运行时异常
|
||||
* - Exception: 其他未知异常
|
||||
*
|
||||
* 响应格式:
|
||||
* {
|
||||
* "timestamp": "2026-01-21T21:50:00",
|
||||
* "status": 401,
|
||||
* "error": "Unauthorized",
|
||||
* "message": "用户名或密码错误",
|
||||
* "path": "/auth/login"
|
||||
* }
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class AuthenticationExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理认证失败异常
|
||||
*
|
||||
* 触发场景:
|
||||
* - 用户名或密码错误
|
||||
* - JWT 令牌无效或过期
|
||||
*
|
||||
* @param ex 异常对象
|
||||
* @return ResponseEntity<Map<String, Object>> 错误响应
|
||||
*/
|
||||
@ExceptionHandler(BadCredentialsException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleBadCredentialsException(BadCredentialsException ex) {
|
||||
Map<String, Object> errorResponse = new HashMap<>();
|
||||
errorResponse.put("timestamp", LocalDateTime.now());
|
||||
errorResponse.put("status", HttpStatus.UNAUTHORIZED.value());
|
||||
errorResponse.put("error", "Unauthorized");
|
||||
errorResponse.put("message", ex.getMessage());
|
||||
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(errorResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户不存在异常
|
||||
*
|
||||
* 触发场景:
|
||||
* - 根据用户名查询用户时,用户不存在
|
||||
*
|
||||
* @param ex 异常对象
|
||||
* @return ResponseEntity<Map<String, Object>> 错误响应
|
||||
*/
|
||||
@ExceptionHandler(UsernameNotFoundException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleUsernameNotFoundException(UsernameNotFoundException ex) {
|
||||
Map<String, Object> errorResponse = new HashMap<>();
|
||||
errorResponse.put("timestamp", LocalDateTime.now());
|
||||
errorResponse.put("status", HttpStatus.UNAUTHORIZED.value());
|
||||
errorResponse.put("error", "Unauthorized");
|
||||
errorResponse.put("message", ex.getMessage());
|
||||
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(errorResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理运行时异常
|
||||
*
|
||||
* 触发场景:
|
||||
* - 用户被禁用
|
||||
* - 业务逻辑异常
|
||||
*
|
||||
* @param ex 异常对象
|
||||
* @return ResponseEntity<Map<String, Object>> 错误响应
|
||||
*/
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException ex) {
|
||||
Map<String, Object> errorResponse = new HashMap<>();
|
||||
errorResponse.put("timestamp", LocalDateTime.now());
|
||||
errorResponse.put("status", HttpStatus.BAD_REQUEST.value());
|
||||
errorResponse.put("error", "Bad Request");
|
||||
errorResponse.put("message", ex.getMessage());
|
||||
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.BAD_REQUEST)
|
||||
.body(errorResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理未知异常
|
||||
*
|
||||
* 触发场景:
|
||||
* - 其他未被捕获的异常
|
||||
*
|
||||
* @param ex 异常对象
|
||||
* @return ResponseEntity<Map<String, Object>> 错误响应
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<Map<String, Object>> handleException(Exception ex) {
|
||||
Map<String, Object> errorResponse = new HashMap<>();
|
||||
errorResponse.put("timestamp", LocalDateTime.now());
|
||||
errorResponse.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
errorResponse.put("error", "Internal Server Error");
|
||||
errorResponse.put("message", "服务器内部错误: " + ex.getMessage());
|
||||
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(errorResponse);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package top.biwin.xinayu.server.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import top.biwin.xianyu.core.entity.AdminUser;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* AdminUser 数据访问层
|
||||
* 提供用户数据的 CRUD 操作
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
@Repository
|
||||
public interface AdminUserRepository extends JpaRepository<AdminUser, Long> {
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return Optional<AdminUser>
|
||||
*/
|
||||
Optional<AdminUser> findByUsername(String username);
|
||||
|
||||
/**
|
||||
* 根据邮箱查询用户
|
||||
*
|
||||
* @param email 邮箱
|
||||
* @return Optional<AdminUser>
|
||||
*/
|
||||
Optional<AdminUser> findByEmail(String email);
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
package top.biwin.xinayu.server.security;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* JWT 认证过滤器
|
||||
* 从请求头中提取 JWT 令牌,验证并设置 Spring Security 上下文
|
||||
*
|
||||
* 工作流程:
|
||||
* 1. 从请求头 "Authorization" 中提取 JWT(格式:Bearer <token>)
|
||||
* 2. 验证 JWT 的有效性
|
||||
* 3. 从 JWT 中提取用户名,加载用户信息
|
||||
* 4. 将认证信息设置到 Spring Security Context
|
||||
*
|
||||
* 安全性说明:
|
||||
* - 使用 OncePerRequestFilter 确保每个请求只执行一次
|
||||
* - 异常情况下不会阻断请求,由后续的认证检查处理
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
@Autowired
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
/**
|
||||
* JWT 过滤逻辑
|
||||
*
|
||||
* 处理流程:
|
||||
* 1. 提取请求头中的 Authorization
|
||||
* 2. 检查是否以 "Bearer " 开头
|
||||
* 3. 提取 JWT 令牌
|
||||
* 4. 验证令牌并提取用户名
|
||||
* 5. 加载用户详情并设置到 Security Context
|
||||
*
|
||||
* @param request HTTP 请求
|
||||
* @param response HTTP 响应
|
||||
* @param filterChain 过滤器链
|
||||
* @throws ServletException Servlet 异常
|
||||
* @throws IOException IO 异常
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
// 1. 从请求头中获取 Authorization
|
||||
final String authorizationHeader = request.getHeader("Authorization");
|
||||
|
||||
String username = null;
|
||||
String jwt = null;
|
||||
|
||||
// 2. 检查 Authorization 头是否存在且以 "Bearer " 开头
|
||||
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
|
||||
// 3. 提取 JWT 令牌(去掉 "Bearer " 前缀)
|
||||
jwt = authorizationHeader.substring(7);
|
||||
|
||||
try {
|
||||
// 4. 从 JWT 中提取用户名
|
||||
username = jwtUtil.getUsernameFromToken(jwt);
|
||||
} catch (Exception e) {
|
||||
// JWT 解析失败,记录日志(可选)
|
||||
logger.warn("JWT 解析失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 如果提取到用户名且当前 Security Context 中没有认证信息
|
||||
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
|
||||
// 6. 加载用户详情
|
||||
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
|
||||
|
||||
// 7. 验证 JWT 是否有效
|
||||
if (jwtUtil.validateToken(jwt, userDetails.getUsername())) {
|
||||
|
||||
// 8. 创建认证令牌
|
||||
UsernamePasswordAuthenticationToken authenticationToken =
|
||||
new UsernamePasswordAuthenticationToken(
|
||||
userDetails,
|
||||
null,
|
||||
userDetails.getAuthorities()
|
||||
);
|
||||
|
||||
// 9. 设置请求详情
|
||||
authenticationToken.setDetails(
|
||||
new WebAuthenticationDetailsSource().buildDetails(request)
|
||||
);
|
||||
|
||||
// 10. 将认证信息设置到 Security Context
|
||||
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
|
||||
}
|
||||
}
|
||||
|
||||
// 11. 继续执行过滤器链
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,177 @@
|
||||
package top.biwin.xinayu.server.security;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JWT 工具类
|
||||
* 负责 JWT 令牌的生成、解析和验证
|
||||
*
|
||||
* 安全性说明:
|
||||
* - 使用 HS256 算法签名
|
||||
* - 密钥从环境变量读取,避免硬编码
|
||||
* - Access Token 短期有效(15分钟)
|
||||
* - Refresh Token 长期有效(7天)
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
@Component
|
||||
public class JwtUtil {
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.access-token-expiration}")
|
||||
private Long accessTokenExpiration;
|
||||
|
||||
@Value("${jwt.refresh-token-expiration}")
|
||||
private Long refreshTokenExpiration;
|
||||
|
||||
/**
|
||||
* 生成访问令牌(Access Token)
|
||||
* 有效期:15分钟
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return JWT 令牌字符串
|
||||
*/
|
||||
public String generateAccessToken(String username) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("type", "access");
|
||||
return createToken(claims, username, accessTokenExpiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成刷新令牌(Refresh Token)
|
||||
* 有效期:7天
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return JWT 令牌字符串
|
||||
*/
|
||||
public String generateRefreshToken(String username) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("type", "refresh");
|
||||
return createToken(claims, username, refreshTokenExpiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从令牌中提取用户名
|
||||
*
|
||||
* @param token JWT 令牌
|
||||
* @return 用户名
|
||||
*/
|
||||
public String getUsernameFromToken(String token) {
|
||||
return extractAllClaims(token).getSubject();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证令牌有效性
|
||||
* 检查令牌是否过期以及用户名是否匹配
|
||||
*
|
||||
* @param token JWT 令牌
|
||||
* @param username 用户名
|
||||
* @return 是否有效
|
||||
*/
|
||||
public boolean validateToken(String token, String username) {
|
||||
final String tokenUsername = getUsernameFromToken(token);
|
||||
return (tokenUsername.equals(username) && !isTokenExpired(token));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证令牌是否有效(不检查用户名)
|
||||
*
|
||||
* @param token JWT 令牌
|
||||
* @return 是否有效
|
||||
*/
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
return !isTokenExpired(token);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查令牌是否过期
|
||||
*
|
||||
* @param token JWT 令牌
|
||||
* @return 是否过期
|
||||
*/
|
||||
private boolean isTokenExpired(String token) {
|
||||
return extractExpiration(token).before(new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从令牌中提取过期时间
|
||||
*
|
||||
* @param token JWT 令牌
|
||||
* @return 过期时间
|
||||
*/
|
||||
private Date extractExpiration(String token) {
|
||||
return extractAllClaims(token).getExpiration();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从令牌中提取所有声明(Claims)
|
||||
*
|
||||
* @param token JWT 令牌
|
||||
* @return Claims 对象
|
||||
*/
|
||||
private Claims extractAllClaims(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(getSigningKey())
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 JWT 令牌
|
||||
*
|
||||
* @param claims 自定义声明
|
||||
* @param subject 主题(用户名)
|
||||
* @param expiration 过期时间(毫秒)
|
||||
* @return JWT 令牌字符串
|
||||
*/
|
||||
private String createToken(Map<String, Object> claims, String subject, Long expiration) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expiration);
|
||||
|
||||
return Jwts.builder()
|
||||
.claims(claims)
|
||||
.subject(subject)
|
||||
.issuedAt(now)
|
||||
.expiration(expiryDate)
|
||||
.signWith(getSigningKey())
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取签名密钥
|
||||
* 使用 HMAC-SHA 算法,密钥长度至少 256 位
|
||||
*
|
||||
* @return SecretKey
|
||||
*/
|
||||
private SecretKey getSigningKey() {
|
||||
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
|
||||
return Keys.hmacShaKeyFor(keyBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取访问令牌过期时间(秒)
|
||||
*
|
||||
* @return 过期时间(秒)
|
||||
*/
|
||||
public Long getAccessTokenExpirationInSeconds() {
|
||||
return accessTokenExpiration / 1000;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package top.biwin.xinayu.server.service;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import top.biwin.xianyu.core.entity.AdminUser;
|
||||
import top.biwin.xinayu.server.repository.AdminUserRepository;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* AdminUser 用户详情服务
|
||||
* 实现 Spring Security 的 UserDetailsService 接口
|
||||
*
|
||||
* ⭐️ 这是主人实现自定义用户查询逻辑的核心位置!
|
||||
*
|
||||
* 主要职责:
|
||||
* 1. 根据用户名从数据库查询 AdminUser
|
||||
* 2. 检查用户是否存在和是否激活
|
||||
* 3. 将 AdminUser 转换为 Spring Security 的 UserDetails
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
@Service
|
||||
public class AdminUserDetailsService implements UserDetailsService {
|
||||
|
||||
@Autowired
|
||||
private AdminUserRepository adminUserRepository;
|
||||
|
||||
/**
|
||||
* 根据用户名加载用户信息
|
||||
*
|
||||
* ⭐️ 主人的自定义逻辑实现位置 #1
|
||||
*
|
||||
* TODO: 主人请在这里实现用户查询逻辑!具体步骤如下:
|
||||
* 1. 调用 adminUserRepository.findByUsername(username) 查询数据库
|
||||
* 2. 如果用户不存在,抛出 UsernameNotFoundException 异常(格式:"用户不存在: " + username)
|
||||
* 3. 检查用户的 isActive 字段,如果为 false,抛出异常(格式:"用户已被禁用: " + username)
|
||||
* 4. 使用 User.builder() 创建 UserDetails 对象:
|
||||
* - username: adminUser.getUsername()
|
||||
* - password: adminUser.getPasswordHash()
|
||||
* - authorities: new ArrayList<>()(空权限列表,主人可以后续扩展角色权限)
|
||||
* 5. 返回构建好的 UserDetails 对象
|
||||
*
|
||||
* 💡 扩展提示:
|
||||
* - 可以在这里添加登录失败次数限制(需要在 AdminUser 实体中添加 failedLoginAttempts 字段)
|
||||
* - 可以添加 IP 白名单验证
|
||||
* - 可以添加账号锁定逻辑
|
||||
* - 如果后续需要角色权限,可以在 AdminUser 中添加 roles 字段,并在这里设置 authorities
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return UserDetails Spring Security 的用户详情对象
|
||||
* @throws UsernameNotFoundException 用户不存在时抛出
|
||||
*/
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
// TODO: 主人请实现这里的逻辑!
|
||||
// 1. AdminUser adminUser = adminUserRepository.findByUsername(username)
|
||||
// .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
|
||||
// 2. if (!adminUser.getIsActive()) { throw new RuntimeException("用户已被禁用: " + username); }
|
||||
// 3. return User.builder()
|
||||
// .username(adminUser.getUsername())
|
||||
// .password(adminUser.getPasswordHash())
|
||||
// .authorities(new ArrayList<>())
|
||||
// .build();
|
||||
|
||||
throw new UnsupportedOperationException("主人,请实现 loadUserByUsername 方法!");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
package top.biwin.xinayu.server.service;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Service;
|
||||
import top.biwin.xinayu.server.dto.LoginResponse;
|
||||
import top.biwin.xinayu.server.dto.RefreshResponse;
|
||||
import top.biwin.xinayu.server.security.JwtUtil;
|
||||
|
||||
/**
|
||||
* 认证服务
|
||||
* 提供登录、刷新令牌等认证相关功能
|
||||
*
|
||||
* ⭐️ 这是主人实现登录和刷新令牌逻辑的核心位置!
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
@Service
|
||||
public class AuthService {
|
||||
|
||||
@Autowired
|
||||
private AuthenticationManager authenticationManager;
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* ⭐️ 主人的自定义逻辑实现位置 #2
|
||||
*
|
||||
* TODO: 主人请在这里实现登录逻辑!具体步骤如下:
|
||||
* 1. 创建认证令牌:
|
||||
* UsernamePasswordAuthenticationToken authToken =
|
||||
* new UsernamePasswordAuthenticationToken(username, password);
|
||||
* 2. 使用 AuthenticationManager 进行认证:
|
||||
* Authentication authentication = authenticationManager.authenticate(authToken);
|
||||
* 注意:如果认证失败,会自动抛出 BadCredentialsException 异常
|
||||
* 3. 认证成功后,生成 JWT 令牌:
|
||||
* String accessToken = jwtUtil.generateAccessToken(username);
|
||||
* String refreshToken = jwtUtil.generateRefreshToken(username);
|
||||
* 4. 获取过期时间:
|
||||
* Long expiresIn = jwtUtil.getAccessTokenExpirationInSeconds();
|
||||
* 5. 构建并返回 LoginResponse 对象:
|
||||
* return new LoginResponse(accessToken, refreshToken, expiresIn, "Bearer");
|
||||
*
|
||||
* 💡 扩展提示:
|
||||
* - 可以在这里记录登录日志(时间、IP、设备信息等)
|
||||
* - 可以重置登录失败次数
|
||||
* - 可以实现多因素认证(2FA)逻辑
|
||||
* - 可以返回用户的基本信息(如昵称、头像等)
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param password 密码(明文)
|
||||
* @return LoginResponse 登录响应(包含 accessToken 和 refreshToken)
|
||||
* @throws BadCredentialsException 认证失败时抛出
|
||||
*/
|
||||
public LoginResponse login(String username, String password) {
|
||||
// TODO: 主人请实现这里的逻辑!
|
||||
// 1. UsernamePasswordAuthenticationToken authToken =
|
||||
// new UsernamePasswordAuthenticationToken(username, password);
|
||||
// 2. Authentication authentication = authenticationManager.authenticate(authToken);
|
||||
// 3. String accessToken = jwtUtil.generateAccessToken(username);
|
||||
// 4. String refreshToken = jwtUtil.generateRefreshToken(username);
|
||||
// 5. Long expiresIn = jwtUtil.getAccessTokenExpirationInSeconds();
|
||||
// 6. return new LoginResponse(accessToken, refreshToken, expiresIn, "Bearer");
|
||||
|
||||
throw new UnsupportedOperationException("主人,请实现 login 方法!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新访问令牌
|
||||
*
|
||||
* ⭐️ 主人的自定义逻辑实现位置 #3
|
||||
*
|
||||
* TODO: 主人请在这里实现刷新令牌逻辑!具体步骤如下:
|
||||
* 1. 验证 refreshToken 的有效性:
|
||||
* if (!jwtUtil.validateToken(refreshToken)) {
|
||||
* throw new BadCredentialsException("刷新令牌无效或已过期");
|
||||
* }
|
||||
* 2. 从 refreshToken 中提取用户名:
|
||||
* String username = jwtUtil.getUsernameFromToken(refreshToken);
|
||||
* 3. 生成新的 Access Token:
|
||||
* String newAccessToken = jwtUtil.generateAccessToken(username);
|
||||
* 4. 获取过期时间:
|
||||
* Long expiresIn = jwtUtil.getAccessTokenExpirationInSeconds();
|
||||
* 5. 构建并返回 RefreshResponse 对象:
|
||||
* return new RefreshResponse(newAccessToken, expiresIn, "Bearer");
|
||||
*
|
||||
* 💡 扩展提示:
|
||||
* - 可以实现 Refresh Token 轮换机制(每次刷新都返回新的 refreshToken)
|
||||
* - 可以实现 Refresh Token 黑名单(用户登出时将 refreshToken 加入黑名单)
|
||||
* - 可以限制单个 refreshToken 的使用次数
|
||||
* - 可以检查用户是否仍然处于激活状态(调用 AdminUserRepository 再次验证)
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return RefreshResponse 刷新响应(包含新的 accessToken)
|
||||
* @throws BadCredentialsException 刷新令牌无效时抛出
|
||||
*/
|
||||
public RefreshResponse refresh(String refreshToken) {
|
||||
// TODO: 主人请实现这里的逻辑!
|
||||
// 1. if (!jwtUtil.validateToken(refreshToken)) {
|
||||
// throw new BadCredentialsException("刷新令牌无效或已过期");
|
||||
// }
|
||||
// 2. String username = jwtUtil.getUsernameFromToken(refreshToken);
|
||||
// 3. String newAccessToken = jwtUtil.generateAccessToken(username);
|
||||
// 4. Long expiresIn = jwtUtil.getAccessTokenExpirationInSeconds();
|
||||
// 5. return new RefreshResponse(newAccessToken, expiresIn, "Bearer");
|
||||
|
||||
throw new UnsupportedOperationException("主人,请实现 refresh 方法!");
|
||||
}
|
||||
}
|
||||
@ -23,6 +23,7 @@ spring:
|
||||
hibernate:
|
||||
ddl-auto: ${app.ddl-auto:update}
|
||||
show-sql: true # Set to false to disable SQL logging
|
||||
open-in-view: false # 生产环境最佳实践,避免懒加载问题
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true # Set to false to disable SQL formatting
|
||||
@ -36,7 +37,24 @@ logging:
|
||||
level:
|
||||
root: INFO
|
||||
com.xianyu.autoreply: DEBUG
|
||||
top.biwin.xinayu: DEBUG
|
||||
org.springframework.security: DEBUG
|
||||
org.hibernate.SQL: INFO # Ensure Hibernate SQL logging is not set to DEBUG/TRACE
|
||||
org.hibernate.type.descriptor.sql: INFO # Ensure Hibernate parameter logging is not set to DEBUG/TRACE
|
||||
file:
|
||||
name: logs/backend-java.log
|
||||
|
||||
# JWT 配置
|
||||
jwt:
|
||||
# JWT 密钥(生产环境请使用环境变量 JWT_SECRET)
|
||||
# 建议使用至少 256 位的随机字符串
|
||||
secret: ${JWT_SECRET:xianyu-freedom-default-secret-key-please-change-in-production-environment-2026}
|
||||
|
||||
# Access Token 过期时间(毫秒)
|
||||
# 15 分钟 = 900,000 毫秒
|
||||
access-token-expiration: 900000
|
||||
|
||||
# Refresh Token 过期时间(毫秒)
|
||||
# 7 天 = 604,800,000 毫秒
|
||||
refresh-token-expiration: 604800000
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user