5. 拦截器 (Interceptor)

拦截器(Interceptor)基础概念

一、说明

1. 什么是拦截器?

拦截器(Interceptor)是一种设计模式,用于在方法调用前后插入额外的处理逻辑。它可以在不修改原有代码的情况下,对请求或响应进行预处理和后处理。

2. 核心特点

  • 非侵入性:不需要修改原有业务代码
  • 可插拔:可以动态添加或移除拦截器
  • 链式调用:多个拦截器可以形成拦截器链
  • AOP思想:体现了面向切面编程的理念

3. 主要应用场景

3.1 Web开发

  • 用户认证和授权
  • 日志记录
  • 性能监控
  • 请求参数校验
  • 跨域处理(CORS)
  • 编码转换

3.2 框架层面

  • Spring MVC的HandlerInterceptor
  • MyBatis的插件机制
  • Struts2的拦截器

4. 拦截器的生命周期

请求到达 → preHandle() → 目标方法执行 → postHandle() → 视图渲染 → afterCompletion()

4.1 preHandle(预处理)

  • 执行时机:在目标方法执行之前
  • 返回值:boolean类型
    • true:继续执行后续拦截器和目标方法
    • false:中断请求,不再执行后续操作
  • 典型用途:权限验证、登录检查

4.2 postHandle(后处理)

  • 执行时机:目标方法执行之后,视图渲染之前
  • 典型用途:修改ModelAndView、添加公共数据

4.3 afterCompletion(完成后处理)

  • 执行时机:整个请求完成之后(视图渲染完成)
  • 典型用途:资源清理、日志记录、性能统计

5. 拦截器 vs 过滤器(Filter)

特性拦截器(Interceptor)过滤器(Filter)
规范Spring框架提供Servlet规范
拦截范围只拦截Controller请求拦截所有请求
依赖依赖Spring容器依赖Servlet容器
注入能力可以注入Spring Bean不能直接注入Spring Bean
执行顺序在Filter之后在Interceptor之前
细粒度更细粒度的控制粗粒度的控制

6. 代码示例(Spring MVC)

@component
public class AuthInterceptor implements HandlerInterceptor {
  
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        // 预处理:检查用户是否登录
        String token = request.getHeader("Authorization");
      
        if (token == null || !validateToken(token)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return false; // 中断请求
        }
      
        return true; // 继续执行
    }
  
    @Override
    public void postHandle(HttpServletRequest request, 
                          HttpServletResponse response, 
                          Object handler, 
                          ModelAndView modelAndView) throws Exception {
        // 后处理:添加公共数据
        if (modelAndView != null) {
            modelAndView.addObject("currentTime", System.currentTimeMillis());
        }
    }
  
    @Override
    public void afterCompletion(HttpServletRequest request, 
                               HttpServletResponse response, 
                               Object handler, 
                               Exception ex) throws Exception {
        // 完成后处理:记录日志
        long startTime = (Long) request.getAttribute("startTime");
        long endTime = System.currentTimeMillis();
        System.out.println("请求耗时:" + (endTime - startTime) + "ms");
    }
}

7. 配置拦截器

@Configuration
public class WebConfig implements WebMvcConfigurer {
  
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor())
                .addPathPatterns("/api/**")      // 拦截路径
                .excludePathPatterns("/api/login"); // 排除路径
    }
}
CleanShot 2025-10-21 at 11.54.36@2x.png

二、流程图

1. 拦截器基本工作流程

graph TD
    A[客户端发起请求] --> B[请求到达DispatcherServlet]
    B --> C{拦截器链preHandle}
    C -->|返回false| D[请求被拦截,返回响应]
    C -->|返回true| E[执行Controller方法]
    E --> F[Controller返回ModelAndView]
    F --> G[拦截器链postHandle]
    G --> H[视图渲染]
    H --> I[拦截器链afterCompletion]
    I --> J[返回响应给客户端]
    D --> J
  
    style C fill:#ffeb3b
    style E fill:#4caf50
    style G fill:#2196f3
    style I fill:#ff9800

