Spring Security整合JWT与Redis实现权限认证

本文详细介绍如何基于Spring Boot整合Spring Security、JWT和Redis,构建一套完整的认证授权体系,实现无状态的身份验证和高效的权限管理。

一、Spring Security核心概念解析

1.1 核心组件介绍

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架,其核心组件包括:

Authentication(认证)

  • 认证是验证用户身份的过程,确认"你是谁"
  • 核心接口:Authentication,包含用户信息、权限列表、凭证等
  • 常见实现:UsernamePasswordAuthenticationToken

Authorization(授权)

  • 授权是验证用户权限的过程,确认"你能做什么"
  • 基于用户的角色或权限进行访问控制
  • 支持方法级、URL级等多种授权方式

SecurityContextHolder

  • 存储已认证用户的详细信息
  • 默认使用ThreadLocal存储,保证线程安全
  • 访问方式:SecurityContextHolder.getContext().getAuthentication()

UserDetailsService

  • 核心接口,用于加载用户特定数据
  • 方法:loadUserByUsername(String username)
  • 返回:UserDetails对象,包含用户详细信息

Filter(过滤器)

  • Spring Security通过过滤器链实现安全控制
  • 常见过滤器:
    • UsernamePasswordAuthenticationFilter:处理表单登录
    • JwtAuthenticationFilter:处理JWT令牌验证(自定义)
    • FilterSecurityInterceptor:进行最终的访问决策

1.2 认证流程详解

复制代码
用户请求 → 过滤器链 → 提取认证信息 → AuthenticationManager
    ↓
AuthenticationProvider(具体认证逻辑)
    ↓
UserDetailsService(加载用户信息)
    ↓
密码比对(BCryptPasswordEncoder)
    ↓
认证成功 → SecurityContextHolder存储Authentication
    ↓
放行请求 → 业务逻辑处理

1.3 JWT与Redis的协同价值

组件 作用 优势
JWT 无状态令牌,携带用户信息 减少数据库查询,支持跨域和分布式
Redis 存储Token黑名单、用户会话信息 快速撤销Token、支持主动登出、提高响应速度
Spring Security 统一的安全框架 成熟的权限控制体系,灵活的扩展机制

三者结合的架构优势:

  • 无状态认证:JWT使服务端无需存储会话,易于水平扩展
  • 灵活的权限控制:支持基于角色和方法的细粒度权限控制
  • 高安全性:Redis实现Token黑名单机制,支持即时撤销
  • 高性能:Redis缓存减少数据库压力

二、技术选型与整合方案

2.1 为什么选择JWT?

传统Session方案的局限:

  • 服务端需要存储会话信息,占用内存
  • 集群环境下需要Session共享(如Spring Session)
  • 跨域支持复杂

JWT的优势:

json 复制代码
{
  "header": {
    "alg": "HS256",
    "typ": "JWT"
  },
  "payload": {
    "sub": "1234567890",
    "name": "张三",
    "role": "ADMIN",
    "exp": 1737262800
  },
  "signature": "HMACSHA256(...)"
}
  • 自包含:Token包含所有必要的用户信息
  • 跨域友好:适合前后端分离架构
  • 无状态:服务端无需存储Session
  • 可扩展:可在Payload中存储自定义信息

2.2 为什么需要Redis?

纯JWT方案存在的问题:

  • Token无法主动撤销:签发后直到过期前都有效
  • 无法实现强制登出:用户修改密码后旧Token仍可使用
  • 无法追踪在线用户:难以实现踢人下线功能

Redis解决方案:

复制代码
Key设计:login:token:{userId}
Value:JWT Token
过期时间:与JWT过期时间一致

黑名单机制:
Key设计:blacklist:token:{tokenValue}
Value:用户ID
过期时间:剩余有效时间

2.3 整体架构设计

复制代码
┌─────────┐      登录请求       ┌──────────────┐
│         │ ──────────────────>│              │
│  前端   │                     │ Spring Boot  │
│         │ <────────────────── │   后端       │
└─────────┘   返回JWT Token    └──────────────┘
                      │                  │
                      │                  ↓
                      │          ┌──────────────┐
                      │          │ Redis        │
                      │          │ Token存储    │
                      │          │ 黑名单管理   │
                      │          └──────────────┘
                      │
                      │ 后续请求携带Token
                      │
┌─────────┐      请求+Header      ┌──────────────┐
│         │ ──────────────────>│              │
│  前端   │  Authorization:    │ Spring Boot  │
│         │  Bearer {JWT}      │              │
└─────────┘                     └──────────────┘
                                      │
                    ┌─────────────────┼─────────────────┐
                    ↓                 ↓                 ↓
              ┌──────────┐    ┌──────────┐      ┌──────────┐
              │ JWT验证  │    │ Redis校验│      │ 权限检查 │
              └──────────┘    └──────────┘      └──────────┘
                    │                 │                 │
                    └─────────────────┼─────────────────┘
                                      ↓
                              通过认证 → 返回数据
                              认证失败 → 返回401/403

三、完整代码实现

3.1 项目结构

复制代码
src/main/java/com/example/security/
├── config/
│   ├── RedisConfig.java              # Redis配置
│   └── SecurityConfig.java           # Spring Security核心配置
├── controller/
│   ├── AuthController.java           # 认证相关接口
│   └── TestController.java           # 测试接口
├── dto/
│   ├── LoginRequest.java             # 登录请求DTO
│   └── LoginResponse.java            # 登录响应DTO
├── entity/
│   └── User.java                     # 用户实体类
├── exception/
│   └── GlobalExceptionHandler.java  # 全局异常处理
├── filter/
│   └── JwtAuthenticationFilter.java # JWT认证过滤器
├── handler/
│   ├── AuthenticationEntryPointImpl.java  # 认证失败处理
│   └── AccessDeniedHandlerImpl.java         # 授权失败处理
├── mapper/
│   └── UserMapper.java               # 用户Mapper
├── security/
│   ├── JwtTokenUtil.java            # JWT工具类
│   └── UserDetailsServiceImpl.java  # 用户详情服务实现
└── service/
    ├── UserService.java              # 用户服务接口
    └── impl/
        └── UserServiceImpl.java      # 用户服务实现

