深入剖析 Spring Boot 3 + JWT + Redis 实现后台权限拦截与单点登录
前言
在博客网站开发中,安全且高效的权限认证体系是不可或缺的基石。传统的 Session 认证在分布式环境下存在扩展性问题,而纯粹的 JWT 无状态认证又难以实现"强制踢人下线"或"单点登录"等复杂业务需求。
基于以上的问题,本人采用springboot3+springmvc+jwt+Redis进行构建这一套"混合式"后台权限管理与单点登录(SSO)校验机制
Spring Security配置讲解:Spring Security
一、 架构设计思路
为了解决上述痛点,我们采用 JWT + Redis 双重校验的混合架构。
在这套架构中:
- JWT 负责携带用户基本信息(如 userId, username)并提供防篡改的签名验证。
- Redis 作为有状态的辅助存储,记录当前有效的 Token,用于控制 Token 的生命周期和唯一性。
- Spring MVC 拦截器 (Interceptor) 作为前端请求进入 Controller 之前的统一关卡,负责执行上述校验逻辑。

二、 核心拦截器实现:JwtInterceptor
JwtInterceptor 是整个权限校验体系的核心枢纽,实现了 HandlerInterceptor 接口。它不仅负责静态的 Token 解析,还负责与 Redis 交互进行动态状态比对。
2.1 拦截器完整代码
java
package com.xuan.common.interceptor;
import com.alibaba.fastjson2.JSON;
import com.nimbusds.jwt.JWTClaimsSet;
import com.xuan.common.domain.Result;
import com.xuan.common.enums.ErrorCode;
import com.xuan.common.utils.JwtUtils;
import com.xuan.common.constant.RedisConstant;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtInterceptor implements HandlerInterceptor {
private final JwtUtils jwtUtils;
private final StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 放行 OPTIONS 请求
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
// 2. 获取并校验 Token 基础信息
String token = request.getHeader(jwtUtils.getHeader());
if (token == null || token.isEmpty()) {
writeErrorResponse(response, ErrorCode.UNAUTHORIZED);
return false;
}
String tokenPrefix = jwtUtils.getTokenPrefix();
if (token.startsWith(tokenPrefix)) {
token = token.substring(tokenPrefix.length()).trim();
}
try {
// 3. 解析 Token
JWTClaimsSet claimsSet = jwtUtils.getAllClaimsFromToken(token);
String username = claimsSet.getSubject();
// 4. Redis 双重比对
String redisKey = RedisConstant.TOKEN_KEY_PREFIX + username;
String storedToken = stringRedisTemplate.opsForValue().get(redisKey);
if (storedToken == null) {
writeErrorResponse(response, ErrorCode.TOKEN_EXPIRED);
return false;
}
if (!storedToken.equals(token)) {
writeErrorResponse(response, ErrorCode.TOKEN_REPLACED);
return false;
}
// 5. 上下文透传
request.setAttribute("userClaims", claimsSet);
request.setAttribute("username", username);
request.setAttribute("userId", claimsSet.getClaim("userId"));
return true;
} catch (Exception e) {
log.error("Token 验证失败: {}", e.getMessage());
writeErrorResponse(response, ErrorCode.TOKEN_INVALID);
return false;
}
}
private void writeErrorResponse(HttpServletResponse response, ErrorCode errorCode) throws Exception {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_OK);
Result<?> result = Result.error(errorCode);
response.getWriter().write(JSON.toJSONString(result));
}
}
2.2 核心逻辑剖析
1. 放行 CORS 预检请求
在前后端分离架构中,浏览器会针对复杂请求发送 OPTIONS 预检请求。预检请求本身不携带自定义 Header(如 Token),因此拦截器必须优先将其放行。这保证了跨域请求的顺利握手。
2. Token 提取与基础校验
拦截器从请求头中获取 Token,判断为空时直接返回未授权错误。为了兼容 OAuth2 等标准协议,代码灵活处理了 Token 的前缀(如 Bearer ),通过截取前缀获取真实的 Token 字符串。
3. JWT 解析与 Redis 双重比对(核心单点登录逻辑)
通过 JwtUtils 解析获取到 username 后,程序会去 Redis 中查找对应的存储记录。此处设计了两个维度的拦截:
- Token 失效拦截: 如果 Redis 中不存在该 Token(值为 null),说明用户可能已主动登出,或 Token 自然过期被 Redis 清理,此时返回
TOKEN_EXPIRED错误。 - 顶号/单点登录拦截: 如果 Redis 中存储的 Token 与当前请求携带的不一致,说明该账号在其他设备或浏览器重新登录了(覆盖了 Redis 中的旧值),此时返回
TOKEN_REPLACED错误,强制当前客户端下线。
知识点补充:JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在网络应用环境间安全地传递声明。它由三部分组成:Header、Payload和Signature。本实现中,通过Redis存储Token用于实现"Token失效"和"多设备登录"控制,当用户登出或在其他设备登录时,Redis中的Token会被删除或更新,使旧Token失效。
4. 上下文透传机制
校验通过后,拦截器将解析出的 userClaims、username 和 userId 存入 HttpServletRequest 的属性 (Attribute) 中。这种设计极其优雅,下游 Controller 或 Service 层不再需要重新解析 Token,直接通过 request.getAttribute("userId") 即可获取身份信息,大幅降低性能开销。
5. 统一的响应规范
writeErrorResponse 方法在处理认证失败时,并没有抛出 401 或 403 的 HTTP 状态码,而是统一将状态设置为 HTTP 200 (HttpServletResponse.SC_OK)。它将 Result 对象转为 JSON 写入响应流,利用业务上的 ErrorCode 区分具体错误。这种设计非常契合现代前端框架(如 Axios)的全局响应拦截器处理习惯。
知识点补充:这种统一错误处理方式避免了HTTP状态码的混乱,前端可以根据
errorCode字段(如2001表示未授权,2003表示无权限)进行相应的处理,而不必依赖HTTP状态码。例如,当返回2001时,前端可以自动跳转到登录页面,提高了用户体验。