2. 多个拦截器执行顺序

graph LR
    A[请求] --> B[Interceptor1.preHandle]
    B --> C[Interceptor2.preHandle]
    C --> D[Interceptor3.preHandle]
    D --> E[Controller执行]
    E --> F[Interceptor3.postHandle]
    F --> G[Interceptor2.postHandle]
    G --> H[Interceptor1.postHandle]
    H --> I[视图渲染]
    I --> J[Interceptor3.afterCompletion]
    J --> K[Interceptor2.afterCompletion]
    K --> L[Interceptor1.afterCompletion]
    L --> M[响应]
  
    style E fill:#4caf50
    style I fill:#9c27b0

3. 拦截器链中断流程

graph TD
    A[请求开始] --> B[Interceptor1.preHandle]
    B -->|返回true| C[Interceptor2.preHandle]
    C -->|返回false| D[请求被拦截]
    D --> E[Interceptor1.afterCompletion]
    E --> F[返回响应]
  
    C -.不执行.-> G[Interceptor3.preHandle]
    G -.不执行.-> H[Controller]
    H -.不执行.-> I[postHandle方法]
  
    style C fill:#f44336
    style D fill:#ff9800
    style E fill:#2196f3

4. 拦截器完整生命周期

sequenceDiagram
    participant Client as 客户端
    participant Filter as 过滤器
    participant Interceptor as 拦截器
    participant Controller as 控制器
    participant View as 视图
  
    Client->>Filter: 1. 发送请求
    Filter->>Interceptor: 2. 过滤后传递
    Interceptor->>Interceptor: 3. preHandle()
    alt preHandle返回true
        Interceptor->>Controller: 4. 执行业务逻辑
        Controller->>Interceptor: 5. 返回ModelAndView
        Interceptor->>Interceptor: 6. postHandle()
        Interceptor->>View: 7. 渲染视图
        View->>Interceptor: 8. 渲染完成
        Interceptor->>Interceptor: 9. afterCompletion()
        Interceptor->>Filter: 10. 返回响应
        Filter->>Client: 11. 响应客户端
    else preHandle返回false
        Interceptor->>Interceptor: 执行afterCompletion()
        Interceptor->>Client: 直接返回响应
    end

5. 拦截器应用架构图

graph TB
    subgraph 客户端层
    A[浏览器/移动端]
    end
  
    subgraph Web层
    B[Filter过滤器]
    C[DispatcherServlet]
    end
  
    subgraph 拦截器层
    D[认证拦截器]
    E[日志拦截器]
    F[性能监控拦截器]
    end
  
    subgraph 业务层
    G[Controller]
    H[Service]
    I[DAO]
    end
  
    A --> B
    B --> C
    C --> D
    D --> E
    E --> F
    F --> G
    G --> H
    H --> I
  
    style D fill:#ff6b6b
    style E fill:#4ecdc4
    style F fill:#45b7d1

三、最佳实践

1. 设计原则

  • 单一职责:每个拦截器只负责一个功能
  • 顺序合理:认证 → 授权 → 日志 → 业务
  • 性能优先:避免在拦截器中执行耗时操作
  • 异常处理:妥善处理异常,避免影响主流程

2. 常见问题

  • 拦截器不生效:检查配置路径和顺序
  • 循环依赖:避免在拦截器中注入可能导致循环依赖的Bean
  • 线程安全:拦截器是单例的,注意线程安全问题

3. 性能优化建议

  • 使用ThreadLocal存储请求级别的数据
  • 避免在preHandle中进行数据库查询
  • 合理使用缓存减少重复计算

总结

拦截器是后端开发中非常重要的组件,它提供了一种优雅的方式来处理横切关注点。通过合理使用拦截器,可以:

✅ 提高代码的可维护性和可扩展性 ✅ 实现关注点分离,保持业务代码的纯净 ✅ 统一处理通用逻辑,减少代码重复 ✅ 灵活控制请求的执行流程

