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:
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,10 +26,9 @@ public class CorsConfig {
|
|||||||
@Value("${cors.allow-credentials}")
|
@Value("${cors.allow-credentials}")
|
||||||
private Boolean allowCredentials;
|
private Boolean allowCredentials;
|
||||||
|
|
||||||
@Value("${cors.max-age:3600}")
|
@Value("${cors.max-age}")
|
||||||
private Long maxAge;
|
private Long maxAge;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建CORS过滤器,配置跨域请求的规则
|
* 创建CORS过滤器,配置跨域请求的规则
|
||||||
* 从配置文件中读取CORS配置,实现配置的统一管理
|
* 从配置文件中读取CORS配置,实现配置的统一管理
|
||||||
@@ -64,7 +63,8 @@ public class CorsConfig {
|
|||||||
config.addExposedHeader("Accept");
|
config.addExposedHeader("Accept");
|
||||||
config.addExposedHeader("Access-Control-Allow-Origin");
|
config.addExposedHeader("Access-Control-Allow-Origin");
|
||||||
config.addExposedHeader("Access-Control-Allow-Credentials");
|
config.addExposedHeader("Access-Control-Allow-Credentials");
|
||||||
|
config.addExposedHeader("X-Total-Count"); // 分页常用
|
||||||
|
config.addExposedHeader("Link"); // HATEOAS 常用
|
||||||
// 设置预检请求的有效期(秒),从配置文件读取
|
// 设置预检请求的有效期(秒),从配置文件读取
|
||||||
config.setMaxAge(maxAge);
|
config.setMaxAge(maxAge);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package com.qf.myafterprojecy.config;
|
package com.qf.myafterprojecy.config;
|
||||||
|
|
||||||
import com.qf.myafterprojecy.utils.JwtUtils;
|
import com.qf.myafterprojecy.utils.JwtUtils;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
@@ -40,19 +42,28 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
private String tokenPrefix;
|
private String tokenPrefix;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
|
||||||
try {
|
@NonNull FilterChain filterChain) throws ServletException, IOException {
|
||||||
// 获取token
|
// 仅处理需要认证的请求,token为空时允许继续执行过滤器链
|
||||||
String token = getTokenFromRequest(request);
|
String token = getTokenFromRequest(request);
|
||||||
// System.out.println(token);
|
|
||||||
if (token != null) {
|
// 如果没有token,继续执行过滤器链,让后续的过滤器或控制器来处理是否需要认证
|
||||||
if (validateToken(token)) {
|
if (token == null) {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
// 从token中获取用户名
|
// 从token中获取用户名
|
||||||
String username = jwtUtils.getUsernameFromToken(token);
|
String username = jwtUtils.getUsernameFromToken(token);
|
||||||
// System.out.println("username: " + username);
|
|
||||||
|
// 如果用户名不为空且当前上下文没有认证信息,则进行认证
|
||||||
|
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||||
// 加载用户信息
|
// 加载用户信息
|
||||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||||
|
|
||||||
|
// 验证token
|
||||||
|
if (jwtUtils.validateToken(token, userDetails)) {
|
||||||
// 创建认证对象
|
// 创建认证对象
|
||||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
||||||
userDetails, null, userDetails.getAuthorities());
|
userDetails, null, userDetails.getAuthorities());
|
||||||
@@ -60,35 +71,18 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
// 设置认证信息到上下文
|
// 设置认证信息到上下文
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
} else {
|
logger.debug("用户 {} 认证成功", username);
|
||||||
// 如果token无效但不为空,返回401
|
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
response.setContentType("application/json");
|
|
||||||
response.getWriter().write("{\"message\":\"无效的认证令牌\"}");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (io.jsonwebtoken.ExpiredJwtException e) {
|
} catch (io.jsonwebtoken.ExpiredJwtException e) {
|
||||||
// 专门处理令牌过期,返回401和明确的错误信息
|
sendError(response, "令牌已过期", HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
logger.error("令牌已过期: {}", e.getMessage());
|
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
response.setContentType("application/json");
|
|
||||||
response.getWriter().write("{\"message\":\"认证令牌已过期\"}");
|
|
||||||
return;
|
return;
|
||||||
} catch (io.jsonwebtoken.JwtException e) {
|
} catch (io.jsonwebtoken.JwtException | IllegalArgumentException e) {
|
||||||
// 其他JWT异常
|
sendError(response, "无效的令牌", HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
logger.error("无效的token格式: {}", e.getMessage());
|
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
response.setContentType("application/json");
|
|
||||||
response.getWriter().write("{\"message\":\"无效的令牌格式\"}");
|
|
||||||
return;
|
return;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("无法设置用户认证: {}", e);
|
logger.error("JWT 认证未知错误", e);
|
||||||
SecurityContextHolder.clearContext();
|
sendError(response, "认证服务异常", HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
response.setContentType("application/json");
|
|
||||||
response.getWriter().write("{\"message\":\"认证失败\"}");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,32 +95,22 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
private String getTokenFromRequest(HttpServletRequest request) {
|
private String getTokenFromRequest(HttpServletRequest request) {
|
||||||
String bearerToken = request.getHeader(tokenHeader);
|
String bearerToken = request.getHeader(tokenHeader);
|
||||||
if (bearerToken != null && bearerToken.startsWith(tokenPrefix + " ")) {
|
if (bearerToken != null && bearerToken.startsWith(tokenPrefix + " ")) {
|
||||||
return bearerToken.substring(tokenPrefix.length() + 1);
|
return bearerToken.substring(tokenPrefix.length() + 1).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证token
|
* 发送未授权响应
|
||||||
*/
|
*/
|
||||||
private boolean validateToken(String token) {
|
|
||||||
try {
|
private void sendError(HttpServletResponse response, String message, int status) throws IOException {
|
||||||
String username = jwtUtils.getUsernameFromToken(token);
|
response.setStatus(status);
|
||||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
return jwtUtils.validateToken(token, userDetails);
|
response.setHeader("Pragma", "no-cache");
|
||||||
} catch (io.jsonwebtoken.ExpiredJwtException e) {
|
response.setDateHeader("Expires", 0);
|
||||||
// 专门处理令牌过期,记录日志但不抛出异常
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
logger.error("令牌已过期: {}", e.getMessage());
|
response.getWriter().write(String.format("{\"message\":\"%s\"}", message));
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
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.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
@@ -27,12 +25,6 @@ import javax.servlet.http.HttpServletResponse;
|
|||||||
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
|
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserDetailsService userDetailsService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private PasswordEncoder passwordEncoder;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||||
|
|
||||||
@@ -59,24 +51,14 @@ public class SecurityConfig {
|
|||||||
// 使用自定义的CORS过滤器,不使用默认的cors()配置
|
// 使用自定义的CORS过滤器,不使用默认的cors()配置
|
||||||
// 配置URL访问权限
|
// 配置URL访问权限
|
||||||
.authorizeRequests()
|
.authorizeRequests()
|
||||||
// 允许公开访问的路径
|
|
||||||
// 登录和认证相关端点应该全部公开
|
|
||||||
|
|
||||||
|
|
||||||
.antMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
|
.antMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
|
||||||
// 文章浏览量增加接口公开
|
|
||||||
.antMatchers(HttpMethod.POST, "/api/articles/view/**").permitAll()
|
.antMatchers(HttpMethod.POST, "/api/articles/view/**").permitAll()
|
||||||
// 所有GET请求公开
|
.antMatchers(HttpMethod.POST, "/api/messages").permitAll() // ← 放在通用 POST 之前
|
||||||
.antMatchers(HttpMethod.GET, "/api/**").permitAll()
|
.antMatchers(HttpMethod.GET, "/api/**").permitAll()
|
||||||
// 公开评论新增接口
|
.antMatchers(HttpMethod.POST, "/api/**").hasRole("ADMIN") // ← 通用规则放最后
|
||||||
.antMatchers(HttpMethod.POST,"/api/messages").permitAll()
|
|
||||||
// 新增、删除、修改操作需要管理员权限
|
|
||||||
.antMatchers(HttpMethod.POST,"/api/**").hasRole("ADMIN")
|
|
||||||
.antMatchers(HttpMethod.PUT, "/api/**").hasRole("ADMIN")
|
.antMatchers(HttpMethod.PUT, "/api/**").hasRole("ADMIN")
|
||||||
.antMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
|
.antMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
|
||||||
// 管理员才能访问的路径
|
|
||||||
.antMatchers("/api/admin/**").hasRole("ADMIN")
|
.antMatchers("/api/admin/**").hasRole("ADMIN")
|
||||||
// 其他所有请求都需要认证
|
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
.and()
|
.and()
|
||||||
// 配置会话管理,使用无状态会话策略
|
// 配置会话管理,使用无状态会话策略
|
||||||
@@ -104,12 +86,6 @@ public class SecurityConfig {
|
|||||||
|
|
||||||
// 确保Spring Security不会添加额外的CharacterEncodingFilter
|
// 确保Spring Security不会添加额外的CharacterEncodingFilter
|
||||||
// 因为我们在CharacterEncodingConfig中已经配置了自定义的过滤器
|
// 因为我们在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()
|
http.exceptionHandling()
|
||||||
|
|||||||
@@ -11,14 +11,18 @@ import org.springframework.security.core.Authentication;
|
|||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证控制器
|
* 认证控制器
|
||||||
* 处理用户登录相关请求
|
* 处理用户登录相关请求
|
||||||
@@ -62,6 +66,7 @@ public class AuthController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户登录接口
|
* 用户登录接口
|
||||||
|
*
|
||||||
* @param loginRequest 登录请求参数
|
* @param loginRequest 登录请求参数
|
||||||
* @return 登录结果
|
* @return 登录结果
|
||||||
*/
|
*/
|
||||||
@@ -71,7 +76,8 @@ public class AuthController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建认证令牌
|
// 创建认证令牌
|
||||||
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
|
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
|
||||||
|
loginRequest.getUsername(), loginRequest.getPassword());
|
||||||
|
|
||||||
// 执行认证
|
// 执行认证
|
||||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||||
@@ -100,6 +106,7 @@ public class AuthController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前登录用户信息
|
* 获取当前登录用户信息
|
||||||
|
*
|
||||||
* @return 当前用户信息
|
* @return 当前用户信息
|
||||||
*/
|
*/
|
||||||
@PostMapping("/info")
|
@PostMapping("/info")
|
||||||
@@ -120,6 +127,7 @@ public class AuthController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户登出接口
|
* 用户登出接口
|
||||||
|
*
|
||||||
* @return 登出结果
|
* @return 登出结果
|
||||||
*/
|
*/
|
||||||
@PostMapping("/logout")
|
@PostMapping("/logout")
|
||||||
@@ -127,4 +135,11 @@ public class AuthController {
|
|||||||
SecurityContextHolder.clearContext();
|
SecurityContextHolder.clearContext();
|
||||||
return ResponseMessage.successEmpty("登出成功");
|
return ResponseMessage.successEmpty("登出成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/debug")
|
||||||
|
public Map<String, String> debug(HttpServletRequest request) {
|
||||||
|
return Collections.singletonMap(
|
||||||
|
"Authorization",
|
||||||
|
request.getHeader("Authorization"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,6 +19,7 @@ public class Nonsense {
|
|||||||
|
|
||||||
@Column(name = "time")
|
@Column(name = "time")
|
||||||
private Date time;
|
private Date time;
|
||||||
|
|
||||||
public Integer getId() {
|
public Integer getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.qf.myafterprojecy.pojo.dto;
|
|||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
public class CategoryAttributeDto {
|
public class CategoryAttributeDto {
|
||||||
private Integer attributeid;
|
private Integer attributeid;
|
||||||
|
|
||||||
@@ -12,6 +11,8 @@ public class CategoryAttributeDto {
|
|||||||
@NotBlank(message = "属性名称不能为空")
|
@NotBlank(message = "属性名称不能为空")
|
||||||
private String attributename;
|
private String attributename;
|
||||||
|
|
||||||
|
private Integer articlecount;
|
||||||
|
|
||||||
// Getters and Setters
|
// Getters and Setters
|
||||||
public Integer getAttributeid() {
|
public Integer getAttributeid() {
|
||||||
return attributeid;
|
return attributeid;
|
||||||
@@ -36,4 +37,12 @@ public class CategoryAttributeDto {
|
|||||||
public void setAttributename(String attributename) {
|
public void setAttributename(String attributename) {
|
||||||
this.attributename = attributename;
|
this.attributename = attributename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getArticlecount() {
|
||||||
|
return articlecount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setArticlecount(Integer articlecount) {
|
||||||
|
this.articlecount = articlecount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,16 +4,17 @@ import java.util.List;
|
|||||||
|
|
||||||
import com.qf.myafterprojecy.pojo.Categoryattribute;
|
import com.qf.myafterprojecy.pojo.Categoryattribute;
|
||||||
|
|
||||||
|
|
||||||
public class CategoryTreeDto {
|
public class CategoryTreeDto {
|
||||||
private Integer id;
|
private Integer id;
|
||||||
private String name;
|
private String name;
|
||||||
private List<Categoryattribute> children;
|
private List<CategoryAttributeDto> children;
|
||||||
|
|
||||||
// 构造方法
|
// 构造方法
|
||||||
public CategoryTreeDto() {
|
public CategoryTreeDto() {
|
||||||
}
|
}
|
||||||
// 全参构造方法
|
// 全参构造方法
|
||||||
public CategoryTreeDto(Integer id, String name, List<Categoryattribute> children) {
|
public CategoryTreeDto(Integer id, String name, List<CategoryAttributeDto> children) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.children = children;
|
this.children = children;
|
||||||
@@ -31,10 +32,10 @@ public class CategoryTreeDto {
|
|||||||
public void setName(String name) {
|
public void setName(String name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
public List<Categoryattribute> getChildren() {
|
public List<CategoryAttributeDto> getChildren() {
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
public void setChildren(List<Categoryattribute> children) {
|
public void setChildren(List<CategoryAttributeDto> children) {
|
||||||
this.children = children;
|
this.children = children;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,11 +18,11 @@ public interface ArticleRepository extends JpaRepository<Article, Integer> {
|
|||||||
* 根据文章ID查询文章信息的方法
|
* 根据文章ID查询文章信息的方法
|
||||||
* 使用JPQL(Java Persistence Query Language)进行查询
|
* 使用JPQL(Java Persistence Query Language)进行查询
|
||||||
*
|
*
|
||||||
* @param id 文章的唯一标识符,作为查询条件
|
* @param articleid 文章的唯一标识符,作为查询条件
|
||||||
* @return 返回一个Optional<Article>对象,可能包含文章信息,也可能为空(如果未找到对应ID的文章)
|
* @return 返回一个Optional<Article>对象,可能包含文章信息,也可能为空(如果未找到对应ID的文章)
|
||||||
*/
|
*/
|
||||||
@Query("SELECT a FROM Article a WHERE a.articleid = :id")
|
@Query("SELECT a FROM Article a WHERE a.articleid = :articleid")
|
||||||
Optional<Article> findById(@Param("id") Integer id);
|
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")
|
@Query("SELECT COUNT(a) FROM Article a WHERE a.status = :status")
|
||||||
Integer countByStatus(@Param("status") Integer 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.qf.myafterprojecy.repository;
|
|||||||
|
|
||||||
import com.qf.myafterprojecy.pojo.Message;
|
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.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import com.qf.myafterprojecy.pojo.dto.ArriclePageDto;
|
|||||||
import com.qf.myafterprojecy.pojo.dto.ArticleDto;
|
import com.qf.myafterprojecy.pojo.dto.ArticleDto;
|
||||||
import com.qf.myafterprojecy.pojo.dto.ArticleTreeDto;
|
import com.qf.myafterprojecy.pojo.dto.ArticleTreeDto;
|
||||||
|
|
||||||
import org.springframework.data.domain.Page;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface IArticleService {
|
public interface IArticleService {
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ package com.qf.myafterprojecy.service.impl;
|
|||||||
|
|
||||||
import com.qf.myafterprojecy.exceptopn.ResponseMessage;
|
import com.qf.myafterprojecy.exceptopn.ResponseMessage;
|
||||||
import com.qf.myafterprojecy.pojo.Category;
|
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.CategoryDto;
|
||||||
import com.qf.myafterprojecy.pojo.dto.CategoryTreeDto;
|
import com.qf.myafterprojecy.pojo.dto.CategoryTreeDto;
|
||||||
|
import com.qf.myafterprojecy.repository.ArticleRepository;
|
||||||
import com.qf.myafterprojecy.repository.CategoryAttributeRepository;
|
import com.qf.myafterprojecy.repository.CategoryAttributeRepository;
|
||||||
import com.qf.myafterprojecy.repository.CategoryRepository;
|
import com.qf.myafterprojecy.repository.CategoryRepository;
|
||||||
import com.qf.myafterprojecy.service.ICategoryService;
|
import com.qf.myafterprojecy.service.ICategoryService;
|
||||||
@@ -30,6 +33,8 @@ public class CategoryService implements ICategoryService {
|
|||||||
private CategoryRepository categoryRepository;
|
private CategoryRepository categoryRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
private CategoryAttributeRepository categoryAttributeRepository;
|
private CategoryAttributeRepository categoryAttributeRepository;
|
||||||
|
@Autowired
|
||||||
|
private ArticleRepository articleRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@@ -185,8 +190,17 @@ public class CategoryService implements ICategoryService {
|
|||||||
CategoryTreeDto node = new CategoryTreeDto();
|
CategoryTreeDto node = new CategoryTreeDto();
|
||||||
node.setId(category.getCategoryid());
|
node.setId(category.getCategoryid());
|
||||||
node.setName(category.getTypename());
|
node.setName(category.getTypename());
|
||||||
|
List<Categoryattribute> categoryAttributes = categoryAttributeRepository.findByCategoryId(category.getCategoryid());
|
||||||
node.setChildren(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;
|
return node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,6 @@ import org.springframework.dao.DataAccessException;
|
|||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
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("保存消息时内容为空");
|
logger.warn("保存消息时内容为空");
|
||||||
throw new IllegalArgumentException("Message content cannot be empty");
|
throw new IllegalArgumentException("Message content cannot be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isEmpty(messageDto.getNickname())) {
|
if ((messageDto.getNickname() == null || messageDto.getNickname().trim().isEmpty())) {
|
||||||
logger.warn("保存消息时昵称为空");
|
logger.warn("保存消息时昵称为空");
|
||||||
throw new IllegalArgumentException("Message nickname cannot be empty");
|
throw new IllegalArgumentException("Message nickname cannot be empty");
|
||||||
}
|
}
|
||||||
@@ -212,7 +211,7 @@ public class MessageService implements IMessageService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResponseMessage<List<Message>> searchMessagesByNickname(String nickname) {
|
public ResponseMessage<List<Message>> searchMessagesByNickname(String nickname) {
|
||||||
if (StringUtils.isEmpty(nickname)) {
|
if ((nickname == null || nickname.trim().isEmpty())) {
|
||||||
logger.warn("根据昵称查询消息时昵称为空");
|
logger.warn("根据昵称查询消息时昵称为空");
|
||||||
return ResponseMessage.badRequest("昵称不能为空");
|
return ResponseMessage.badRequest("昵称不能为空");
|
||||||
}
|
}
|
||||||
@@ -414,7 +413,6 @@ public class MessageService implements IMessageService {
|
|||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 递归按时间排序子节点
|
* 递归按时间排序子节点
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties.Pageable;
|
|
||||||
import org.springframework.dao.DataAccessException;
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
|||||||
5
src/main/resources/META-INF/spring-devtools.properties
Normal file
5
src/main/resources/META-INF/spring-devtools.properties
Normal 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
|
||||||
@@ -38,10 +38,11 @@ jwt.token-prefix=Bearer
|
|||||||
# 安全与CORS配置 - 开发用
|
# 安全与CORS配置 - 开发用
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# 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-methods=GET,POST,PUT,DELETE,OPTIONS
|
||||||
cors.allowed-headers=*
|
cors.allowed-headers=*
|
||||||
cors.allow-credentials=true
|
cors.allow-credentials=true
|
||||||
|
cors.max-age=3600
|
||||||
|
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# 日志配置 - 开发用(详细日志便于调试)
|
# 日志配置 - 开发用(详细日志便于调试)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ spring.jpa.properties.hibernate.order_updates=true
|
|||||||
# ====================================================================
|
# ====================================================================
|
||||||
# JWT 配置 - 生产用(敏感信息从环境变量读取)
|
# JWT 配置 - 生产用(敏感信息从环境变量读取)
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
jwt.secret=${JWT_SECRET:6a1f4832-29bf-4ac5-9408-a8813b6f2dfe}
|
jwt.secret=${JWT_SECRET:mySecretKey123}
|
||||||
jwt.expiration=${JWT_EXPIRATION:3600000}
|
jwt.expiration=${JWT_EXPIRATION:3600000}
|
||||||
jwt.header=Authorization
|
jwt.header=Authorization
|
||||||
jwt.token-prefix=Bearer
|
jwt.token-prefix=Bearer
|
||||||
|
|||||||
Reference in New Issue
Block a user