网站搭建实操(三)后台管理-2-forum-core
forum-core核心配置模块
forum-core 是微服务的核心配置模块,提供:
-
统一配置:所有服务共享的配置(跨域、拦截器、过滤器)
-
AOP切面:日志、权限、限流等通用功能
-
安全配置:JWT过滤器、认证配置
-
异常处理:全局异常处理器
-
数据填充:MyBatis-Plus自动填充
pom文件
java
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>forum-backend</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>forum-core</artifactId>
<packaging>jar</packaging>
<description>核心配置模块 - 提供全局配置、拦截器、过滤器、AOP等核心功能</description>
<dependencies>
<!-- Common模块 -->
<dependency>
<groupId>org.example</groupId>
<artifactId>forum-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Spring Boot Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Boot Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Validation -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
</dependencies>
<build>
<finalName>forum-core</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
web MVC配置解决跨域等问题
设计原因:
-
- 统一配置跨域,解决前后端分离的跨域问题
-
- 配置拦截器执行顺序:限流 -> 认证
-
- 统一JSON序列化格式,确保前后端日期格式一致
java
package org.example.core.config;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.example.core.interceptor.AuthenticationInterceptor;
import org.example.core.interceptor.RateLimitInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
* Web MVC配置类
*
* 设计原因:
* 1. 统一配置跨域,解决前后端分离的跨域问题
* 2. 配置拦截器执行顺序:限流 -> 认证
* 3. 统一JSON序列化格式,确保前后端日期格式一致
*/
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final AuthenticationInterceptor authenticationInterceptor;
private final RateLimitInterceptor rateLimitInterceptor;
/**
* 配置跨域请求
* 为什么需要跨域?浏览器同源策略限制,前后端分离部署时域名/端口不同
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080") // 允许的源
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
/**
* 配置拦截器
* 执行顺序:限流拦截器 -> 认证拦截器
* 为什么这个顺序?先限流后认证,减少无效请求的认证开销
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 限流拦截器(最先执行)
registry.addInterceptor(rateLimitInterceptor)
.addPathPatterns("/api/**")
.order(1);
// 认证拦截器
registry.addInterceptor(authenticationInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns(
"/api/auth/login",
"/api/auth/register",
"/api/auth/captcha",
"/swagger-ui/**",
"/v3/api-docs/**",
"/doc.html"
)
.order(2);
}
/**
* 配置消息转换器
* 为什么需要自定义?统一日期格式,避免时区问题
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(0, mappingJackson2HttpMessageConverter());
}
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(objectMapper());
return converter;
}
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// 忽略未知属性,防止前端传递多余字段导致反序列化失败
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 日期格式配置
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// Java 8时间模块配置
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDateTime.class,
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
objectMapper.registerModule(javaTimeModule);
return objectMapper;
}
}
认证拦截器
- 设计原因:
-
- 在请求进入Controller前解析JWT Token
-
- 将用户信息存入请求属性,供后续使用
-
- 不在此处验证权限,只做解析,权限由Spring Security管理
java
package org.example.core.interceptor;
import com.forum.common.utils.JwtUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 认证拦截器
*
* 设计原因:
* 1. 在请求进入Controller前解析JWT Token
* 2. 将用户信息存入请求属性,供后续使用
* 3. 不在此处验证权限,只做解析,权限由Spring Security管理
*
* 为什么用拦截器而不是过滤器?
* - 拦截器可以访问Spring MVC的上下文
* - 可以更细粒度地控制哪些路径需要拦截
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthenticationInterceptor implements HandlerInterceptor {
private final JwtUtils jwtUtils;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = extractToken(request);
if (token != null && jwtUtils.validateToken(token)) {
Long userId = jwtUtils.getUserIdFromToken(token);
String username = jwtUtils.getUsernameFromToken(token);
// 存入请求属性,供Controller使用
request.setAttribute("userId", userId);
request.setAttribute("username", username);
log.debug("认证成功 - userId: {}, uri: {}", userId, request.getRequestURI());
}
return true; // 始终放行,让Spring Security做权限控制
}
private String extractToken(HttpServletRequest request) {
String header = request.getHeader(jwtUtils.getTokenHeader());
return jwtUtils.extractTokenFromHeader(header);
}
}
限流拦截器
设计原因:
-
- 防止恶意请求和DDoS攻击
-
- 保护后端服务,避免资源耗尽
-
- 使用Redis实现分布式限流
java
package org.example.core.interceptor;
import com.forum.common.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;
/**
* 限流拦截器
*
* 设计原因:
* 1. 防止恶意请求和DDoS攻击
* 2. 保护后端服务,避免资源耗尽
* 3. 使用Redis实现分布式限流
*
* 限流算法:计数器算法(滑动窗口)
* 为什么选择计数器?实现简单,性能好,适合API限流
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RateLimitInterceptor implements HandlerInterceptor{
private final RedisUtils redisUtils;
@Value("${rate-limit.enabled:true}")
private Boolean enabled;
@Value("${rate-limit.default-limit:100}")
private Long defaultLimit;
@Value("${rate-limit.default-window:60}")
private Long defaultWindow;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (!enabled) {
return true;
}
String uri = request.getRequestURI();
String key = getRateLimitKey(request, uri);
if (!checkRateLimit(key)) {
log.warn("请求被限流 - uri: {}, key: {}", uri, key);
throw new BusinessException(429, "请求过于频繁,请稍后再试");
}
return true;
}
private boolean checkRateLimit(String key) {
Long current = redisUtils.increment(key, 1);
if (current == 1) {
redisUtils.expire(key, defaultWindow, TimeUnit.SECONDS);
}
return current <= defaultLimit;
}
private String getRateLimitKey(HttpServletRequest request, String uri) {
String ip = getClientIp(request);
return "rate:limit:ip:" + ip + ":" + uri;
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddr();
}
return ip != null ? ip.split(",")[0].trim() : "unknown";
}
}
日志切面
- 设计原因:
-
- 统一记录接口调用日志,便于问题排查
-
- 记录请求参数和响应结果,追踪数据流向
-
- 记录接口执行时间,监控性能
java
package org.example.core.aop;
import com.alibaba.fastjson.JSON;
import com.forum.common.annotation.LogAnnotation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 日志切面
*
* 设计原因:
* 1. 统一记录接口调用日志,便于问题排查
* 2. 记录请求参数和响应结果,追踪数据流向
* 3. 记录接口执行时间,监控性能
*
* 为什么使用AOP?非侵入式,业务代码无需关注日志
*/
@Slf4j
@Aspect
@Component
public class LogAspect {
@Pointcut("@annotation(com.forum.common.annotation.LogAnnotation)")
public void logPointCut() {
}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes != null ? attributes.getRequest() : null;
Map<String, Object> logMap = new HashMap<>();
logMap.put("module", logAnnotation.module());
logMap.put("operation", logAnnotation.operation());
logMap.put("method", method.getName());
if (request != null) {
logMap.put("uri", request.getRequestURI());
logMap.put("ip", getClientIp(request));
}
// 记录请求参数
if (logAnnotation.needParam()) {
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0) {
String params = Arrays.stream(args)
.filter(arg -> !(arg instanceof HttpServletRequest))
.map(arg -> {
try {
return JSON.toJSONString(arg);
} catch (Exception e) {
return arg.toString();
}
})
.collect(Collectors.joining(", "));
logMap.put("params", params);
}
}
Object result = null;
try {
result = joinPoint.proceed();
logMap.put("result", logAnnotation.needResult() ? JSON.toJSONString(result) : "success");
logMap.put("status", "success");
} catch (Exception e) {
logMap.put("status", "error");
logMap.put("error", e.getMessage());
throw e;
} finally {
long costTime = System.currentTimeMillis() - startTime;
logMap.put("costTime", costTime + "ms");
// 根据日志级别记录
switch (logAnnotation.level()) {
case INFO:
log.info("接口日志: {}", JSON.toJSONString(logMap));
break;
case WARN:
log.warn("接口日志: {}", JSON.toJSONString(logMap));
break;
case ERROR:
log.error("接口日志: {}", JSON.toJSONString(logMap));
break;
default:
log.debug("接口日志: {}", JSON.toJSONString(logMap));
}
}
return result;
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddr();
}
return ip != null ? ip.split(",")[0].trim() : "";
}
}
MyBatis-Plus配置类
- 设计原因:
-
- 分页插件:支持物理分页,性能好
-
- 乐观锁插件:防止并发更新问题
-
- 防全表更新插件:防止误操作
-
- 自动填充:统一处理创建时间、更新时间
java
package org.example.core.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
/**
* MyBatis-Plus配置类
*/
@Slf4j
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件 - 支持物理分页,避免内存分页
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInterceptor.setOptimizeJoin(true);
paginationInterceptor.setMaxLimit(1000L);
interceptor.addInnerInterceptor(paginationInterceptor);
// 乐观锁插件 - 解决并发更新问题
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 防全表更新插件 - 防止误操作删除/更新所有数据
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
log.info("MyBatis-Plus拦截器配置完成");
return interceptor;
}
/**
* 自动填充处理器
* 为什么需要自动填充?统一处理创建人、创建时间、更新人、更新时间
* 避免每个业务代码都重复设置
*/
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MetaObjectHandler() {
@Override
public void insertFill(MetaObject metaObject) {
// 创建时间
this.strictInsertFill(metaObject, "createdTime", LocalDateTime.class, LocalDateTime.now());
// 更新时间
this.strictInsertFill(metaObject, "updatedTime", LocalDateTime.class, LocalDateTime.now());
// 逻辑删除默认值
this.strictInsertFill(metaObject, "isDeleted", Integer.class, 0);
}
@Override
public void updateFill(MetaObject metaObject) {
// 更新时间
this.strictUpdateFill(metaObject, "updatedTime", LocalDateTime.class, LocalDateTime.now());
}
};
}
}
JWT认证过滤器
- 设计原因:
-
- 在Spring Security过滤器链中解析JWT Token
-
- 将用户信息设置到SecurityContext
-
- 支持无状态认证,适合微服务架构
java
package org.example.core.security;
import com.forum.common.utils.JwtUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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 javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* JWT认证过滤器
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter{
private final JwtUtils jwtUtils;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String token = extractToken(request);
if (token != null && jwtUtils.validateToken(token)) {
String username = jwtUtils.getUsernameFromToken(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 加载用户详情
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 创建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置到SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (Exception e) {
log.error("JWT认证失败: {}", e.getMessage());
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String header = request.getHeader(jwtUtils.getTokenHeader());
return jwtUtils.extractTokenFromHeader(header);
}
}
Spring Security配置类
- 设计原因:
-
- 禁用Session,使用JWT无状态认证
-
- 配置放行路径(登录、注册、文档等)
-
- 添加JWT过滤器
java
package org.example.core.security;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security配置类
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 放行路径
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/swagger-ui/**").permitAll()
.antMatchers("/v3/api-docs/**").permitAll()
.antMatchers("/doc.html").permitAll()
.antMatchers("/actuator/**").permitAll()
// 其他请求需要认证
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}