3.2 依赖配置(pom.xml)

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>2.7.18</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>security-jwt-redis</artifactId>
    <version>1.0.0</version>
    <name>security-jwt-redis</name>
    <description>Spring Security整合JWT与Redis实现权限认证</description>

    <properties>
        <java.version>1.8</java.version>
        <jjwt.version>0.9.1</jjwt.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jjwt.version}</version>
        </dependency>

        <!-- MyBatis Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3</version>
        </dependency>

        <!-- MySQL驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Hutool工具类 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.20</version>
        </dependency>

        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-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>

3.3 配置文件(application.yml)

yaml 复制代码
server:
  port: 8080

spring:
  application:
    name: security-jwt-redis
  
  # 数据源配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
    username: root
    password: your_password
  
  # Redis配置
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
    timeout: 3000ms
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0

# MyBatis Plus配置
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.security.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# JWT配置
jwt:
  # JWT密钥(生产环境应使用更复杂的密钥)
  secret: your-secret-key-should-be-long-enough
  # Token过期时间(毫秒)- 7天
  expiration: 604800000
  # Token前缀
  token-prefix: Bearer 
  # 请求头名称
  header: Authorization

# 日志配置
logging:
  level:
    com.example.security: debug
    org.springframework.security: debug

3.4 数据库表结构

sql 复制代码
-- 用户表
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) NOT NULL COMMENT '密码(加密)',
  `nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-禁用 1-启用',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 角色表
CREATE TABLE `role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `role_name` varchar(50) NOT NULL COMMENT '角色名称',
  `role_code` varchar(50) NOT NULL COMMENT '角色编码',
  `description` varchar(200) DEFAULT NULL COMMENT '描述',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_role_code` (`role_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

-- 权限表
CREATE TABLE `permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '权限ID',
  `perm_name` varchar(50) NOT NULL COMMENT '权限名称',
  `perm_code` varchar(100) NOT NULL COMMENT '权限编码',
  `description` varchar(200) DEFAULT NULL COMMENT '描述',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_perm_code` (`perm_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表';

-- 用户角色关联表
CREATE TABLE `user_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `role_id` bigint(20) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';

-- 角色权限关联表
CREATE TABLE `role_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NOT NULL COMMENT '角色ID',
  `permission_id` bigint(20) NOT NULL COMMENT '权限ID',
  PRIMARY KEY (`id`),
  KEY `idx_role_id` (`role_id`),
  KEY `idx_permission_id` (`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限关联表';

-- 插入测试数据
INSERT INTO `user` (`id`, `username`, `password`, `nickname`, `email`, `status`) VALUES
(1, 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '管理员', 'admin@example.com', 1),
(2, 'user', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '普通用户', 'user@example.com', 1);
-- 密码都是:123456

INSERT INTO `role` (`id`, `role_name`, `role_code`) VALUES
(1, '管理员', 'ROLE_ADMIN'),
(2, '普通用户', 'ROLE_USER');

INSERT INTO `permission` (`id`, `perm_name`, `perm_code`) VALUES
(1, '用户管理', 'user:manage'),
(2, '系统管理', 'system:manage'),
(3, '数据查询', 'data:query');

INSERT INTO `user_role` (`user_id`, `role_id`) VALUES
(1, 1),
(2, 2);

INSERT INTO `role_permission` (`role_id`, `permission_id`) VALUES
(1, 1),
(1, 2),
(2, 3);

3.5 实体类与DTO

User.java

java 复制代码
package com.example.security.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 用户实体类
 */
@Data
@TableName("user")
public class User {
    
    @TableId(type = IdType.AUTO)
    private Long id;
    
    /**
     * 用户名
     */
    private String username;
    
    /**
     * 密码(加密存储)
     */
    private String password;
    
    /**
     * 昵称
     */
    private String nickname;
    
    /**
     * 邮箱
     */
    private String email;
    
    /**
     * 手机号
     */
    private String phone;
    
    /**
     * 状态:0-禁用 1-启用
     */
    private Integer status;
    
    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    
    /**
     * 更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}

LoginRequest.java

java 复制代码
package com.example.security.dto;

import lombok.Data;

import javax.validation.constraints.NotBlank;

/**
 * 登录请求DTO
 */
@Data
public class LoginRequest {
    
    /**
     * 用户名
     */
    @NotBlank(message = "用户名不能为空")
    private String username;
    
    /**
     * 密码
     */
    @NotBlank(message = "密码不能为空")
    private String password;
}

LoginResponse.java

java 复制代码
package com.example.security.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 登录响应DTO
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginResponse {
    
    /**
     * JWT Token
     */
    private String token;
    
    /**
     * Token类型
     */
    private String tokenType = "Bearer";
    
    /**
     * 过期时间(秒)
     */
    private Long expiresIn;
    
    /**
     * 用户信息
     */
    private UserInfo userInfo;
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class UserInfo {
        private Long id;
        private String username;
        private String nickname;
    }
}

3.6 JWT工具类实现

java 复制代码
package com.example.security.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JWT工具类
 * 负责Token的生成、解析和验证
 */
@Slf4j
@Component
public class JwtTokenUtil {
    
    @Value("${jwt.secret}")
    private String secret;
    
    @Value("${jwt.expiration}")
    private Long expiration;
    
    /**
     * Token前缀
     */
    private static final String TOKEN_PREFIX = "Bearer ";
    
    /**
     * 生成Token
     *
     * @param userDetails 用户详情
     * @return JWT Token
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", userDetails.getUsername());
        return generateToken(claims, userDetails.getUsername());
    }
    
    /**
     * 生成Token(带自定义声明)
     *
     * @param claims 自定义声明
     * @param subject 主题(通常是用户名)
     * @return JWT Token
     */
    private String generateToken(Map<String, Object> claims, String subject) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);
        
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }
    
    /**
     * 从Token中获取用户名
     *
     * @param token JWT Token
     * @return 用户名
     */
    public String getUsernameFromToken(String token) {
        return getClaimsFromToken(token).getSubject();
    }
    
    /**
     * 从Token中获取过期时间
     *
     * @param token JWT Token
     * @return 过期时间
     */
    public Date getExpirationDateFromToken(String token) {
        return getClaimsFromToken(token).getExpiration();
    }
    
    /**
     * 解析Token获取Claims
     *
     * @param token JWT Token
     * @return Claims对象
     */
    private Claims getClaimsFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }
    
    /**
     * 验证Token是否过期
     *
     * @param token JWT Token
     * @return true-未过期 false-已过期
     */
    public Boolean isTokenExpired(String token) {
        try {
            Date expiration = getExpirationDateFromToken(token);
            return expiration.before(new Date());
        } catch (Exception e) {
            return true;
        }
    }
    
    /**
     * 验证Token是否有效
     *
     * @param token JWT Token
     * @param userDetails 用户详情
     * @return true-有效 false-无效
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        try {
            String username = getUsernameFromToken(token);
            return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
        } catch (Exception e) {
            log.error("Token验证失败: {}", e.getMessage());
            return false;
        }
    }
    
    /**
     * 验证Token格式是否正确
     *
     * @param token JWT Token
     * @return true-有效 false-无效
     */
    public Boolean validateToken(String token) {
        try {
            getClaimsFromToken(token);
            return !isTokenExpired(token);
        } catch (Exception e) {
            log.error("Token格式验证失败: {}", e.getMessage());
            return false;
        }
    }
    
    /**
     * 获取Token过期时间(秒)
     *
     * @return 过期时间(秒)
     */
    public Long getExpiration() {
        return expiration / 1000;
    }
    
    /**
     * 从请求头中提取Token
     *
     * @param authHeader Authorization请求头
     * @return JWT Token(不含Bearer前缀)
     */
    public String extractToken(String authHeader) {
        if (authHeader != null && authHeader.startsWith(TOKEN_PREFIX)) {
            return authHeader.substring(TOKEN_PREFIX.length());
        }
        return null;
    }
}