拦截器实现登录校验与令牌校验详解

一、业务场景说明

1. 为什么需要登录校验?

在实际的Web应用中,很多接口需要用户登录后才能访问,例如:

  • 个人中心信息
  • 订单管理
  • 购物车操作
  • 用户设置

传统做法的问题:

// ❌ 在每个Controller方法中都要写重复代码
@GetMapping("/user/info")
public Result getUserInfo(HttpServletRequest request) {
    // 重复的校验逻辑
    String token = request.getHeader("Authorization");
    if (token == null || !validateToken(token)) {
        return Result.error("未登录");
    }
  
    // 业务逻辑
    return Result.success(userService.getUserInfo());
}

使用拦截器的优势:

  • ✅ 统一处理,避免代码重复
  • ✅ 关注点分离,业务代码更纯净
  • ✅ 易于维护和修改
  • ✅ 灵活配置拦截规则

二、令牌(Token)认证机制

1. 什么是Token?

Token(令牌)是服务器生成的一串加密字符串,用于标识用户身份。

2. Token认证流程

sequenceDiagram
    participant C as 客户端
    participant S as 服务器
    participant DB as 数据库/Redis
  
    Note over C,DB: 登录阶段
    C->>S: 1. 发送用户名密码
    S->>DB: 2. 验证用户信息
    DB-->>S: 3. 返回用户数据
    S->>S: 4. 生成Token
    S->>DB: 5. 存储Token
    S-->>C: 6. 返回Token
  
    Note over C,DB: 访问受保护资源
    C->>S: 7. 携带Token请求
    S->>S: 8. 拦截器校验Token
    S->>DB: 9. 验证Token有效性
    DB-->>S: 10. 返回验证结果
    alt Token有效
        S->>S: 11. 执行业务逻辑
        S-->>C: 12. 返回数据
    else Token无效
        S-->>C: 13. 返回401未授权
    end

3. Token的类型

类型说明特点适用场景
UUID Token随机生成的唯一字符串需要服务端存储传统Web应用
JWT TokenJSON Web Token自包含用户信息,无需存储微服务、移动端
Session Token基于Session的令牌依赖Cookie传统Web应用

三、完整的登录校验流程

1. 整体架构图

