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
  • 微服务架构
  • 中大型企业应用

参考资源:

相关推荐
什么都不会的Tristan2 小时前
redis-原理篇-Dict
数据库·redis·缓存
三角叶蕨2 小时前
【苍穹外卖】day1
java
WAZYY06192 小时前
通过LocalDateTime判断当前日期是否失效(附Java 中常用的 ISO 格式)
java·iso·日期·localdate·时间处理·日期处理·日期格式
超级种码2 小时前
Redis:Redis键值淘汰策略
redis·spring·bootstrap
皙然2 小时前
SpringBoot 自动装配深度解析:从底层原理到自定义 starter 实战(含源码断点调试)
java·spring boot·spring
重生之绝世牛码2 小时前
Linux软件安装 —— Redis集群安装(三主三从)
大数据·linux·运维·数据库·redis·数据库开发·软件安装
NE_STOP2 小时前
SpringBoot3-外部化配置与aop实现
java
ThinkPet2 小时前
【AI】大模型知识入门扫盲以及SpringAi快速入门
java·人工智能·ai·大模型·rag·springai·mcp
派大鑫wink3 小时前
【Day39】Spring 核心注解:@Component、@Autowired、@Configuration 等
java·后端·spring