3.7 Redis配置与操作

RedisConfig.java

java 复制代码
package com.example.security.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis配置类
 */
@Configuration
@EnableCaching
public class RedisConfig {
    
    /**
     * 配置RedisTemplate
     * 使用String序列化Key,JSON序列化Value
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        
        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        
        // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = 
            new Jackson2JsonRedisSerializer<>(Object.class);
        
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(
            LaissezFaireSubTypeValidator.instance,
            ObjectMapper.DefaultTyping.NON_FINAL
        );
        
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        
        template.afterPropertiesSet();
        return template;
    }
}

RedisService.java

java 复制代码
package com.example.security.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * Redis服务类
 * 封装Redis常用操作
 */
@Service
public class RedisService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * Token存储Key前缀
     */
    private static final String TOKEN_PREFIX = "login:token:";
    
    /**
     * 黑名单Key前缀
     */
    private static final String BLACKLIST_PREFIX = "blacklist:token:";
    
    /**
     * 存储Token
     *
     * @param userId 用户ID
     * @param token JWT Token
     * @param timeout 过期时间(秒)
     */
    public void setToken(Long userId, String token, long timeout) {
        String key = TOKEN_PREFIX + userId;
        redisTemplate.opsForValue().set(key, token, timeout, TimeUnit.SECONDS);
    }
    
    /**
     * 获取Token
     *
     * @param userId 用户ID
     * @return JWT Token
     */
    public String getToken(Long userId) {
        String key = TOKEN_PREFIX + userId;
        return (String) redisTemplate.opsForValue().get(key);
    }
    
    /**
     * 删除Token
     *
     * @param userId 用户ID
     */
    public void deleteToken(Long userId) {
        String key = TOKEN_PREFIX + userId;
        redisTemplate.delete(key);
    }
    
    /**
     * 将Token加入黑名单
     *
     * @param token JWT Token
     * @param timeout 过期时间(秒)
     */
    public void addToBlacklist(String token, long timeout) {
        String key = BLACKLIST_PREFIX + token;
        redisTemplate.opsForValue().set(key, "1", timeout, TimeUnit.SECONDS);
    }
    
    /**
     * 检查Token是否在黑名单中
     *
     * @param token JWT Token
     * @return true-在黑名单中 false-不在
     */
    public boolean isTokenBlacklisted(String token) {
        String key = BLACKLIST_PREFIX + token;
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }
    
    /**
     * 检查Token是否有效
     *
     * @param userId 用户ID
     * @param token JWT Token
     * @return true-有效 false-无效
     */
    public boolean isTokenValid(Long userId, String token) {
        String storedToken = getToken(userId);
        // Token不存在或Token已变化,说明已失效
        if (storedToken == null || !storedToken.equals(token)) {
            return false;
        }
        // 检查是否在黑名单中
        return !isTokenBlacklisted(token);
    }
    