graph TB
    subgraph 客户端
        A[浏览器/APP]
    end
  
    subgraph 服务端
        B[登录接口<br/>/login]
        C[业务接口<br/>/api/**]
        D[登录拦截器<br/>LoginInterceptor]
        E[Controller层]
        F[Service层]
    end
  
    subgraph 存储层
        G[(Redis<br/>存储Token)]
        H[(MySQL<br/>用户数据)]
    end
  
    A -->|1.登录请求| B
    B -->|2.验证用户| H
    B -->|3.生成Token| G
    B -->|4.返回Token| A
  
    A -->|5.携带Token访问| C
    C -->|6.拦截校验| D
    D -->|7.验证Token| G
    D -->|8.放行| E
    E --> F
    F -->|9.返回数据| A
  
    style D fill:#ff6b6b
    style B fill:#4ecdc4
    style G fill:#ffe66d

2. 详细流程图

flowchart TD
    Start([用户发起请求]) --> CheckPath{是否需要<br/>登录校验?}
  
    CheckPath -->|不需要<br/>如:/login| DirectPass[直接放行]
    DirectPass --> Controller[执行Controller]
  
    CheckPath -->|需要<br/>如:/api/**| GetToken[从请求头获取Token]
    GetToken --> HasToken{Token<br/>是否存在?}
  
    HasToken -->|不存在| Return401A[返回401<br/>未登录]
  
    HasToken -->|存在| ValidateFormat{Token<br/>格式正确?}
    ValidateFormat -->|否| Return401B[返回401<br/>Token格式错误]
  
    ValidateFormat -->|是| CheckRedis{Redis中<br/>Token存在?}
    CheckRedis -->|不存在| Return401C[返回401<br/>Token已过期]
  
    CheckRedis -->|存在| CheckExpire{Token<br/>是否过期?}
    CheckExpire -->|已过期| DeleteToken[删除过期Token]
    DeleteToken --> Return401D[返回401<br/>登录已过期]
  
    CheckExpire -->|未过期| GetUserInfo[获取用户信息]
    GetUserInfo --> SetThreadLocal[存入ThreadLocal]
    SetThreadLocal --> RefreshToken[刷新Token过期时间]
    RefreshToken --> PassRequest[放行请求]
    PassRequest --> Controller
  
    Controller --> Business[执行业务逻辑]
    Business --> ClearThreadLocal[清理ThreadLocal]
    ClearThreadLocal --> ReturnResponse[返回响应]
  
    Return401A --> End([结束])
    Return401B --> End
    Return401C --> End
    Return401D --> End
    ReturnResponse --> End
  
    style CheckPath fill:#FFD700
    style HasToken fill:#FF6347
    style CheckRedis fill:#FF6347
    style CheckExpire fill:#FF6347
    style PassRequest fill:#32CD32
    style Return401A fill:#DC143C
    style Return401B fill:#DC143C
    style Return401C fill:#DC143C
    style Return401D fill:#DC143C

四、代码实现

1. 项目结构

com.example.demo
├── config
│   └── WebConfig.java              // 拦截器配置
├── interceptor
│   └── LoginInterceptor.java       // 登录拦截器
├── controller
│   ├── LoginController.java        // 登录控制器
│   └── UserController.java         // 业务控制器
├── service
│   └── UserService.java            // 用户服务
├── utils
│   ├── JwtUtil.java                // JWT工具类
│   ├── TokenUtil.java              // Token工具类
│   └── ThreadLocalUtil.java        // ThreadLocal工具类
└── entity
    └── User.java                   // 用户实体

2. 登录拦截器实现

package com.example.demo.interceptor;

import com.example.demo.utils.JwtUtil;
import com.example.demo.utils.ThreadLocalUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 登录校验拦截器
 */
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
      
        // 1. 获取请求路径(用于日志)
        String requestURI = request.getRequestURI();
        log.info("拦截到请求: {}", requestURI);
      
        // 2. 从请求头中获取Token
        String token = request.getHeader("Authorization");
      
        // 3. 判断Token是否存在
        if (!StringUtils.hasLength(token)) {
            log.warn("Token不存在,请求路径: {}", requestURI);
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":401,\"message\":\"未登录,请先登录\"}");
            return false;
        }
      
        try {
            // 4. 验证Token格式并解析
            Map<String, Object> claims = JwtUtil.parseToken(token);
          
            // 5. 从Redis中验证Token是否存在(防止Token被盗用)
            String redisToken = redisTemplate.opsForValue().get("token:" + token);
            if (redisToken == null) {
                log.warn("Token在Redis中不存在或已过期: {}", token);
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write("{\"code\":401,\"message\":\"登录已过期,请重新登录\"}");
                return false;
            }
          
            // 6. 将用户信息存入ThreadLocal(供后续业务使用)
            ThreadLocalUtil.set(claims);
            log.info("用户信息已存入ThreadLocal: {}", claims.get("userId"));
          
            // 7. 刷新Token过期时间(滑动过期策略)
            redisTemplate.expire("token:" + token, 30, TimeUnit.MINUTES);
          
            // 8. 放行请求
            return true;
          
        } catch (Exception e) {
            // Token解析失败
            log.error("Token解析失败: {}", e.getMessage());
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":401,\"message\":\"Token无效或已过期\"}");
            return false;
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, 
                               HttpServletResponse response, 
                               Object handler, 
                               Exception ex) throws Exception {
        // 清理ThreadLocal,防止内存泄漏
        ThreadLocalUtil.remove();
        log.info("ThreadLocal已清理");
    }
}

