Spring Boot 全局鉴权认证简单实现方案
Springboot3.3.13+jdk17+SpringSecurity+Redis实现了一个简单的全局鉴权认证系统,其中 token 存储在 Redis 中并设置过期时间。这里贴出来供大家参考。
首先需要添加必要的依赖,然后实现核心配置和功能类:
1. Maven 依赖配置
<?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 配置
@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 连接工厂*/@Beanpublic 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*/@Beanpublic 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 序列化和反序列化 valueJackson2JsonRedisSerializer<Object> jacksonSerializer = getJacksonSerializer();template.setValueSerializer(jacksonSerializer);template.setHashValueSerializer(jacksonSerializer);// 初始化 RedisTemplatetemplate.afterPropertiesSet();return template;}/*** 创建并配置 Jackson2JsonRedisSerializer*/private Jackson2JsonRedisSerializer<Object> getJacksonSerializer() {// 配置 ObjectMapperObjectMapper objectMapper = new ObjectMapper();// 设置序列化的可见性:任何字段(包括私有字段)都序列化objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);// 启用默认的类型信息,以便反序列化时能正确识别类型objectMapper.activateDefaultTyping(// 使用宽松的类型验证器LaissezFaireSubTypeValidator.instance,// 对所有非final类型存储类型信息ObjectMapper.DefaultTyping.NON_FINAL,JsonTypeInfo.As.PROPERTY);// 通过构造函数创建 Jackson2JsonRedisSerializerreturn new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);}
}
3. JWT 工具类
@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. 自定义用户详情服务
@Service
public class CustomUserDetailsService implements UserDetailsService {@Value("${security.password}")private String securityPassword;@Value("${security.username}")private String securityUsername;@Overridepublic 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 存储服务
@Service
public class TokenService {@Autowiredprivate 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 认证过滤器
@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;}@Overrideprotected 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 配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {private final JwtAuthenticationFilter jwtAuthenticationFilter;public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {this.jwtAuthenticationFilter = jwtAuthenticationFilter;}/*** 认证失败处理类*/@Autowiredprivate AuthenticationEntryPointImpl unauthorizedHandler;@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 安全过滤器链配置*/@Beanpublic 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();}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {return authConfig.getAuthenticationManager();}
}
8. 认证控制器
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 tokenString jwt = jwtUtil.generateToken(loginRequest.getUsername());// 将token存储到RedistokenService.storeToken(loginRequest.getUsername(), jwt);// 返回tokenMap<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中删除tokentokenService.deleteToken(username);}return ResponseEntity.ok("成功登出");}// 登录请求DTOpublic static class LoginRequest {private String username;private String password;// getter和setterpublic 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.认证失败处理类
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {@Overridepublic 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. 应用配置文件
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.227port: 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: adminpassword: 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 的生命周期。