    /**
     * 设置缓存
     *
     * @param key 键
     * @param value 值
     * @param timeout 过期时间(秒)
     */
    public void set(String key, Object value, long timeout) {
        redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    }
    
    /**
     * 获取缓存
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }
    
    /**
     * 删除缓存
     *
     * @param key 键
     */
    public void delete(String key) {
        redisTemplate.delete(key);
    }
    
    /**
     * 判断Key是否存在
     *
     * @param key 键
     * @return true-存在 false-不存在
     */
    public Boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }
}

3.8 UserDetailsService实现

java 复制代码
package com.example.security.security;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.security.entity.User;
import com.example.security.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * 用户详情服务实现
 * 负责从数据库加载用户信息
 */
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户信息
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        User user = userMapper.selectOne(queryWrapper);
        
        if (user == null) {
            log.error("用户不存在: {}", username);
            throw new UsernameNotFoundException("用户不存在: " + username);
        }
        
        // 查询用户权限(简化示例,实际应从关联表查询)
        Collection<GrantedAuthority> authorities = getUserAuthorities(user);
        
        // 返回UserDetails实现
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                user.getStatus() == 1, // 账号启用状态
                true,                   // 账号未过期
                true,                   // 凭证未过期
                true,                   // 账号未锁定
                authorities
        );
    }
    
    /**
     * 获取用户权限列表
     * TODO: 实际应从数据库查询用户角色和权限
     *
     * @param user 用户信息
     * @return 权限集合
     */
    private Collection<GrantedAuthority> getUserAuthorities(User user) {
        List<GrantedAuthority> authorities = new ArrayList<>();
        
        // 根据用户名赋予不同角色(演示用)
        if ("admin".equals(user.getUsername())) {
            authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
            authorities.add(new SimpleGrantedAuthority("user:manage"));
            authorities.add(new SimpleGrantedAuthority("system:manage"));
        } else {
            authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
            authorities.add(new SimpleGrantedAuthority("data:query"));
        }
        
        return authorities;
    }
}

3.9 JWT认证过滤器

java 复制代码
package com.example.security.filter;

import com.example.security.entity.User;
import com.example.security.handler.AuthenticationEntryPointImpl;
import com.example.security.security.JwtTokenUtil;
import com.example.security.security.UserDetailsServiceImpl;
import com.example.security.service.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * JWT认证过滤器
 * 拦截请求,验证JWT Token
 */
@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    
    @Autowired
    private RedisService redisService;
    
    @Value("${jwt.header}")
    private String tokenHeader;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain chain)
            throws ServletException, IOException {
        
        // 从请求头中获取Token
        String authHeader = request.getHeader(tokenHeader);
        String token = jwtTokenUtil.extractToken(authHeader);
        
        // 如果Token存在且格式正确
        if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            try {
                // 验证Token格式
                if (!jwtTokenUtil.validateToken(token)) {
                    log.warn("Token格式无效: {}", token);
                    chain.doFilter(request, response);
                    return;
                }
                
                // 获取用户名
                String username = jwtTokenUtil.getUsernameFromToken(token);
                
                // 加载用户详情
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                
                // 验证Token是否有效
                // 获取用户ID(这里简化处理,实际应从Token中获取)
                Long userId = getUserIdFromToken(token);
                
                // 检查Redis中Token是否有效且不在黑名单中
                if (!redisService.isTokenValid(userId, token)) {
                    log.warn("Token已失效或被撤销: userId={}, token={}", userId, token);
                    chain.doFilter(request, response);
                    return;
                }
                
                // 验证Token
                if (jwtTokenUtil.validateToken(token, userDetails)) {
                    // 创建认证对象
                    UsernamePasswordAuthenticationToken authentication = 
                        new UsernamePasswordAuthenticationToken(
                            userDetails, 
                            null, 
                            userDetails.getAuthorities()
                        );
                    
                    // 设置详细信息
                    authentication.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request)
                    );
                    
                    // 将认证信息存入SecurityContext
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                    
                    log.debug("用户认证成功: {}", username);
                }
            } catch (Exception e) {
                log.error("JWT认证失败", e);
                SecurityContextHolder.clearContext();
            }
        }
        
        chain.doFilter(request, response);
    }
    
    /**
     * 从Token中获取用户ID(简化处理)
     * 实际应在生成Token时将userId存入Claims
     *
     * @param token JWT Token
     * @return 用户ID
     */
    private Long getUserIdFromToken(String token) {
        // 这里简化处理,根据用户名查询用户ID
        // 实际应在生成Token时将userId存入Claims中
        String username = jwtTokenUtil.getUsernameFromToken(token);
        if ("admin".equals(username)) {
            return 1L;
        } else if ("user".equals(username)) {
            return 2L;
        }
        return 0L;
    }
}

3.10 自定义认证和授权处理器

AuthenticationEntryPointImpl.java

java 复制代码
package com.example.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 认证失败处理器
 * 当用户未认证(未登录或Token无效)时触发
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    
    @Override
    public void commence(HttpServletRequest request, 
                        HttpServletResponse response, 
                        AuthenticationException authException)
            throws IOException, ServletException {
        
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        
        Map<String, Object> result = new HashMap<>();
        result.put("code", 401);
        result.put("message", "认证失败,请先登录");
        result.put("data", null);
        
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.writeValue(response.getWriter(), result);
    }
}

AccessDeniedHandlerImpl.java

