云计算百科
云计算领域专业知识百科平台

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的优势:

{
"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 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)

server:
port: 8080

spring:
application:
name: securityjwtredis

# 数据源配置
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: yoursecretkeyshouldbelongenough
# Token过期时间(毫秒)- 7天
expiration: 604800000
# Token前缀
token-prefix: Bearer
# 请求头名称
header: Authorization

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

3.4 数据库表结构

— 用户表
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

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

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

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工具类实现

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

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

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实现

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认证过滤器

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

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

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核心配置

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接口

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

package com.example.security.service;

import com.example.security.entity.User;

/**
* 用户服务接口
*/

public interface UserService {

/**
* 根据用户名查询用户
*
* @param username 用户名
* @return 用户信息
*/

User getUserByUsername(String username);
}

UserServiceImpl.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

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

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 全局异常处理

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 启动类

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 测试环境准备

  • 启动Redis服务
  • # Windows
    redis-server.exe

    # Linux/Mac
    redis-server

  • 启动MySQL服务并执行上述建表SQL脚本

  • 启动Spring Boot应用

  • 4.2 接口测试

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

    1. 用户登录测试

    请求:

    POST http://localhost:8080/auth/login
    Content-Type: application/json

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

    预期响应:

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

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

    请求:

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

    预期响应:

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

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

    请求:

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

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

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

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

    {
    "code": 403,
    "message": "权限不足,拒绝访问",
    "data": null
    }

    4. 无Token访问测试

    请求:

    GET http://localhost:8080/api/user/info

    预期响应:

    {
    "code": 401,
    "message": "认证失败,请先登录",
    "data": null
    }

    5. 用户登出测试

    请求:

    POST http://localhost:8080/auth/logout
    Authorization: Bearer {你的Token}

    预期响应:

    {
    "code": 200,
    "message": "登出成功"
    }

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

    6. Token刷新测试

    请求:

    POST http://localhost:8080/auth/refresh
    Authorization: Bearer {你的Token}

    预期响应:

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

    4.3 Redis验证

    登录后查看Redis中的数据:

    # 连接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)
    • 或引导用户重新登录

    // 前端处理示例
    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

    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加入黑名单

    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验证

    // 降级策略
    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安全

    # 生产环境配置建议
    jwt:
    # 密钥长度至少256位(32字节以上)
    secret: ${JWT_SECRET:yourverylongrandomsecretkeyforproduction}
    # Token有效期不宜过长,建议7天以下
    expiration: 604800000 # 7天
    # 使用HTTPS传输,防止Token被窃取

    代码层面:

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

    2. 密码安全

    // 使用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安全

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

    安全措施:

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

    4. SQL注入防护

    // 使用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. 权限设计原则

    • 最小权限原则:用户只拥有完成任务所需的最小权限
    • 职责分离:敏感操作需要多个角色共同授权
    • 审计日志:记录所有敏感操作

    // 敏感操作示例
    @PreAuthorize("hasRole('ADMIN') and hasAuthority('system:manage')")
    @PostMapping("/system/critical-action")
    @Transactional
    public Result criticalAction() {
    // 操作前记录审计日志
    auditLogService.logOperation("执行敏感操作", getCurrentUser());

    // 执行业务逻辑
    // …

    return Result.success();
    }

    6. CORS配置

    @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连接池优化

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

    2. Token缓存优化

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

    3. 异步处理

    // 登录成功后异步记录日志
    @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 监控与日志

    重要日志记录

    @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实现权限认证的完整方案,包括:

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

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

    适用场景:

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

    参考资源:

    • Spring Security官方文档:https://spring.io/projects/spring-security
    • JWT规范:https://jwt.io/
    • Redis官方文档:https://redis.io/documentation
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Spring Security整合JWT与Redis实现权限认证
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!