3. 拦截器配置

package com.example.demo.config;

import com.example.demo.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Web配置类 - 注册拦截器
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                // 拦截所有请求
                .addPathPatterns("/**")
                // 排除不需要登录的路径
                .excludePathPatterns(
                        "/user/login",           // 登录接口
                        "/user/register",        // 注册接口
                        "/doc.html",             // Swagger文档
                        "/swagger-resources/**", // Swagger资源
                        "/webjars/**",           // Swagger UI
                        "/v2/api-docs",          // Swagger API
                        "/error",                // 错误页面
                        "/favicon.ico",          // 网站图标
                        "/static/**"             // 静态资源
                );
    }
}

4. JWT工具类

package com.example.demo.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

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

/**
 * JWT工具类
 */
public class JwtUtil {

    // 密钥(实际项目中应该放在配置文件中)
    private static final String SECRET_KEY = "your-secret-key-must-be-very-long-and-secure";
  
    // Token有效期(毫秒)
    private static final long EXPIRATION = 1000 * 60 * 60 * 24; // 24小时

    /**
     * 生成Token
     * @param claims 用户信息
     * @return Token字符串
     */
    public static String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)                              // 设置载荷
                .setIssuedAt(new Date())                        // 签发时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) // 过期时间
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 签名算法
                .compact();
    }

    /**
     * 解析Token
     * @param token Token字符串
     * @return 用户信息
     */
    public static Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 验证Token是否过期
     * @param token Token字符串
     * @return true-已过期,false-未过期
     */
    public static boolean isTokenExpired(String token) {
        try {
            Claims claims = parseToken(token);
            return claims.getExpiration().before(new Date());
        } catch (Exception e) {
            return true;
        }
    }
}

5. ThreadLocal工具类

package com.example.demo.utils;

import java.util.Map;

/**
 * ThreadLocal工具类 - 存储当前登录用户信息
 */
public class ThreadLocalUtil {
  
    private static final ThreadLocal<Map<String, Object>> THREAD_LOCAL = new ThreadLocal<>();

    /**
     * 存储用户信息
     */
    public static void set(Map<String, Object> value) {
        THREAD_LOCAL.set(value);
    }

    /**
     * 获取用户信息
     */
    public static Map<String, Object> get() {
        return THREAD_LOCAL.get();
    }

    /**
     * 清除用户信息(防止内存泄漏)
     */
    public static void remove() {
        THREAD_LOCAL.remove();
    }
}

6. 登录控制器

package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import com.example.demo.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 登录控制器
 */
@Slf4j
@RestController
@RequestMapping("/user")
public class LoginController {

    @Autowired
    private UserService userService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 用户登录
     */
    @PostMapping("/login")
    public Result<String> login(@RequestBody User user) {
        log.info("用户登录: {}", user.getUsername());
      
        // 1. 验证用户名密码
        User dbUser = userService.login(user.getUsername(), user.getPassword());
        if (dbUser == null) {
            return Result.error("用户名或密码错误");
        }
      
        // 2. 生成Token
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", dbUser.getId());
        claims.put("username", dbUser.getUsername());
        String token = JwtUtil.generateToken(claims);
      
        // 3. 将Token存入Redis(设置过期时间30分钟)
        redisTemplate.opsForValue().set(
            "token:" + token, 
            dbUser.getId().toString(), 
            30, 
            TimeUnit.MINUTES
        );
      
        log.info("登录成功,Token: {}", token);
        return Result.success(token);
    }

    /**
     * 用户登出
     */
    @PostMapping("/logout")
    public Result<Void> logout(@RequestHeader("Authorization") String token) {
        // 从Redis中删除Token
        redisTemplate.delete("token:" + token);
        log.info("用户登出,Token已删除");
        return Result.success();
    }
}

7. 业务控制器

package com.example.demo.controller;

import com.example.demo.utils.ThreadLocalUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * 用户业务控制器
 */