java 复制代码
package com.example.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 授权失败处理器
 * 当用户已认证但权限不足时触发
 */
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    
    @Override
    public void handle(HttpServletRequest request, 
                     HttpServletResponse response, 
                     AccessDeniedException accessDeniedException)
            throws IOException, ServletException {
        
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        
        Map<String, Object> result = new HashMap<>();
        result.put("code", 403);
        result.put("message", "权限不足,拒绝访问");
        result.put("data", null);
        
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.writeValue(response.getWriter(), result);
    }
}

3.11 Spring Security核心配置

java 复制代码
package com.example.security.config;

import com.example.security.filter.JwtAuthenticationFilter;
import com.example.security.handler.AccessDeniedHandlerImpl;
import com.example.security.handler.AuthenticationEntryPointImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * Spring Security核心配置类
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级权限控制
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    
    @Autowired
    private AuthenticationEntryPointImpl authenticationEntryPoint;
    
    @Autowired
    private AccessDeniedHandlerImpl accessDeniedHandler;
    
    /**
     * 密码编码器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    /**
     * 认证管理器
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // 禁用CSRF(JWT方案不需要)
            .csrf().disable()
            
            // 禁用CORS(前后端分离需要单独配置)
            .cors().disable()
            
            // 无状态Session
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            
            // 配置请求授权规则
            .authorizeRequests()
                // 登录接口允许匿名访问
                .antMatchers("/auth/login", "/auth/register").permitAll()
                // Swagger文档允许匿名访问
                .antMatchers("/swagger-ui/**", "/swagger-resources/**", 
                           "/v2/api-docs", "/webjars/**").permitAll()
                // 其他请求需要认证
                .anyRequest().authenticated()
            .and()
            
            // 配置异常处理
            .exceptionHandling()
                // 认证失败处理器
                .authenticationEntryPoint(authenticationEntryPoint)
                // 授权失败处理器
                .accessDeniedHandler(accessDeniedHandler)
            .and()
            
            // 添加JWT过滤器(在UsernamePasswordAuthenticationFilter之前)
            .addFilterBefore(jwtAuthenticationFilter, 
                           UsernamePasswordAuthenticationFilter.class);
    }
}

3.12 Mapper接口

java 复制代码
package com.example.security.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.security.entity.User;
import org.apache.ibatis.annotations.Mapper;

/**
 * 用户Mapper接口
 */
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

3.13 Service层

UserService.java

java 复制代码
package com.example.security.service;

import com.example.security.entity.User;

/**
 * 用户服务接口
 */
public interface UserService {
    
    /**
     * 根据用户名查询用户
     *
     * @param username 用户名
     * @return 用户信息
     */
    User getUserByUsername(String username);
}

UserServiceImpl.java

java 复制代码
package com.example.security.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.security.entity.User;
import com.example.security.mapper.UserMapper;
import com.example.security.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 用户服务实现
 */
@Service
public class UserServiceImpl implements UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Override
    public User getUserByUsername(String username) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        return userMapper.selectOne(queryWrapper);
    }
}

3.14 Controller层

AuthController.java
java 复制代码
package com.example.security.controller;

import com.example.security.dto.LoginRequest;
import com.example.security.dto.LoginResponse;
import com.example.security.entity.User;
import com.example.security.security.JwtTokenUtil;
import com.example.security.service.RedisService;
import com.example.security.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;

/**
 * 认证控制器
 */
@Slf4j
@RestController
@RequestMapping("/auth")
public class AuthController {
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private RedisService redisService;
    
    /**
     * 用户登录
     *
     * @param loginRequest 登录请求
     * @return 登录响应
     */
    @PostMapping("/login")
    public LoginResponse login(@Valid @RequestBody LoginRequest loginRequest) {
        String username = loginRequest.getUsername();
        String password = loginRequest.getPassword();
        
        log.info("用户登录请求: username={}", username);
        
        // 使用AuthenticationManager进行认证
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(username, password)
        );
        
        // 认证成功后,获取用户详情
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        
        // 生成JWT Token
        String token = jwtTokenUtil.generateToken(userDetails);
        long expiresIn = jwtTokenUtil.getExpiration();
        
        // 查询用户信息
        User user = userService.getUserByUsername(username);
        
        // 将Token存储到Redis
        redisService.setToken(user.getId(), token, expiresIn);
        
        log.info("用户登录成功: userId={}, username={}", user.getId(), username);
        
        // 构造返回结果
        LoginResponse.UserInfo userInfo = new LoginResponse.UserInfo(
            user.getId(), 
            user.getUsername(), 
            user.getNickname()
        );
        
