本文详细介绍如何基于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: 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 数据库表结构
— 用户表
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 测试环境准备
# 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:your–very–long–random–secret–key–for–production}
# 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实现权限认证的完整方案,包括:
核心优势:
- ✅ 无状态认证,易于水平扩展
- ✅ Redis支持Token主动撤销
- ✅ 细粒度权限控制
- ✅ 高性能、高可用
适用场景:
- 前后端分离的Web应用
- 移动端API
- 微服务架构
- 中大型企业应用
参考资源:
- Spring Security官方文档:https://spring.io/projects/spring-security
- JWT规范:https://jwt.io/
- Redis官方文档:https://redis.io/documentation
网硕互联帮助中心





评论前必须登录!
注册