Spring Boot 全局鉴权认证简单实现方案

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

相关推荐
泉城老铁2 小时前
Spring Boot和Vue.js项目中实现文件压缩下载功能
前端·spring boot·后端
麦兜*3 小时前
Spring Boot 项目 Docker 化:从零到一的完整实战指南
数据库·spring boot·redis·后端·spring·缓存·docker
JarvanMo3 小时前
我尝试了Appwrite, Supabase和 Firebase Databases
前端·后端
老葱头蒸鸡3 小时前
(8)ASP.NET Core2.2 中的MVC路由一
后端·asp.net·mvc
uhakadotcom3 小时前
分享近期学到的postgresql的几个实用的新特性!
后端·面试·github
刘立军3 小时前
本地大模型编程实战(37)使用知识图谱增强RAG(3)
后端·架构·llm
天天摸鱼的java工程师3 小时前
Java 设计模式(观察者模式)+ Redis:游戏成就系统(条件达成检测、奖励自动发放)
java·后端