@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserController {

    /**
     * 获取当前登录用户信息
     */
    @GetMapping("/info")
    public Result<Map<String, Object>> getUserInfo() {
        // 从ThreadLocal中获取用户信息(拦截器已存入)
        Map<String, Object> userInfo = ThreadLocalUtil.get();
        log.info("获取用户信息: {}", userInfo);
        return Result.success(userInfo);
    }

    /**
     * 修改用户信息
     */
    @PutMapping("/update")
    public Result<Void> updateUser(@RequestBody User user) {
        // 从ThreadLocal获取当前用户ID
        Map<String, Object> claims = ThreadLocalUtil.get();
        Long userId = Long.valueOf(claims.get("userId").toString());
      
        // 业务逻辑...
        log.info("用户{}修改信息", userId);
        return Result.success();
    }
}

五、Token存储方案对比

1. 存储方案对比表

方案优点缺点适用场景
Redis性能高、支持过期、易于管理需要额外维护Redis高并发、分布式系统
数据库持久化、数据安全性能较低、增加数据库压力小型系统
JWT自包含无需存储、减轻服务器压力无法主动失效、Token较大微服务、无状态系统
内存Map实现简单重启丢失、不支持分布式开发测试环境

2. 推荐方案:Redis + JWT

graph LR
    A[客户端] -->|携带JWT Token| B[服务器]
    B -->|解析JWT| C{Token格式<br/>是否正确?}
    C -->|否| D[返回401]
    C -->|是| E[从Redis验证]
    E -->|不存在| F[返回401<br/>Token已失效]
    E -->|存在| G[放行请求]
  
    style C fill:#FFD700
    style E fill:#FF6347
    style G fill:#32CD32
    style D fill:#DC143C
    style F fill:#DC143C

优势:

  • JWT自包含用户信息,减少数据库查询
  • Redis存储Token状态,支持主动失效
  • 结合两者优点,性能和安全兼顾

六、安全增强方案

1. Token刷新机制

sequenceDiagram
    participant C as 客户端
    participant S as 服务器
    participant R as Redis
  
    C->>S: 1. 携带Access Token请求
    S->>R: 2. 验证Token
    R-->>S: 3. Token即将过期
    S->>S: 4. 生成新的Access Token
    S->>R: 5. 存储新Token
    S-->>C: 6. 返回数据+新Token
    C->>C: 7. 更新本地Token

实现代码:

@Override
public boolean preHandle(HttpServletRequest request, 
                       HttpServletResponse response, 
                       Object handler) throws Exception {
  
    String token = request.getHeader("Authorization");
  
    // 验证Token...
  
    // 检查Token剩余有效期
    Long expire = redisTemplate.getExpire("token:" + token, TimeUnit.MINUTES);
  
    // 如果剩余时间少于5分钟,自动刷新
    if (expire != null && expire < 5) {
        // 生成新Token
        Map<String, Object> claims = JwtUtil.parseToken(token);
        String newToken = JwtUtil.generateToken(claims);
      
        // 删除旧Token
        redisTemplate.delete("token:" + token);
      
        // 存储新Token
        redisTemplate.opsForValue().set(
            "token:" + newToken, 
            claims.get("userId").toString(), 
            30, 
            TimeUnit.MINUTES
        );
      
        // 在响应头中返回新Token
        response.setHeader("New-Token", newToken);
        log.info("Token已自动刷新");
    }
  
    return true;
}

2. 双Token机制(Access Token + Refresh Token)

graph TD
    A[用户登录] --> B[生成Access Token<br/>有效期15分钟]
    B --> C[生成Refresh Token<br/>有效期7天]
    C --> D[返回两个Token]
  
    D --> E[正常请求<br/>携带Access Token]
    E --> F{Access Token<br/>是否过期?}
  
    F -->|未过期| G[正常访问]
    F -->|已过期| H[使用Refresh Token<br/>请求新的Access Token]
  
    H --> I{Refresh Token<br/>是否有效?}
    I -->|有效| J[生成新的Access Token]
    I -->|无效| K[要求重新登录]
  
    J --> G
  
    style B fill:#4CAF50
    style C fill:#2196F3
    style F fill:#FFD700
    style I fill:#FF6347