        return new LoginResponse(token, "Bearer", expiresIn, userInfo);
    }
    
    /**
     * 用户登出
     *
     * @param request HTTP请求
     * @return 响应结果
     */
    @PostMapping("/logout")
    public Map<String, Object> logout(HttpServletRequest request) {
        // 从SecurityContext获取已认证用户信息
        Authentication authentication = 
            SecurityContextHolder.getContext().getAuthentication();
        
        if (authentication != null && authentication.isAuthenticated()) {
            String username = authentication.getName();
            User user = userService.getUserByUsername(username);
            
            // 获取请求头中的Token
            String authHeader = request.getHeader("Authorization");
            String token = jwtTokenUtil.extractToken(authHeader);
            
            // 从Redis删除Token
            redisService.deleteToken(user.getId());
            
            // 将Token加入黑名单
            long remainingTime = getRemainingTokenTime(token);
            if (remainingTime > 0) {
                redisService.addToBlacklist(token, remainingTime);
            }
            
            // 清除SecurityContext
            SecurityContextHolder.clearContext();
            
            log.info("用户登出成功: userId={}, username={}", user.getId(), username);
        }
        
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "登出成功");
        return result;
    }
    
    /**
     * 刷新Token
     *
     * @param request HTTP请求
     * @return 新的Token
     */
    @PostMapping("/refresh")
    public LoginResponse refreshToken(HttpServletRequest request) {
        // 从请求头获取Token
        String authHeader = request.getHeader("Authorization");
        String oldToken = jwtTokenUtil.extractToken(authHeader);
        
        // 验证旧Token
        String username = jwtTokenUtil.getUsernameFromToken(oldToken);
        User user = userService.getUserByUsername(username);
        
        // 检查Redis中Token是否有效
        if (!redisService.isTokenValid(user.getId(), oldToken)) {
            throw new RuntimeException("Token已失效,请重新登录");
        }
        
        // 生成新Token
        UserDetails userDetails = new org.springframework.security.core.userdetails.User(
            username, 
            "", 
            user.getStatus() == 1,
            true, 
            true, 
            true, 
            null
        );
        String newToken = jwtTokenUtil.generateToken(userDetails);
        long expiresIn = jwtTokenUtil.getExpiration();
        
        // 更新Redis中的Token
        redisService.setToken(user.getId(), newToken, expiresIn);
        
        log.info("Token刷新成功: userId={}, username={}", user.getId(), username);
        
        LoginResponse.UserInfo userInfo = new LoginResponse.UserInfo(
            user.getId(), 
            user.getUsername(), 
            user.getNickname()
        );
        
        return new LoginResponse(newToken, "Bearer", expiresIn, userInfo);
    }
    
    /**
     * 获取Token剩余有效时间(秒)
     *
     * @param token JWT Token
     * @return 剩余时间(秒)
     */
    private long getRemainingTokenTime(String token) {
        try {
            Date expirationDate = jwtTokenUtil.getExpirationDateFromToken(token);
            Date now = new Date();
            long remainingTime = (expirationDate.getTime() - now.getTime()) / 1000;
            return Math.max(0, remainingTime);
        } catch (Exception e) {
            return 0;
        }
    }
}

TestController.java

java 复制代码
package com.example.security.controller;

import com.example.security.entity.User;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * 测试控制器
 */
@RestController
@RequestMapping("/api")
public class TestController {
    
    /**
     * 获取当前用户信息
     * 需要登录认证
     */
    @GetMapping("/user/info")
    public Map<String, Object> getUserInfo(Authentication authentication) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "获取成功");
        result.put("username", authentication.getName());
        result.put("authorities", authentication.getAuthorities());
        return result;
    }
    
    /**
     * 管理员接口
     * 需要ADMIN角色
     */
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/admin/dashboard")
    public Map<String, Object> adminDashboard() {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "管理员仪表盘");
        result.put("data", "这里只有管理员能看到");
        return result;
    }
    
    /**
     * 用户管理接口
     * 需要user:manage权限
     */
    @PreAuthorize("hasAuthority('user:manage')")
    @GetMapping("/users")
    public Map<String, Object> getUserList() {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "获取用户列表成功");
        result.put("data", "用户列表数据...");
        return result;
    }
    
    /**
     * 数据查询接口
     * 需要data:query权限
     */
    @PreAuthorize("hasAuthority('data:query')")
    @GetMapping("/data/query")
    public Map<String, Object> queryData() {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "数据查询成功");
        result.put("data", "查询结果数据...");
        return result;
    }
    
    /**
     * 系统管理接口
     * 需要system:manage权限
     */
    @PreAuthorize("hasAuthority('system:manage')")
    @PostMapping("/system/config")
    public Map<String, Object> updateSystemConfig() {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "系统配置更新成功");
        return result;
    }
    
    /**
     * 普通用户可访问接口
     * 需要USER角色或data:query权限
     */
    @PreAuthorize("hasRole('USER') or hasAuthority('data:query')")
    @GetMapping("/user/profile")
    public Map<String, Object> getUserProfile() {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "获取用户资料成功");
        result.put("data", "用户资料数据...");
        return result;
    }
}

3.15 全局异常处理

java 复制代码
package com.example.security.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

/**
 * 全局异常处理器
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    /**
     * 处理权限不足异常
     */
    @ExceptionHandler(AccessDeniedException.class)
    public Map<String, Object> handleAccessDeniedException(AccessDeniedException e) {
        log.error("权限不足异常", e);
        Map<String, Object> result = new HashMap<>();
        result.put("code", 403);
        result.put("message", "权限不足,拒绝访问");
        result.put("data", null);
        return result;
    }
    
    /**
     * 处理运行时异常
     */
    @ExceptionHandler(RuntimeException.class)
    public Map<String, Object> handleRuntimeException(RuntimeException e) {
        log.error("运行时异常", e);
        Map<String, Object> result = new HashMap<>();
        result.put("code", 500);
        result.put("message", "系统异常:" + e.getMessage());
        result.put("data", null);
        return result;
    }
    
    /**
     * 处理其他异常
     */
    @ExceptionHandler(Exception.class)
    public Map<String, Object> handleException(Exception e) {
        log.error("系统异常", e);
        Map<String, Object> result = new HashMap<>();
        result.put("code", 500);
        result.put("message", "系统异常");
        result.put("data", null);
        return result;
    }
}

3.16 启动类

java 复制代码
package com.example.security;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 应用启动类
 */
@SpringBootApplication
@MapperScan("com.example.security.mapper")
public class SecurityJwtRedisApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(SecurityJwtRedisApplication.class, args);
        System.out.println("========================================");
        System.out.println("Security JWT Redis 应用启动成功!");
        System.out.println("访问地址:http://localhost:8080");
        System.out.println("========================================");
    }
}

四、功能测试与验证

4.1 测试环境准备

  1. 启动Redis服务
bash 复制代码
# Windows
redis-server.exe

