当前位置: 首页 > news >正文

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 的生命周期。

http://www.dtcms.com/a/395891.html

相关文章:

  • 【靶场】webshop渗透攻击
  • 深入浅出现代GPU架构:核心类型、精度模式与选择
  • 开发避坑指南(53):git 命令行标签维护方法
  • javaEE初阶 网络编程(socket初识)
  • 基于Springboot + vue3实现的实验室研究生信息管理系统
  • TwinCat是什么
  • Linux 修炼:进程概念(下)
  • PostgreSQL 全表 count 优化实践:从 SeqScan 痛点分析到 heapam 改进与性能突破
  • 第17讲 机器学习vs神经网络
  • 1. 设计模式--工厂方法模式
  • SpringBoot常用配置
  • 【论文阅读】π0:用于通用机器人控制的视觉-语言-动作流模型
  • Spring 框架学习指南
  • Vue3 父子组件通信实战:props 与 provide/inject 方案对比及用法解析
  • el-image标签预览和VForm打包后项目上层级冲突问题
  • QML学习笔记(九)QML的全局对象
  • element里的select自定义输入的时候,不用点击下拉框选中自定义输入,而是当焦点失去的时候自动赋值输入的内容
  • 链改2.0+港促会,携手赋能 Web3引企赴港!
  • C++第二篇:命名空间(namespace)
  • vcsa 重启服务
  • QT 两种库写法 LIBS += .a和LIBS += -L -l
  • 比斯特自动化|电动自行车电池点焊机的作用与使用
  • Django 模型与 ORM 全解析(一):从基础到实战的完整指南
  • NW955NW960美光固态闪存NW963NW971
  • iOS 26 软件兼容性大检查,哪些 App 出问题、API 变动要注意、旧功能不支持兼容性测试全流程
  • HarmonyOS NEXT互动卡片开发:从原理到实战的完整指南
  • 邪修实战系列(6)
  • Clover: 1靶场渗透
  • 智慧供水管网监测解决方案:实现压力、流量、水质数据集与监控
  • 深入理解Java虚拟机内存模型