Update: cache module, security modules and build

This commit is contained in:
likingcode
2026-03-18 15:18:41 +08:00
parent 4a0737ddeb
commit d257e3595d
48 changed files with 2076 additions and 822 deletions

View File

@@ -0,0 +1,100 @@
package com.example.scaffold.cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
/**
* 缓存配置 - 支持 Caffeine(本地) 和 Redis(分布式)
*
* 学习要点:
* 1. @EnableCaching - 启用 Spring Cache
* 2. @Cacheable - 缓存方法结果
* 3. @CacheEvict - 清除缓存
* 4. @CachePut - 更新缓存
*/
@Slf4j
@Configuration
@EnableCaching
public class CacheConfig {
/**
* Caffeine 本地缓存 - 默认
*/
@Bean
@Primary
@ConditionalOnProperty(name = "cache.type", havingValue = "caffeine", matchIfMissing = true)
public CacheManager caffeineCacheManager() {
log.info("🚀 启用 Caffeine 本地缓存");
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
// 初始容量
.initialCapacity(100)
// 最大容量
.maximumSize(1000)
// 写入后过期时间
.expireAfterWrite(10, TimeUnit.MINUTES)
// 记录命中率
.recordStats()
);
return cacheManager;
}
/**
* Redis 分布式缓存
*/
@Bean
@ConditionalOnProperty(name = "cache.type", havingValue = "redis")
public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
log.info("🚀 启用 Redis 分布式缓存");
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// 键序列化
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
// 值序列化
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
// 过期时间
.entryTtl(Duration.ofMinutes(10))
// 不缓存 null
.disableCachingNullValues();
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
}
/**
* 自定义缓存 Key 生成器
*/
@Bean
public KeyGenerator customKeyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getSimpleName()).append(":");
sb.append(method.getName()).append(":");
for (Object param : params) {
sb.append(param).append("-");
}
return sb.toString();
};
}
}

View File

@@ -0,0 +1,143 @@
package com.example.scaffold.cache;
import com.example.scaffold.entity.User;
import com.example.scaffold.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* 缓存服务 - 演示 Spring Cache 和 Redis 操作
*
* 学习要点:
* 1. @Cacheable - 方法结果缓存
* 2. @CachePut - 更新缓存
* 3. @CacheEvict - 清除缓存
* 4. RedisTemplate - 直接操作 Redis
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CacheService {
private final UserMapper userMapper;
private final StringRedisTemplate redisTemplate;
/**
* 根据ID查询用户 - 使用缓存
*
* @Cacheable 注解说明:
* - value: 缓存名称
* - key: 缓存键(支持 SpEL 表达式)
* - unless: 条件,为 true 时不缓存
*/
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
log.info("🔍 [Cache] 从数据库查询用户: id={}", id);
return userMapper.findById(id);
}
/**
* 更新用户 - 更新缓存
*
* @CachePut 注解说明:
* - 方法一定会执行
* - 执行后更新缓存
*/
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
log.info("💾 [Cache] 更新用户并刷新缓存: id={}", user.getId());
userMapper.update(user);
return user;
}
/**
* 删除用户 - 清除缓存
*
* @CacheEvict 注解说明:
* - 清除指定缓存
* - allEntries = true: 清除所有条目
* - beforeInvocation = true: 方法执行前清除
*/
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
log.info("🗑️ [Cache] 删除用户并清除缓存: id={}", id);
userMapper.deleteById(id);
}
/**
* 清除所有用户缓存
*/
@CacheEvict(value = "users", allEntries = true)
public void clearUserCache() {
log.info("🧹 [Cache] 清除所有用户缓存");
}
// ==================== Redis 直接操作 ====================
/**
* Redis String 操作
*/
public void setString(String key, String value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
log.info("📝 [Redis] SET {} = {} (TTL: {} {})", key, value, timeout, unit);
}
public String getString(String key) {
String value = redisTemplate.opsForValue().get(key);
log.info("📝 [Redis] GET {} = {}", key, value);
return value;
}
/**
* Redis Hash 操作
*/
public void setHash(String key, String field, String value) {
redisTemplate.opsForHash().put(key, field, value);
log.info("📝 [Redis] HSET {} {} = {}", key, field, value);
}
public String getHash(String key, String field) {
Object value = redisTemplate.opsForHash().get(key, field);
log.info("📝 [Redis] HGET {} {} = {}", key, field, value);
return value != null ? value.toString() : null;
}
/**
* Redis List 操作
*/
public void pushToList(String key, String value) {
redisTemplate.opsForList().rightPush(key, value);
log.info("📝 [Redis] RPUSH {} {}", key, value);
}
/**
* Redis Set 操作
*/
public void addToSet(String key, String value) {
redisTemplate.opsForSet().add(key, value);
log.info("📝 [Redis] SADD {} {}", key, value);
}
/**
* Redis 分布式锁
*/
public boolean tryLock(String key, String value, long timeout, TimeUnit unit) {
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(key, value, timeout, unit);
log.info("🔒 [Redis] TRY_LOCK {} = {}", key, success);
return Boolean.TRUE.equals(success);
}
public void unlock(String key) {
redisTemplate.delete(key);
log.info("🔓 [Redis] UNLOCK {}", key);
}
}