# Linux/Mac
redis-server
  1. 启动MySQL服务并执行上述建表SQL脚本

  2. 启动Spring Boot应用

4.2 接口测试

使用Postman或curl进行接口测试。

1. 用户登录测试

请求:

http 复制代码
POST http://localhost:8080/auth/login
Content-Type: application/json

{
  "username": "admin",
  "password": "123456"
}

预期响应:

json 复制代码
{
  "token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE3MzcyNjI4MDB9.xxx",
  "tokenType": "Bearer",
  "expiresIn": 604800,
  "userInfo": {
    "id": 1,
    "username": "admin",
    "nickname": "管理员"
  }
}

2. 访问需要认证的接口(带Token)

请求:

http 复制代码
GET http://localhost:8080/api/user/info
Authorization: Bearer {你的Token}

预期响应:

json 复制代码
{
  "code": 200,
  "message": "获取成功",
  "username": "admin",
  "authorities": [
    {
      "authority": "ROLE_ADMIN"
    },
    {
      "authority": "user:manage"
    },
    {
      "authority": "system:manage"
    }
  ]
}

3. 权限测试 - 管理员接口

请求:

http 复制代码
GET http://localhost:8080/api/admin/dashboard
Authorization: Bearer {你的Token}

预期响应(使用admin账号Token):

json 复制代码
{
  "code": 200,
  "message": "管理员仪表盘",
  "data": "这里只有管理员能看到"
}

预期响应(使用user账号Token):

json 复制代码
{
  "code": 403,
  "message": "权限不足,拒绝访问",
  "data": null
}

4. 无Token访问测试

请求:

http 复制代码
GET http://localhost:8080/api/user/info

预期响应:

json 复制代码
{
  "code": 401,
  "message": "认证失败,请先登录",
  "data": null
}

5. 用户登出测试

请求:

http 复制代码
POST http://localhost:8080/auth/logout
Authorization: Bearer {你的Token}

预期响应:

json 复制代码
{
  "code": 200,
  "message": "登出成功"
}

验证:使用已登出的Token再次访问接口,应该返回401

6. Token刷新测试

请求:

http 复制代码
POST http://localhost:8080/auth/refresh
Authorization: Bearer {你的Token}

预期响应:

json 复制代码
{
  "token": "新的JWT Token",
  "tokenType": "Bearer",
  "expiresIn": 604800,
  "userInfo": {
    "id": 1,
    "username": "admin",
    "nickname": "管理员"
  }
}

4.3 Redis验证

登录后查看Redis中的数据:

bash 复制代码
# 连接Redis
redis-cli

# 查看所有Key
keys *

# 查看Token
get login:token:1

# 登出后查看黑名单
keys blacklist:*

4.4 测试用例汇总表

测试场景 测试接口 请求方式 预期结果
用户登录 /auth/login POST 返回JWT Token
未认证访问 /api/user/info GET 返回401
已认证访问 /api/user/info GET(带Token) 返回用户信息
管理员权限 /api/admin/dashboard GET(admin Token) 返回200
权限不足 /api/admin/dashboard GET(user Token) 返回403
用户登出 /auth/logout POST(带Token) 返回200,Token失效
Token刷新 /auth/refresh POST(带Token) 返回新Token
登出后访问 /api/user/info GET(已登出Token) 返回401

五、注意事项与最佳实践

5.1 常见问题与解决方案

问题1:Token过期后如何处理?

解决方案:

  • 前端在请求失败时检查401状态码
  • 引入刷新Token机制(Refresh Token)
  • 或引导用户重新登录
