feat: 优化JWT认证过滤器并增强跨域配置

refactor: 重构安全配置和认证控制器逻辑

fix: 修正消息服务中的空值检查逻辑

feat(article): 为分类属性添加文章计数功能

chore: 删除不再使用的初始化类和字符编码配置

style: 清理代码中的无用导入和空白行

perf(cors): 添加开发环境跨域配置和响应头

docs: 更新JWT配置注释和默认密钥

test: 移除Spring Data Web分页属性导入

ci: 添加spring-devtools配置支持JJWT库
This commit is contained in:
qingfeng1121
2025-12-25 13:25:25 +08:00
parent d679661fac
commit fe3bff2642
18 changed files with 128 additions and 238 deletions

View File

@@ -1,48 +0,0 @@
package com.qf.myafterprojecy.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.filter.CharacterEncodingFilter;
import javax.servlet.Filter;
import java.nio.charset.StandardCharsets;
/**
* 字符编码配置类
* 确保所有HTTP请求和响应都使用UTF-8编码解决中文乱码问题
*/
@Configuration
public class CharacterEncodingConfig {
/**
* 创建字符编码过滤器
* 优先级设置为最高,确保在所有其他过滤器之前执行
*/
@Bean
public FilterRegistrationBean<Filter> characterEncodingFilter() {
// 创建字符编码过滤器
CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
// 设置请求编码为UTF-8
encodingFilter.setEncoding(StandardCharsets.UTF_8.name());
// 强制请求使用UTF-8编码
encodingFilter.setForceRequestEncoding(true);
// 强制响应使用UTF-8编码
encodingFilter.setForceResponseEncoding(true);
// 创建过滤器注册Bean
FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>(encodingFilter);
// 设置过滤器顺序为最高优先级
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
// 为所有请求路径注册过滤器
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}

View File

@@ -26,10 +26,9 @@ public class CorsConfig {
@Value("${cors.allow-credentials}")
private Boolean allowCredentials;
@Value("${cors.max-age:3600}")
@Value("${cors.max-age}")
private Long maxAge;
/**
* 创建CORS过滤器配置跨域请求的规则
* 从配置文件中读取CORS配置实现配置的统一管理
@@ -64,7 +63,8 @@ public class CorsConfig {
config.addExposedHeader("Accept");
config.addExposedHeader("Access-Control-Allow-Origin");
config.addExposedHeader("Access-Control-Allow-Credentials");
config.addExposedHeader("X-Total-Count"); // 分页常用
config.addExposedHeader("Link"); // HATEOAS 常用
// 设置预检请求的有效期(秒),从配置文件读取
config.setMaxAge(maxAge);

View File

@@ -1,10 +1,12 @@
package com.qf.myafterprojecy.config;
import com.qf.myafterprojecy.utils.JwtUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
@@ -40,19 +42,28 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
private String tokenPrefix;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
// 获取token
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
// 仅处理需要认证的请求token为空时允许继续执行过滤器链
String token = getTokenFromRequest(request);
// System.out.println(token);
if (token != null) {
if (validateToken(token)) {
// 如果没有token继续执行过滤器链让后续的过滤器或控制器来处理是否需要认证
if (token == null) {
filterChain.doFilter(request, response);
return;
}
try {
// 从token中获取用户名
String username = jwtUtils.getUsernameFromToken(token);
// System.out.println("username: " + username);
// 如果用户名不为空且当前上下文没有认证信息,则进行认证
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 加载用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 验证token
if (jwtUtils.validateToken(token, userDetails)) {
// 创建认证对象
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
@@ -60,35 +71,18 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
// 设置认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
// 如果token无效但不为空返回401
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"message\":\"无效的认证令牌\"}");
return;
logger.debug("用户 {} 认证成功", username);
}
}
} catch (io.jsonwebtoken.ExpiredJwtException e) {
// 专门处理令牌过期返回401和明确的错误信息
logger.error("令牌已过期: {}", e.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"message\":\"认证令牌已过期\"}");
sendError(response, "令牌过期", HttpServletResponse.SC_UNAUTHORIZED);
return;
} catch (io.jsonwebtoken.JwtException e) {
// 其他JWT异常
logger.error("无效的token格式: {}", e.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"message\":\"无效的令牌格式\"}");
} catch (io.jsonwebtoken.JwtException | IllegalArgumentException e) {
sendError(response, "无效的令牌", HttpServletResponse.SC_UNAUTHORIZED);
return;
} catch (Exception e) {
logger.error("无法设置用户认证: {}", e);
SecurityContextHolder.clearContext();
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"message\":\"认证失败\"}");
logger.error("JWT 认证未知错误", e);
sendError(response, "认证服务异常", HttpServletResponse.SC_UNAUTHORIZED);
return;
}
@@ -101,32 +95,22 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader(tokenHeader);
if (bearerToken != null && bearerToken.startsWith(tokenPrefix + " ")) {
return bearerToken.substring(tokenPrefix.length() + 1);
return bearerToken.substring(tokenPrefix.length() + 1).trim();
}
return null;
}
/**
* 验证token
* 发送未授权响应
*/
private boolean validateToken(String token) {
try {
String username = jwtUtils.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
return jwtUtils.validateToken(token, userDetails);
} catch (io.jsonwebtoken.ExpiredJwtException e) {
// 专门处理令牌过期,记录日志但不抛出异常
logger.error("令牌已过期: {}", e.getMessage());
return false;
} catch (io.jsonwebtoken.JwtException e) {
// 其他JWT异常
logger.error("无效的token格式: {}", e.getMessage());
return false;
} catch (Exception e) {
// 其他异常
logger.error("验证token失败: {}", e.getMessage());
return false;
}
private void sendError(HttpServletResponse response, String message, int status) throws IOException {
response.setStatus(status);
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(String.format("{\"message\":\"%s\"}", message));
}
}

View File

@@ -11,8 +11,6 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletResponse;
@@ -27,12 +25,6 @@ import javax.servlet.http.HttpServletResponse;
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@@ -52,31 +44,21 @@ public class SecurityConfig {
* @throws Exception 配置过程中可能出现的异常
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain filterChain( HttpSecurity http) throws Exception {
http
// 禁用CSRF保护对于API服务通常不需要
.csrf().disable()
// 使用自定义的CORS过滤器不使用默认的cors()配置
// 配置URL访问权限
.authorizeRequests()
// 允许公开访问的路径
// 登录和认证相关端点应该全部公开
.antMatchers(HttpMethod.POST,"/api/auth/**").permitAll()
// 文章浏览量增加接口公开
.antMatchers(HttpMethod.POST,"/api/articles/view/**").permitAll()
// 所有GET请求公开
.antMatchers(HttpMethod.GET,"/api/**").permitAll()
// 公开评论新增接口
.antMatchers(HttpMethod.POST,"/api/messages").permitAll()
// 新增、删除、修改操作需要管理员权限
.antMatchers(HttpMethod.POST,"/api/**").hasRole("ADMIN")
.antMatchers(HttpMethod.PUT,"/api/**").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE,"/api/**").hasRole("ADMIN")
// 管理员才能访问的路径
.antMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
.antMatchers(HttpMethod.POST, "/api/articles/view/**").permitAll()
.antMatchers(HttpMethod.POST, "/api/messages").permitAll() // ← 放在通用 POST 之前
.antMatchers(HttpMethod.GET, "/api/**").permitAll()
.antMatchers(HttpMethod.POST, "/api/**").hasRole("ADMIN") // ← 通用规则放最后
.antMatchers(HttpMethod.PUT, "/api/**").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
.antMatchers("/api/admin/**").hasRole("ADMIN")
// 其他所有请求都需要认证
.anyRequest().authenticated()
.and()
// 配置会话管理,使用无状态会话策略
@@ -104,12 +86,6 @@ public class SecurityConfig {
// 确保Spring Security不会添加额外的CharacterEncodingFilter
// 因为我们在CharacterEncodingConfig中已经配置了自定义的过滤器
http.addFilterBefore((request, response, chain) -> {
// 确保响应使用UTF-8编码
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
chain.doFilter(request, response);
}, JwtAuthenticationFilter.class);
// 配置访问拒绝处理器
http.exceptionHandling()

View File

@@ -11,14 +11,18 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
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 java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
/**
* 认证控制器
* 处理用户登录相关请求
@@ -62,6 +66,7 @@ public class AuthController {
/**
* 用户登录接口
*
* @param loginRequest 登录请求参数
* @return 登录结果
*/
@@ -71,7 +76,8 @@ public class AuthController {
try {
// 创建认证令牌
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(), loginRequest.getPassword());
// 执行认证
Authentication authentication = authenticationManager.authenticate(authenticationToken);
@@ -100,6 +106,7 @@ public class AuthController {
/**
* 获取当前登录用户信息
*
* @return 当前用户信息
*/
@PostMapping("/info")
@@ -120,6 +127,7 @@ public class AuthController {
/**
* 用户登出接口
*
* @return 登出结果
*/
@PostMapping("/logout")
@@ -127,4 +135,11 @@ public class AuthController {
SecurityContextHolder.clearContext();
return ResponseMessage.successEmpty("登出成功");
}
@GetMapping("/debug")
public Map<String, String> debug(HttpServletRequest request) {
return Collections.singletonMap(
"Authorization",
request.getHeader("Authorization"));
}
}

View File

@@ -1,65 +0,0 @@
package com.qf.myafterprojecy.init;
import com.qf.myafterprojecy.pojo.Users;
import com.qf.myafterprojecy.repository.UsersRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Optional;
/**
* 用户数据初始化类
* 在应用启动时检查并创建管理员账号
*/
@Component
public class UserDataInit implements ApplicationRunner {
private static final Logger logger = LoggerFactory.getLogger(UserDataInit.class);
@Autowired
private UsersRepository usersRepository;
@Autowired
private PasswordEncoder passwordEncoder;
// 管理员账号信息
private static final String ADMIN_USERNAME = "qf1121";
private static final String ADMIN_PASSWORD = "qf1121";
private static final String ADMIN_EMAIL = "admin@qf1121.com";
private static final String ADMIN_PHONE = "13800138000";
private static final Integer ADMIN_ROLE = 1; // 1表示管理员角色
@Override
public void run(ApplicationArguments args) {
logger.info("开始检查管理员账号...");
// 检查管理员账号是否已存在
Optional<Users> adminUser = usersRepository.findByUsername(ADMIN_USERNAME);
if (adminUser.isPresent()) {
logger.info("管理员账号 {} 已存在,无需创建", ADMIN_USERNAME);
} else {
// 创建管理员账号
Users newAdmin = new Users();
newAdmin.setUsername(ADMIN_USERNAME);
// 加密密码
newAdmin.setPassword(passwordEncoder.encode(ADMIN_PASSWORD));
newAdmin.setEmail(ADMIN_EMAIL);
newAdmin.setPhone(ADMIN_PHONE);
newAdmin.setRole(ADMIN_ROLE);
newAdmin.setCreateTime(LocalDateTime.now());
try {
usersRepository.save(newAdmin);
logger.info("管理员账号 {} 创建成功", ADMIN_USERNAME);
} catch (Exception e) {
logger.error("创建管理员账号失败: {}", e.getMessage());
}
}
}
}

View File

@@ -19,6 +19,7 @@ public class Nonsense {
@Column(name = "time")
private Date time;
public Integer getId() {
return id;
}

View File

@@ -2,7 +2,6 @@ package com.qf.myafterprojecy.pojo.dto;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
public class CategoryAttributeDto {
private Integer attributeid;
@@ -12,6 +11,8 @@ public class CategoryAttributeDto {
@NotBlank(message = "属性名称不能为空")
private String attributename;
private Integer articlecount;
// Getters and Setters
public Integer getAttributeid() {
return attributeid;
@@ -36,4 +37,12 @@ public class CategoryAttributeDto {
public void setAttributename(String attributename) {
this.attributename = attributename;
}
public Integer getArticlecount() {
return articlecount;
}
public void setArticlecount(Integer articlecount) {
this.articlecount = articlecount;
}
}

View File

@@ -4,16 +4,17 @@ import java.util.List;
import com.qf.myafterprojecy.pojo.Categoryattribute;
public class CategoryTreeDto {
private Integer id;
private String name;
private List<Categoryattribute> children;
private List<CategoryAttributeDto> children;
// 构造方法
public CategoryTreeDto() {
}
// 全参构造方法
public CategoryTreeDto(Integer id, String name, List<Categoryattribute> children) {
public CategoryTreeDto(Integer id, String name, List<CategoryAttributeDto> children) {
this.id = id;
this.name = name;
this.children = children;
@@ -31,10 +32,10 @@ public class CategoryTreeDto {
public void setName(String name) {
this.name = name;
}
public List<Categoryattribute> getChildren() {
public List<CategoryAttributeDto> getChildren() {
return children;
}
public void setChildren(List<Categoryattribute> children) {
public void setChildren(List<CategoryAttributeDto> children) {
this.children = children;
}
}

View File

@@ -18,11 +18,11 @@ public interface ArticleRepository extends JpaRepository<Article, Integer> {
* 根据文章ID查询文章信息的方法
* 使用JPQLJava Persistence Query Language进行查询
*
* @param id 文章的唯一标识符,作为查询条件
* @param articleid 文章的唯一标识符,作为查询条件
* @return 返回一个Optional<Article>对象可能包含文章信息也可能为空如果未找到对应ID的文章
*/
@Query("SELECT a FROM Article a WHERE a.articleid = :id")
Optional<Article> findById(@Param("id") Integer id);
@Query("SELECT a FROM Article a WHERE a.articleid = :articleid")
Optional<Article> findById(@Param("articleid") Integer articleid);
/**
* 根据标题查询文章列表
@@ -132,4 +132,7 @@ public interface ArticleRepository extends JpaRepository<Article, Integer> {
*/
@Query("SELECT COUNT(a) FROM Article a WHERE a.status = :status")
Integer countByStatus(@Param("status") Integer status);
// 统计指定属性ID的文章数量
@Query("SELECT COUNT(a) FROM Article a WHERE a.status = :status AND a.attributeid = :attributeid")
Integer countByStatusAndAttributeid(@Param("status") Integer status, @Param("attributeid") Integer attributeid);
}

View File

@@ -2,7 +2,6 @@ package com.qf.myafterprojecy.repository;
import com.qf.myafterprojecy.pojo.Message;
import org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties.Pageable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.repository.JpaRepository;

View File

@@ -6,8 +6,6 @@ import com.qf.myafterprojecy.pojo.dto.ArriclePageDto;
import com.qf.myafterprojecy.pojo.dto.ArticleDto;
import com.qf.myafterprojecy.pojo.dto.ArticleTreeDto;
import org.springframework.data.domain.Page;
import java.util.List;
public interface IArticleService {

View File

@@ -2,8 +2,11 @@ package com.qf.myafterprojecy.service.impl;
import com.qf.myafterprojecy.exceptopn.ResponseMessage;
import com.qf.myafterprojecy.pojo.Category;
import com.qf.myafterprojecy.pojo.Categoryattribute;
import com.qf.myafterprojecy.pojo.dto.CategoryAttributeDto;
import com.qf.myafterprojecy.pojo.dto.CategoryDto;
import com.qf.myafterprojecy.pojo.dto.CategoryTreeDto;
import com.qf.myafterprojecy.repository.ArticleRepository;
import com.qf.myafterprojecy.repository.CategoryAttributeRepository;
import com.qf.myafterprojecy.repository.CategoryRepository;
import com.qf.myafterprojecy.service.ICategoryService;
@@ -30,6 +33,8 @@ public class CategoryService implements ICategoryService {
private CategoryRepository categoryRepository;
@Autowired
private CategoryAttributeRepository categoryAttributeRepository;
@Autowired
private ArticleRepository articleRepository;
@Override
@Transactional(readOnly = true)
@@ -185,8 +190,17 @@ public class CategoryService implements ICategoryService {
CategoryTreeDto node = new CategoryTreeDto();
node.setId(category.getCategoryid());
node.setName(category.getTypename());
node.setChildren(categoryAttributeRepository.findByCategoryId(category.getCategoryid()));
List<Categoryattribute> categoryAttributes = categoryAttributeRepository.findByCategoryId(category.getCategoryid());
List<CategoryAttributeDto> categoryAttributeDtos = categoryAttributes.stream()
.map(attr -> {
CategoryAttributeDto dto = new CategoryAttributeDto();
dto.setAttributeid(attr.getAttributeid());
dto.setCategoryid(attr.getCategoryid());
dto.setAttributename(attr.getAttributename());
dto.setArticlecount(articleRepository.countByStatusAndAttributeid(1, attr.getAttributeid()));
return dto;
}).collect(Collectors.toList());
node.setChildren(categoryAttributeDtos);
return node;
}
}

View File

@@ -16,7 +16,6 @@ import org.springframework.dao.DataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Comparator;
@@ -81,12 +80,12 @@ public class MessageService implements IMessageService {
}
// 业务逻辑校验
if (StringUtils.isEmpty(messageDto.getContent())) {
if ((messageDto.getContent() == null || messageDto.getContent().trim().isEmpty())) {
logger.warn("保存消息时内容为空");
throw new IllegalArgumentException("Message content cannot be empty");
}
if (StringUtils.isEmpty(messageDto.getNickname())) {
if ((messageDto.getNickname() == null || messageDto.getNickname().trim().isEmpty())) {
logger.warn("保存消息时昵称为空");
throw new IllegalArgumentException("Message nickname cannot be empty");
}
@@ -212,7 +211,7 @@ public class MessageService implements IMessageService {
@Override
public ResponseMessage<List<Message>> searchMessagesByNickname(String nickname) {
if (StringUtils.isEmpty(nickname)) {
if ((nickname == null || nickname.trim().isEmpty())) {
logger.warn("根据昵称查询消息时昵称为空");
return ResponseMessage.badRequest("昵称不能为空");
}
@@ -414,7 +413,6 @@ public class MessageService implements IMessageService {
}
return count;
}
/**
* 递归按时间排序子节点
*/

View File

@@ -11,7 +11,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties.Pageable;
import org.springframework.dao.DataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;

View File

@@ -0,0 +1,5 @@
# Include JJWT library in restart classloader to prevent ClassNotFoundException
restart.include.jjwt=/jjwt-[\w\d\.\-]+.jar
restart.include.jjwt-api=/jjwt-api-[\w\d\.\-]+.jar
restart.include.jjwt-impl=/jjwt-impl-[\w\d\.\-]+.jar
restart.include.jjwt-jackson=/jjwt-jackson-[\w\d\.\-]+.jar

View File

@@ -38,10 +38,11 @@ jwt.token-prefix=Bearer
# 安全与CORS配置 - 开发用
# ====================================================================
# CORS配置开发环境允许所有本地前端访问
cors.allowed-origins=http://localhost:3000,http://localhost:8080,http://localhost:5173
cors.allowed-origins=http://localhost:3000,http://localhost:8080,http://localhost:5173,http://localhost:5174
cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS
cors.allowed-headers=*
cors.allow-credentials=true
cors.max-age=3600
# ====================================================================
# 日志配置 - 开发用(详细日志便于调试)

View File

@@ -37,7 +37,7 @@ spring.jpa.properties.hibernate.order_updates=true
# ====================================================================
# JWT 配置 - 生产用(敏感信息从环境变量读取)
# ====================================================================
jwt.secret=${JWT_SECRET:6a1f4832-29bf-4ac5-9408-a8813b6f2dfe}
jwt.secret=${JWT_SECRET:mySecretKey123}
jwt.expiration=${JWT_EXPIRATION:3600000}
jwt.header=Authorization
jwt.token-prefix=Bearer