View File

@@ -0,0 +1,206 @@
package com.example.scaffold.learning;
import com.example.scaffold.cache.CacheService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 高级功能学习控制器
*
* 学习内容:
* 1. Redis 数据类型操作
* 2. 缓存策略
* 3. 分布式锁
* 4. 认证方案对比
*/
@Slf4j
@RestController
@RequestMapping("/api/learning/advanced")
@RequiredArgsConstructor
public class AdvancedLearningController {
private final StringRedisTemplate redisTemplate;
private final CacheService cacheService;
@Value("${auth.type:none}")
private String authType;
@Value("${cache.type:caffeine}")
private String cacheType;
@Value("${spring.datasource.driver-class-name}")
private String dbDriver;
/**
* 系统配置概览
*/
@GetMapping("/config")
public Map<String, Object> getConfig() {
String dbType = dbDriver.contains("h2") ? "H2" :
dbDriver.contains("mysql") ? "MySQL" :
dbDriver.contains("postgresql") ? "PostgreSQL" : "Unknown";
return Map.of(
"认证方案", Map.of(
"当前", authType,
"可选", "none | jwt | satoken",
"说明", Map.of(
"none", "无认证,开发测试用",
"jwt", "Spring Security + JWT标准方案",
"satoken", "Sa-Token轻量级方案"
)
),
"缓存方案", Map.of(
"当前", cacheType,
"可选", "caffeine | redis",
"说明", Map.of(
"caffeine", "本地缓存,单机高性能",
"redis", "分布式缓存,支持集群"
)
),
"数据库", Map.of(
"当前", dbType,
"可选", "H2 | MySQL | PostgreSQL",
"驱动", dbDriver
)
);
}
/**
* Redis 数据类型演示
*/
@GetMapping("/redis/types")
public Map<String, Object> redisTypes() {
return Map.of(
"String", "字符串,最基本的数据类型",
"Hash", "哈希,适合存储对象",
"List", "列表,支持队列/栈操作",
"Set", "集合,去重、交并差运算",
"SortedSet", "有序集合,支持排名",
"Bitmap", "位图,海量布尔值存储",
"HyperLogLog", "基数统计UV统计",
"Geo", "地理位置,附近的人",
"Stream", "流,消息队列"
);
}
/**
* Redis String 操作演示
*/
@PostMapping("/redis/string")
public Map<String, Object> redisString(@RequestParam String key,
@RequestParam String value,
@RequestParam(defaultValue = "60") long ttl) {
cacheService.setString(key, value, ttl, TimeUnit.SECONDS);
return Map.of(
"操作", "SET",
"key", key,
"value", value,
"ttl", ttl + ""
);
}
@GetMapping("/redis/string")
public Map<String, Object> getRedisString(@RequestParam String key) {
String value = cacheService.getString(key);
return Map.of(
"操作", "GET",
"key", key,
"value", value,
"存在", value != null
);
}
/**
* 缓存穿透/击穿/雪崩解决方案
*/
@GetMapping("/cache/problems")
public Map<String, Object> cacheProblems() {
return Map.of(
"缓存穿透", Map.of(
"问题", "查询不存在的数据,每次都打到数据库",
"解决", "布隆过滤器 | 缓存空值",
"代码", "@Cacheable(unless=\"#result == null\")"
),
"缓存击穿", Map.of(
"问题", "热点key过期大量请求打到数据库",
"解决", "互斥锁 | 逻辑过期",
"代码", "synchronized 或 Redis 分布式锁"
),
"缓存雪崩", Map.of(
"问题", "大量key同时过期数据库压力激增",
"解决", "随机过期时间 | 多级缓存",
"代码", "expire + random(60)"
)
);
}
/**
* 分布式锁演示
*/
@PostMapping("/redis/lock")
public Map<String, Object> distributedLock(@RequestParam String resource,
@RequestParam(defaultValue = "10") long ttl) {
String lockKey = "lock:" + resource;
String lockValue = Thread.currentThread().getName();
boolean locked = cacheService.tryLock(lockKey, lockValue, ttl, TimeUnit.SECONDS);
if (locked) {
try {
// 模拟业务操作
Thread.sleep(1000);
return Map.of(
"success", true,
"message", "获取锁成功,执行业务操作",
"resource", resource,
"lockKey", lockKey
);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return Map.of("success", false, "error", "业务中断");
} finally {
cacheService.unlock(lockKey);
}
} else {
return Map.of(
"success", false,
"message", "获取锁失败,资源被占用",
"resource", resource
);
}
}
/**
* JWT vs Sa-Token 对比
*/
@GetMapping("/auth/compare")
public Map<String, Object> authCompare() {
return Map.of(
"Spring Security + JWT", Map.of(
"优点", "标准方案、生态完善、社区活跃",
"缺点", "配置复杂、学习曲线陡、代码量大",
"适用", "大型项目、企业级应用",
"复杂度", "⭐⭐⭐⭐⭐"
),
"Sa-Token", Map.of(
"优点", "轻量级、API简洁、功能丰富、文档友好",
"缺点", "相对较新、生态不如Spring Security",
"适用", "中小型项目、快速开发",
"复杂度", "⭐⭐"
),
"选择建议", Map.of(
"快速开发", "Sa-Token",
"企业级", "Spring Security",
"学习成本", "Sa-Token 更低",
"扩展性", "Spring Security 更强"
)
);
}
}