3. IP绑定验证

@Override
public boolean preHandle(HttpServletRequest request, 
                       HttpServletResponse response, 
                       Object handler) throws Exception {
  
    String token = request.getHeader("Authorization");
    String currentIp = getClientIp(request);
  
    // 从Redis获取Token绑定的IP
    String storedIp = redisTemplate.opsForValue().get("token:ip:" + token);
  
    // 验证IP是否一致
    if (storedIp != null && !storedIp.equals(currentIp)) {
        log.warn("IP地址不匹配,可能存在Token盗用。存储IP: {}, 当前IP: {}", storedIp, currentIp);
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write("{\"code\":401,\"message\":\"登录异常,请重新登录\"}");
        return false;
    }
  
    return true;
}

/**
 * 获取客户端真实IP
 */
private String getClientIp(HttpServletRequest request) {
    String ip = request.getHeader("X-Forwarded-For");
    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getHeader("X-Real-IP");
    }
    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getRemoteAddr();
    }
    return ip;
}

七、完整的登录校验流程图(含异常处理)

flowchart TD
    Start([客户端请求]) --> GetPath[获取请求路径]
    GetPath --> CheckWhitelist{是否在<br/>白名单中?}
  
    CheckWhitelist -->|是<br/>/login等| Pass1[直接放行]
    Pass1 --> Controller[执行Controller]
  
    CheckWhitelist -->|否| GetToken[获取Token]
    GetToken --> TokenExists{Token<br/>存在?}
  
    TokenExists -->|否| Log1[记录日志:<br/>Token不存在]
    Log1 --> Return401_1[返回401:<br/>未登录]
  
    TokenExists -->|是| ParseToken[解析Token]
    ParseToken --> ParseSuccess{解析<br/>成功?}
  
    ParseSuccess -->|否| Log2[记录日志:<br/>Token格式错误]
    Log2 --> Return401_2[返回401:<br/>Token无效]
  
    ParseSuccess -->|是| CheckRedis[查询Redis]
    CheckRedis --> RedisExists{Redis中<br/>存在?}
  
    RedisExists -->|否| Log3[记录日志:<br/>Token已失效]
    Log3 --> Return401_3[返回401:<br/>登录过期]
  
    RedisExists -->|是| CheckExpire{检查<br/>过期时间}
    CheckExpire -->|<5分钟| RefreshToken[刷新Token]
    RefreshToken --> SetThreadLocal[存入ThreadLocal]
  
    CheckExpire -->|>=5分钟| SetThreadLocal
    SetThreadLocal --> UpdateRedis[更新Redis过期时间]
    UpdateRedis --> Pass2[放行请求]
    Pass2 --> Controller
  
    Controller --> ExecuteBusiness[执行业务逻辑]
    ExecuteBusiness --> HasException{是否有<br/>异常?}
  
    HasException -->|是| HandleException[异常处理]
    HandleException --> ClearThreadLocal1[清理ThreadLocal]
    ClearThreadLocal1 --> ReturnError[返回错误响应]
  
    HasException -->|否| ClearThreadLocal2[清理ThreadLocal]
    ClearThreadLocal2 --> ReturnSuccess[返回成功响应]
  
    Return401_1 --> End([结束])
    Return401_2 --> End
    Return401_3 --> End
    ReturnError --> End
    ReturnSuccess --> End
  
    style CheckWhitelist fill:#FFD700
    style TokenExists fill:#FF6347
    style ParseSuccess fill:#FF6347
    style RedisExists fill:#FF6347
    style Pass2 fill:#32CD32
    style Return401_1 fill:#DC143C
    style Return401_2 fill:#DC143C
    style Return401_3 fill:#DC143C

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