三、 全局 Web 配置:WebConfig
有了强大的拦截器,我们还需要将其注册到 Spring 容器中,并配置其拦截规则。WebConfig 类实现了 WebMvcConfigurer 接口,承担了路由规划、跨域处理和静态资源映射的职责。
3.1 WebConfig 完整代码
java
package com.xuan.common.config;
import com.xuan.common.interceptor.JwtInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/api/admin/**")
.excludePathPatterns(
"/api/admin/auth/login",
"/doc.html",
"/webjars/**",
"/v3/api-docs/**",
"/swagger-resources/**");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:./uploads/");
registry.addResourceHandler("/doc.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
3.2 核心配置剖析
1. 拦截器路由分配
通过重写 addInterceptors 方法,我们将 JwtInterceptor 注册到系统中。
- 精准打击:
addPathPatterns("/api/admin/**")明确指出只有后台管理接口需要经过严格的 JWT 校验。 - 白名单放行:
excludePathPatterns排除了登录接口,防止未登录用户无法获取 Token 的死循环。同时排除了 Swagger/Knife4j 的 API 文档及静态资源路径,方便开发调试。
知识点补充:Spring MVC的拦截器机制允许在请求处理前后执行特定逻辑。通过
addPathPatterns指定需要拦截的路径模式,excludePathPatterns排除不需要拦截的路径。这种配置方式使后台管理API必须通过认证,而登录接口等公共接口可以无需认证。
2. 全局跨域支持 (CORS)
通过 addCorsMappings 方法配置了强大的跨域策略:允许所有源、所有方法和所有请求头。配合 allowCredentials(true) 允许携带凭证(如 Cookie),并设置 maxAge(3600) 缓存预检请求一小时,有效提升了跨域握手性能。
知识点补充:CORS(跨源资源共享)是浏览器安全机制,防止恶意网站通过脚本访问其他域的资源。Spring Boot通过
CorsRegistry配置跨域策略,allowCredentials(true)表示允许发送Cookie等凭证信息,这对需要认证的API非常重要。maxAge设置预检请求的缓存时间,减少浏览器发送预检请求的次数。
3. 静态资源动静分离
addResourceHandlers 方法将对外暴露的 URL /uploads/** 映射到了服务器本地的文件目录 file:./uploads/。这在处理博客系统的文章插图、用户头像等本地上传文件时非常实用,实现了基础的动静资源分离。同时,也保障了 API 接口文档 UI 资源的正常加载。
知识点补充:Spring Boot默认将
static、public等目录下的文件作为静态资源。通过addResourceHandlers可以自定义静态资源的访问路径,file:./uploads/表示将/uploads/**请求映射到项目根目录下的uploads文件夹,方便处理文件上传后的访问。

四、 总结
通过 WebConfig 的路由规划与跨域支持,结合 JwtInterceptor 基于 JWT 和 Redis 的深度校验,我们使用 Spring Boot 构建了一套既能防御未授权访问,又能处理并发登录状态的高可用权限系统。这套体系结构清晰、职责单一、前后端交互友好,为后续复杂的业务开发扫清了安全障碍。