View File

@@ -0,0 +1,97 @@
package com.example.scaffold.security.jwt;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* JWT 认证过滤器
*
* 学习要点:
* 1. OncePerRequestFilter - 确保每个请求只过滤一次
* 2. SecurityContextHolder - Spring Security 上下文
* 3. 认证流程:提取 Token → 验证 → 设置上下文
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
// 1. 从请求中提取 JWT Token
String jwt = getJwtFromRequest(request);
// 2. 验证 Token
if (StringUtils.hasText(jwt) && jwtUtil.validateToken(jwt)) {
// 3. 从 Token 中获取用户信息
String username = jwtUtil.getUsernameFromToken(jwt);
Long userId = jwtUtil.getUserIdFromToken(jwt);
// 4. 获取角色这里简化处理实际应从数据库或Token中解析
List<SimpleGrantedAuthority> authorities = Arrays.asList(
new SimpleGrantedAuthority("ROLE_USER")
);
// 5. 创建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
new JwtUserDetails(userId, username),
null,
authorities
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 6. 设置 Security 上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("JWT 认证成功: userId={}, username={}", userId, username);
}
} catch (Exception e) {
log.error("JWT 认证失败: {}", e.getMessage());
SecurityContextHolder.clearContext();
}
// 继续过滤器链
filterChain.doFilter(request, response);
}
/**
* 从请求头中提取 JWT Token
*
* 支持格式:
* Authorization: Bearer <token>
*/
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
/**
* JWT 用户信息
*/
public record JwtUserDetails(Long userId, String username) {}
}

View File

@@ -0,0 +1,103 @@
package com.example.scaffold.security.jwt;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.User;
import org.springframework.security.core.userdetails.UserDetails;
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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* JWT Security 配置
*
* 学习要点:
* 1. @ConditionalOnProperty - 条件化配置
* 2. SecurityFilterChain - 安全过滤器链
* 3. 无状态会话Stateless Session
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@ConditionalOnProperty(name = "auth.type", havingValue = "jwt")
public class JwtSecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRFJWT 不需要)
.csrf(AbstractHttpConfigurer::disable)
// 配置无状态会话
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 配置请求授权
.authorizeHttpRequests(auth -> auth
// 公开端点
.requestMatchers("/", "/index.html", "/ioc.html", "/aop.html",
"/mybatis.html", "/transaction.html",
"/css/**", "/js/**", "/favicon.ico").permitAll()
.requestMatchers("/api/auth/**", "/h2-console/**").permitAll()
.requestMatchers("/api/learning/**").permitAll()
// 其他需要认证
.anyRequest().authenticated()
)
// 添加 JWT 过滤器
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// H2 Console 配置
.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()));
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 内存用户(演示用,实际应从数据库加载)
*/
@Bean
public UserDetailsService userDetailsService() {
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder().encode("admin123"))
.roles("ADMIN", "USER")
.build();
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder().encode("user123"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService());
provider.setPasswordEncoder(passwordEncoder());
return new ProviderManager(provider);
}
}

View File

