Springboot3.3.13+jdk17+SpringSecurity+Redis实现了一个简单的全局鉴权认证系统,其中 token 存储在 Redis 中并设置过期时间。这里贴出来供大家参考。
首先需要添加必要的依赖,然后实现核心配置和功能类:
1. Maven 依赖配置
xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.13</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>security-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>security-demo</name>
<description>Demo project for Spring Boot Security with Redis</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Boot Starter Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Jackson 用于JSON处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- JWT 工具 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. Redis 配置
java
@Configuration
public class RedisConfig {
@Value("${spring.redis.mode}")
private String redisMode;
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.database}")
private int database;
@Value("${spring.redis.sentinel.master}")
private String sentinelMaster;
@Value("${spring.redis.sentinel.nodes}")
private String sentinelNodes;
@Value("${spring.redis.cluster.nodes}")
private String clusterNodes;
@Value("${spring.redis.cluster.max-redirects}")
private int clusterMaxRedirects;
/**
* 创建 Redis 连接工厂
*/
@Bean
public RedisConnectionFactory redisConnectionFactory() {
switch (redisMode) {
case "standalone":
return createStandaloneConnectionFactory();
case "sentinel":
return createSentinelConnectionFactory();
case "cluster":
return createClusterConnectionFactory();
default:
throw new IllegalArgumentException("Invalid Redis mode configuration: " + redisMode);
}
}
/**
* 创建单机模式连接工厂
*/
private RedisConnectionFactory createStandaloneConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(host);
config.setPort(port);
config.setPassword(password);
config.setDatabase(database);
return new LettuceConnectionFactory(config);
}
/**
* 创建哨兵模式连接工厂
*/
private RedisConnectionFactory createSentinelConnectionFactory() {
RedisSentinelConfiguration config = new RedisSentinelConfiguration()
.master(sentinelMaster);
// 配置哨兵节点
for (String node : sentinelNodes.split(",")) {
String[] parts = node.split(":");
config.sentinel(parts[0], Integer.parseInt(parts[1]));
}
config.setPassword(password);
config.setDatabase(database);
return new LettuceConnectionFactory(config);
}
/**
* 创建集群模式连接工厂
*/
private RedisConnectionFactory createClusterConnectionFactory() {
RedisClusterConfiguration config = new RedisClusterConfiguration();
// 配置集群节点
for (String node : clusterNodes.split(",")) {
String[] parts = node.split(":");
config.clusterNode(parts[0], Integer.parseInt(parts[1]));
}
config.setMaxRedirects(clusterMaxRedirects);
config.setPassword(password);
return new LettuceConnectionFactory(config);
}
/**
* 创建 RedisTemplate
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// 使用 Jackson2JsonRedisSerializer来序列化对象为JSON 序列化和反序列化 value
Jackson2JsonRedisSerializer<Object> jacksonSerializer = getJacksonSerializer();
template.setValueSerializer(jacksonSerializer);
template.setHashValueSerializer(jacksonSerializer);
// 初始化 RedisTemplate
template.afterPropertiesSet();
return template;
}
/**
* 创建并配置 Jackson2JsonRedisSerializer
*/
private Jackson2JsonRedisSerializer<Object> getJacksonSerializer() {
// 配置 ObjectMapper
ObjectMapper objectMapper = new ObjectMapper();
// 设置序列化的可见性:任何字段(包括私有字段)都序列化
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 启用默认的类型信息,以便反序列化时能正确识别类型
objectMapper.activateDefaultTyping(
// 使用宽松的类型验证器
LaissezFaireSubTypeValidator.instance,
// 对所有非final类型存储类型信息
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
// 通过构造函数创建 Jackson2JsonRedisSerializer
return new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
}
}
3. JWT 工具类
java
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
/**
* 生成签名使用的密钥
*/
private Key getSigningKey() {
byte[] keyBytes = secret.getBytes();
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* 从token中获取用户名
*/
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
/**
* 从token中获取过期时间
*/
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
/**
* 从token中获取声明
*/
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
/**
* 从token中获取所有声明
*/
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 检查token是否过期
*/
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
/**
* 生成token
*/
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username);
}
/**
* 创建token
*/
private String createToken(Map<String, Object> claims, String subject) {
return Jwts
.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
/**
* 验证token
*/
public Boolean validateToken(String token, String username) {
final String extractedUsername = extractUsername(token);
return (extractedUsername.equals(username) && !isTokenExpired(token));
}
}
4. 自定义用户详情服务
java
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Value("${security.password}")
private String securityPassword;
@Value("${security.username}")
private String securityUsername;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (securityUsername.equals(username)) {
String encodedPassword = encryptPassword(securityPassword);
return User.withUsername(securityUsername)
.password(encodedPassword)
.roles("ADMIN")
.build();
} else {
throw new UsernameNotFoundException("User not found with username: " + username);
}
}
/**
* 生成BCryptPasswordEncoder密码
*
* @param password 密码
* @return 加密字符串
*/
public static String encryptPassword(String password) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder.encode(password);
}
}
5. Token 存储服务
java
@Service
public class TokenService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* Token在Redis中的过期时间(毫秒),应与JWT过期时间保持一致
*/
@Value("${jwt.expiration}")
private Long TOKEN_EXPIRE_SECONDS;
/**
* 存储token到Redis
*/
public void saveToken(String username, String token) {
String key = "token:" + username;
stringRedisTemplate.opsForValue().set(key, token, TOKEN_EXPIRE_SECONDS, TimeUnit.MICROSECONDS);
}
/**
* 从Redis中获取token
*/
public String getToken(String username) {
String key = "token:" + username;
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 从Redis中删除token
*/
public void deleteToken(String username) {
String key = "token:" + username;
stringRedisTemplate.delete(key);
}
/**
* 验证token是否有效
*/
public boolean validateToken(String username, String token) {
String storedToken = getToken(username);
return storedToken != null && storedToken.equals(token);
}
}
6. JWT 认证过滤器
java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final CustomUserDetailsService userDetailsService;
private final TokenService tokenService;
public JwtAuthenticationFilter(JwtUtil jwtUtil, CustomUserDetailsService userDetailsService, TokenService tokenService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
this.tokenService = tokenService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 获取请求头中的Authorization字段
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
// 检查Authorization头是否存在且以Bearer开头
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
try {
username = jwtUtil.extractUsername(jwt);
} catch (Exception e) {
logger.error("无法解析JWT token", e);
}
}
// 如果用户名不为空且当前上下文没有认证信息
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 验证token是否有效
if (jwtUtil.validateToken(jwt, userDetails.getUsername()) && tokenService.validateToken(username, jwt)) {
// 创建认证令牌
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
// 继续过滤器链
filterChain.doFilter(request, response);
}
}
7. Spring Security 配置
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 安全过滤器链配置
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF保护,因为我们使用JWT(API服务通常不需要)
.csrf(AbstractHttpConfigurer::disable)
// 配置请求授权
.authorizeHttpRequests(authz -> authz
// 允许无需认证访问的路径
.requestMatchers(
"/doc.html", // Knife4j文档界面
"/webjars/**", // Knife4j静态资源
"/v3/api-docs/**", // OpenAPI文档
"/swagger-resources", // Swagger资源
"/favicon.ico", // 网站图标
"/error", // 错误页面
"/actuator/health" // 健康检查
).permitAll()
// 认证相关接口允许匿名访问
.requestMatchers("/auth/**").permitAll()
// 其他所有请求需要认证
.anyRequest().authenticated()
)// 不创建会话,因为我们使用JWT
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 认证失败处理类
.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler));
// 添加JWT过滤器
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
8. 认证控制器
java
package com.example.securitydemo.controller;
import com.example.securitydemo.service.TokenService;
import com.example.securitydemo.util.JwtUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
private final TokenService tokenService;
public AuthController(AuthenticationManager authenticationManager, JwtUtil jwtUtil, TokenService tokenService) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
this.tokenService = tokenService;
}
@PostMapping("/login")
public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) {
// 认证用户
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成JWT token
String jwt = jwtUtil.generateToken(loginRequest.getUsername());
// 将token存储到Redis
tokenService.storeToken(loginRequest.getUsername(), jwt);
// 返回token
Map<String, String> response = new HashMap<>();
response.put("token", jwt);
response.put("type", "Bearer");
return ResponseEntity.ok(response);
}
@PostMapping("/logout")
public ResponseEntity<?> logoutUser() {
// 获取当前登录用户
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
String username = authentication.getName();
// 从Redis中删除token
tokenService.deleteToken(username);
}
return ResponseEntity.ok("成功登出");
}
// 登录请求DTO
public static class LoginRequest {
private String username;
private String password;
// getter和setter
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
}
9.认证失败处理类
java
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
int code = 403;
String msg = StrUtil.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
renderString(response, JSON.toJSONString(ApiResponse.error(msg, code)));
}
public static void renderString(HttpServletResponse response, String string) {
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
} catch (IOException e) {
e.printStackTrace();
}
}
}
10. 应用配置文件
yaml
spring:
redis:
# Redis 运行模式: standalone(单机), sentinel(哨兵), cluster(集群)
mode: standalone
# 通用配置
# 使用的数据库索引 (0-15)
database: 0
# 连接超时时间(毫秒)
timeout: 60000
# Redis 服务器密码
password: Dkdh@13579
# 连接池配置
lettuce:
pool:
# 连接池最大连接数
max-active: 8
# 最大阻塞等待时间(负值表示无限制)
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 0
# 单机模式配置
host: 172.16.18.227
port: 6379
# 哨兵模式配置
sentinel:
# 哨兵主节点名称
master: mymaster
# 哨兵节点列表,格式为 "host:port" 的逗号分隔列表
nodes: 127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381
# 集群模式配置
cluster:
# 集群节点列表,格式为 "host:port" 的逗号分隔列表
nodes: 127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003,127.0.0.1:7004,127.0.0.1:7005,127.0.0.1:7006
# 最大重定向次数
max-redirects: 3
# JWT配置
jwt:
#jwt加密钥
secret: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
# expiration-ms: 86400000 # token过期时间:24小时,单位毫秒
# token过期时间:30分钟,单位毫秒
expiration: 1800000
#用户密码 用户名默认为admin
security:
username: admin
password: 123456
实现说明
1. 整体流程:
- 用户通过登录接口获取 JWT token
- 服务器将 token 存储在 Redis 中并设置过期时间
- 后续请求在 Authorization 头中携带 token
- 过滤器验证 token 有效性和 Redis 中的存在性
- 验证通过则允许访问受保护资源
2.关键实现点:
- 使用 Spring Security 实现全局认证
- 配置不需要认证的路径(/api/public/** 和 /api/auth/login)
- 其他所有路径都需要认证
- JWT token 存储在 Redis 中,过期自动删除
- 使用 JWT 过滤器验证请求中的 token
3.使用方法:
- 启动 Redis 服务器
- 运行 Spring Boot 应用
- 先通过 POST /api/auth/login 获取 token
- 访问其他接口时在请求头中添加 Authorization: Bearer {token}
这个实现满足了基本需求,提供了一个安全的全局鉴权认证系统,同时通过 Redis 管理 token 的生命周期。