java 复制代码
// 前端处理示例
axios.interceptors.response.use(
  response => response,
  error => {
    if (error.response.status === 401) {
      // Token过期,跳转登录页
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

问题2:如何实现踢人下线功能?

解决方案:

  • 删除Redis中的Token记录
  • 将Token加入黑名单
  • 清除用户Session
java 复制代码
public void forceLogout(Long userId) {
    // 1. 删除Redis中的Token
    redisService.deleteToken(userId);
    
    // 2. 将当前Token加入黑名单
    String token = getCurrentToken();
    redisService.addToBlacklist(token, getRemainingTime(token));
}

问题3:如何处理并发登录?

方案一:允许同一用户多设备登录

  • Redis Key设计为 login:token:{userId}:{deviceId}
  • 支持多点登录,记录在线设备

方案二:单点登录(踢出其他设备)

  • 登录时删除旧Token
  • 将旧Token加入黑名单
java 复制代码
public String login(LoginRequest request) {
    // ...认证逻辑...
    
    // 单点登录:删除旧Token
    String oldToken = redisService.getToken(user.getId());
    if (oldToken != null) {
        redisService.addToBlacklist(oldToken, jwtTokenUtil.getExpiration());
    }
    
    // 生成并存储新Token
    String newToken = jwtTokenUtil.generateToken(userDetails);
    redisService.setToken(user.getId(), newToken, expiresIn);
    
    return newToken;
}

问题4:Redis故障时如何降级?

解决方案:

  • 使用本地缓存(如Caffeine)作为降级方案
  • 或临时关闭Token校验,仅依赖JWT验证
java 复制代码
// 降级策略
public boolean isTokenValid(Long userId, String token) {
    try {
        return redisService.isTokenValid(userId, token);
    } catch (Exception e) {
        log.error("Redis故障,降级为仅JWT验证", e);
        // Redis不可用时,只验证Token格式和有效期
        return jwtTokenUtil.validateToken(token);
    }
}

5.2 安全最佳实践

1. Token安全

yaml 复制代码
# 生产环境配置建议
jwt:
  # 密钥长度至少256位(32字节以上)
  secret: ${JWT_SECRET:your-very-long-random-secret-key-for-production}
  # Token有效期不宜过长,建议7天以下
  expiration: 604800000  # 7天
  # 使用HTTPS传输,防止Token被窃取

代码层面:

  • 敏感接口增加防重放攻击机制(nonce、timestamp)
  • Token存储在HttpOnly Cookie中(防止XSS攻击)
  • 定期刷新密钥(Key Rotation)

2. 密码安全

java 复制代码
// 使用BCryptPasswordEncoder加密
@Bean
public PasswordEncoder passwordEncoder() {
    // 强度设置为10-12
    return new BCryptPasswordEncoder(12);
}

// 密码复杂度校验
public boolean validatePassword(String password) {
    // 至少8位,包含大小写字母、数字、特殊字符
    String pattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$";
    return password.matches(pattern);
}

3. Redis安全

yaml 复制代码
spring:
  redis:
    password: ${REDIS_PASSWORD}  # 设置Redis密码
    # 使用Redis集群或哨兵模式保证高可用

安全措施:

  • 禁止Redis暴露在公网
  • 使用独立数据库存储敏感数据
  • 定期备份Redis数据

4. SQL注入防护

java 复制代码
// 使用MyBatis Plus的参数绑定,避免SQL注入
@Mapper
public interface UserMapper extends BaseMapper<User> {
    // 正确:使用QueryWrapper或注解
    @Select("SELECT * FROM user WHERE username = #{username}")
    User findByUsername(@Param("username") String username);
    
    // 错误:字符串拼接,存在SQL注入风险
    // @Select("SELECT * FROM user WHERE username = '" + username + "'")
}

5. 权限设计原则

  • 最小权限原则:用户只拥有完成任务所需的最小权限
  • 职责分离:敏感操作需要多个角色共同授权
  • 审计日志:记录所有敏感操作
java 复制代码
// 敏感操作示例
@PreAuthorize("hasRole('ADMIN') and hasAuthority('system:manage')")
@PostMapping("/system/critical-action")
@Transactional
public Result criticalAction() {
    // 操作前记录审计日志
    auditLogService.logOperation("执行敏感操作", getCurrentUser());
    
    // 执行业务逻辑
    // ...
    
    return Result.success();
}

6. CORS配置

java 复制代码
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")  // 生产环境指定具体域名
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

5.3 性能优化建议

1. Redis连接池优化

yaml 复制代码
spring:
  redis:
    lettuce:
      pool:
        max-active: 16        # 最大连接数
        max-idle: 8           # 最大空闲连接
        min-idle: 2           # 最小空闲连接
        max-wait: 3000ms      # 最大等待时间
      shutdown-timeout: 100ms

2. Token缓存优化

java 复制代码
// 缓存用户权限信息,减少数据库查询
@Cacheable(value = "user:permissions", key = "#userId")
public Set<String> getUserPermissions(Long userId) {
    // 查询数据库
    return permissionMapper.selectPermissionsByUserId(userId);
}

3. 异步处理

java 复制代码
// 登录成功后异步记录日志
@Async
public void recordLoginLog(Long userId, String ip) {
    LoginLog log = new LoginLog();
    log.setUserId(userId);
    log.setIp(ip);
    log.setLoginTime(new Date());
    loginLogMapper.insert(log);
}

5.4 监控与日志

重要日志记录

java 复制代码
@Slf4j
@Component
public class LoginAuditListener {
    
    @EventListener
    public void handleAuthenticationSuccess(AuthenticationSuccessEvent event) {
        String username = event.getAuthentication().getName();
        log.info("用户登录成功: {}", username);
        // 发送到日志系统
    }
    
    @EventListener
    public void handleAuthenticationFailure(AuthenticationFailureEvent event) {
        String username = event.getAuthentication().getName();
        log.warn("用户登录失败: {}", username);
    }
}

监控指标

  • 登录成功率/失败率
  • Token发放数量
  • Redis命中率
  • API响应时间
  • 并发登录数

六、总结

本文详细介绍了Spring Security整合JWT与Redis实现权限认证的完整方案,包括:

  1. 核心概念:Spring Security的核心组件和工作原理
  2. 技术选型:JWT+Redis协同的优势和架构设计
  3. 完整实现:从项目结构到核心代码的完整实现
  4. 功能测试:详细的测试用例和预期结果
  5. 最佳实践:安全性、性能优化、常见问题解决方案

核心优势:

  • ✅ 无状态认证,易于水平扩展
  • ✅ Redis支持Token主动撤销
  • ✅ 细粒度权限控制
  • ✅ 高性能、高可用

适用场景:

  • 前后端分离的Web应用
  • 移动端API
  • 微服务架构
  • 中大型企业应用

参考资源:

相关推荐
咩咩啃树皮8 小时前
第40篇:Vue3组件化开发精讲——组件拆分、复用、父子通信、工程化架构
java·前端·架构
鱟鲥鳚9 小时前
Spring Boot 集成 LangChain4j:从模型调用到 Tool Calling(Demo版)
java·spring boot
大模型码小白10 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程
腾渊信息科技公司11 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
爱笑的源码基地12 小时前
高并发 Redis 缓存门诊HIS系统源码,含财务统计药房进销存
java·程序·门诊系统·诊所系统·云诊所源码
wuqingshun31415912 小时前
TCP超时重传机制是为了解决什么问题?
java
二宝哥13 小时前
CentOS 7.9 系统下 Redis 5.0.4 单机安装与源码编译安装指南
redis·centos
莫逸风15 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
z1234567898615 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
yaoxin52112316 小时前
470. Java 反射 - Member 接口与 AccessFlag
java·开发语言·python