@@ -0,0 +1,140 @@
package com.example.scaffold.security.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
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 工具类
*
* 学习要点:
* 1. JWT 结构Header.Payload.Signature
* 2. 签名算法HS256、HS512
* 3. Token 过期机制
* 4. 刷新 Token 策略
*/
@Slf4j
@Component
public class JwtUtil {
@Value("${auth.jwt.secret:your-secret-key-here-must-be-at-least-256-bits}")
private String secret;
@Value("${auth.jwt.expiration:86400000}")
private Long expiration;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
/**
* 生成 JWT Token
*
* @param userId 用户ID
* @param username 用户名
* @param roles 角色列表
* @return JWT Token
*/
public String generateToken(Long userId, String username, String... roles) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
claims.put("roles", roles);
return Jwts.builder()
.claims(claims)
.subject(username)
.issuedAt(now)
.expiration(expiryDate)
.signWith(getSigningKey(), Jwts.SIG.HS256)
.compact();
}
/**
* 从 Token 中获取用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
return claims != null ? claims.getSubject() : null;
}
/**
* 从 Token 中获取用户ID
*/
public Long getUserIdFromToken(String token) {
Claims claims = parseToken(token);
return claims != null ? claims.get("userId", Long.class) : null;
}
/**
* 验证 Token 是否有效
*/
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (ExpiredJwtException e) {
log.warn("JWT Token 已过期: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
log.warn("不支持的 JWT Token: {}", e.getMessage());
} catch (MalformedJwtException e) {
log.warn("无效的 JWT Token: {}", e.getMessage());
} catch (SecurityException e) {
log.warn("JWT 签名验证失败: {}", e.getMessage());
} catch (IllegalArgumentException e) {
log.warn("JWT Token 为空或非法: {}", e.getMessage());
}
return false;
}
/**
* 解析 Token
*/
private Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 检查 Token 是否即将过期(用于刷新)
*
* @param token JWT Token
* @param threshold 阈值(毫秒)
* @return 是否即将过期
*/
public boolean isTokenExpiredSoon(String token, long threshold) {
try {
Claims claims = parseToken(token);
Date expiration = claims.getExpiration();
return expiration.getTime() - System.currentTimeMillis() < threshold;
} catch (Exception e) {
return true;
}
}
/**
* 刷新 Token
*/
public String refreshToken(String token) {
Claims claims = parseToken(token);
Long userId = claims.get("userId", Long.class);
String username = claims.getSubject();
String roles = claims.get("roles", String.class);
return generateToken(userId, username, roles);
}
}

View File

@@ -0,0 +1,71 @@
package com.example.scaffold.security.satoken;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Sa-Token 配置
*
* 学习要点:
* 1. Sa-Token 相比 Spring Security 更轻量
* 2. 支持分布式 Session
* 3. 支持登录设备控制
* 4. 支持权限认证、角色认证
*/
@Slf4j
@Configuration
@ConditionalOnProperty(name = "auth.type", havingValue = "satoken")
public class SaTokenConfig implements WebMvcConfigurer {
/**
* 注册 Sa-Token 拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handle -> {
// 登录校验
StpUtil.checkLogin();
}))
.addPathPatterns("/api/**")
.excludePathPatterns(
"/api/auth/**",
"/api/learning/**",
"/api/users/count"
);
}
/**
* Sa-Token 全局过滤器
*/
@Bean
public SaServletFilter saServletFilter() {
return new SaServletFilter()
// 拦截路径
.addInclude("/**")
// 排除路径
.addExclude("/", "/index.html", "/ioc.html", "/aop.html",
"/mybatis.html", "/transaction.html", "/users.html", "/api.html",
"/css/**", "/js/**", "/favicon.ico",
"/h2-console/**")
// 认证函数
.setAuth(obj -> {
SaRouter.match("/api/**")
.notMatch("/api/auth/**", "/api/learning/**")
.check(r -> StpUtil.checkLogin());
})
// 异常处理
.setError(e -> {
log.error("Sa-Token 认证失败: {}", e.getMessage());
return "{\"code\": 401, \"message\": \"未登录\"}";
});
}
}

View File

@@ -242,6 +242,22 @@
<button class="btn btn-primary" onclick="testApi('GET', '/api/learning/transaction/propagation', null, 'learnResult4')">测试</button>
<div id="learnResult4" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>反射 - 类信息</strong>
<div class="url">/api/learning/reflection/class-info</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/learning/reflection/class-info', null, 'learnResult5')">测试</button>
<div id="learnResult5" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>高级 - 性能统计</strong>
<div class="url">/api/learning/advanced/performance</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/learning/advanced/performance', null, 'learnResult6')">测试</button>
<div id="learnResult6" class="result"></div>
</div>
</div>
</div>
</div>