Update: cache module, security modules and build
This commit is contained in:
100
src/main/java/com/example/scaffold/cache/CacheConfig.java
vendored
Normal file
100
src/main/java/com/example/scaffold/cache/CacheConfig.java
vendored
Normal 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();
|
||||
};
|
||||
}
|
||||
}
|
||||
143
src/main/java/com/example/scaffold/cache/CacheService.java
vendored
Normal file
143
src/main/java/com/example/scaffold/cache/CacheService.java
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 更强"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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
|
||||
// 禁用 CSRF(JWT 不需要)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
140
src/main/java/com/example/scaffold/security/jwt/JwtUtil.java
Normal file
140
src/main/java/com/example/scaffold/security/jwt/JwtUtil.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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\": \"未登录\"}";
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user