乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 SpringMVC概述与DispatcherServlet

    • 本章导读:Spring MVC概述与DispatcherServlet
    • Spring MVC 是什么
    • MVC 设计模式
    • 前端控制器模式
    • DispatcherServlet
    • 核心组件协作
  • 第2章 控制器与请求映射

    • 本章导读:控制器与请求映射
    • Controller
    • RestController
    • RequestMapping
    • GetMapping
    • PostMapping
    • PutMapping
    • DeleteMapping
    • PathVariable
    • RESTful
    • 请求映射原理
  • 第3章 请求参数获取与转换

    • 本章导读:请求参数获取与转换
    • RequestParam
    • RequestBody
    • RequestHeader
    • CookieValue
    • Model
    • ModelAttribute
    • 数据绑定原理
    • 数据校验
  • 第4章 响应数据与视图解析

    • 本章导读:响应数据与视图解析
    • ResponseBody
    • ResponseEntity
    • ModelAndView
    • ViewResolver
    • HttpMessageConverter
    • forward与redirect
  • 第5章 拦截器过滤器与跨域

    • 本章导读:拦截器、过滤器与跨域
    • HandlerInterceptor
    • WebMvcConfigurer
    • CrossOrigin
    • 登录验证实战
  • 第6章 文件上传与异常处理

    • 本章导读:文件上传与异常处理
    • MultipartFile
    • 文件下载
    • ExceptionHandler
    • ControllerAdvice
    • RestControllerAdvice
    • ResponseStatus
  • 第7章 高级特性与最佳实践

    • 本章导读:高级特性与最佳实践
    • SessionAttributes
    • SessionAttribute
    • RedirectAttributes
    • MockMvc测试
    • 国际化
    • 最佳实践
  • 第8章 扩展与异步机制

    • 本章导读:扩展与异步机制
    • 异步请求处理
    • 自定义参数解析器
    • 内容协商

登录验证实战

本章是 Spring MVC 教程中"拦截器、过滤器与跨域"章节的端到端实战。在理解了 HandlerInterceptor 的 preHandle / postHandle / afterCompletion 三个回调点后,本章通过一个完整的登录验证系统,展示拦截器在真实项目中的落地方式。这不是机制讲解,而是"从需求到代码"的完整闭环。


定义与作用

登录验证是 Web 应用最基础的访问控制机制。它的核心逻辑是:对于需要保护的资源,检查当前用户是否已登录;未登录则阻止访问,并重定向到登录页。

在 Spring MVC 中,登录验证通常由 HandlerInterceptor 实现,而非 Servlet Filter。原因在于:

  • Interceptor 能访问 Spring 容器:可以注入 Service、读取配置文件,与 Spring 生态无缝集成
  • Interceptor 能获取 Controller 信息:通过 HandlerMethod 知道即将访问的是哪个类的哪个方法,可以做细粒度控制(如某些方法免登录)
  • Interceptor 与 Spring MVC 生命周期绑定:preHandle 在 Controller 之前执行,postHandle 在视图渲染之前执行,afterCompletion 在请求结束后执行,天然适合"检查 → 放行/拦截 → 清理"的流程

本实战的场景是:员工管理系统的大部分页面需要登录后才能访问,但登录页、注册页、静态资源(CSS/JS/图片)必须对未登录用户开放。

生活类比:办公大楼的门禁系统

想象一栋办公大楼:

  • Interceptor 是大堂的门禁闸机:每个人进入办公区(Controller)之前,必须经过闸机检查。闸机刷员工卡(检查 Session 中的登录用户),合法则开门放行,非法则引导到访客登记处(登录页)。
  • Filter 是大楼外围的围墙:围墙在闸机更外层,负责检查是否携带危险物品(编码过滤、XSS 防护)。围墙不知道你是去 3 楼还是 5 楼,它只负责"能不能进大楼"。
  • 登录页是访客登记处:未带员工卡的人被引导到这里,填写信息(输入用户名密码),验证通过后领取临时门禁卡(写入 Session)。
  • 登出是交还门禁卡:离开大楼时,在闸机处刷卡注销(清除 Session),下次再来需要重新登记。

这个实战的关键在于:闸机(Interceptor)知道办公区布局(HandlerMapping),能精确控制哪些楼层要刷卡、哪些楼层不用刷。


核心原理

登录验证拦截流程

流程解读:

  1. 首次访问(未登录):用户访问 /home,LoginInterceptor 的 preHandle 检查 Session,发现没有 user 属性,返回 false 并发送重定向到 /login。
  2. 登录提交:用户在登录页填写表单,POST 到 /login。LoginController 验证成功后,将 User 对象写入 Session,然后重定向到 /home。
  3. 再次访问(已登录):用户再次访问 /home,Interceptor 检查 Session 发现 user 存在,返回 true 放行,HomeController 正常执行。
  4. 登出:用户访问 /logout,LoginController 调用 session.invalidate() 清除整个 Session,然后重定向到登录页。

完整示例

场景

飞翔科技的员工管理系统已上线运行。CTO 大翔发现,未登录用户直接输入 URL(如 /employees)就能访问敏感数据,存在严重安全隐患。他要求:

  1. 所有功能页面必须登录后才能访问
  2. 登录页(/login)、注册页(/register)、静态资源(/css/**、/js/**、/images/**)不能拦截
  3. 登录验证用 Session 机制,登出后 Session 完全清除
  4. 未登录用户访问受保护页面时,自动跳转到登录页,登录成功后自动回到原页面

架构师白歌设计"Interceptor + Session + 重定向"方案。小崔负责 LoginInterceptor 和 LoginController,黄俪负责登录页面和首页,李眉负责 Session 超时配置。

拦截路径配置表

路径是否拦截说明
/login❌ 不拦截登录页本身不能要求登录
/register❌ 不拦截注册页同理
/css/**❌ 不拦截静态资源,无需登录
/js/**❌ 不拦截静态资源,无需登录
/images/**❌ 不拦截静态资源,无需登录
/api/public/**❌ 不拦截公开 API(如验证码)
/**✅ 拦截其余所有路径都需要登录

小崔的 LoginInterceptor

// LoginInterceptor.java
package com.feixiang.web.interceptor;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@Component
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 在 Controller 执行前检查登录状态
     */
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        
        HttpSession session = request.getSession();
        Object user = session.getAttribute("currentUser");
        
        if (user == null) {
            // 未登录:保存当前请求 URL,登录成功后跳回来
            String requestUri = request.getRequestURI();
            String queryString = request.getQueryString();
            if (queryString != null) {
                requestUri += "?" + queryString;
            }
            session.setAttribute("redirectAfterLogin", requestUri);
            
            // 重定向到登录页
            response.sendRedirect(request.getContextPath() + "/login");
            return false;  // 拦截,不执行后续 Controller
        }
        
        // 已登录:放行
        return true;
    }
}

代码分析:

  • preHandle 返回 true 表示放行,返回 false 表示拦截
  • 未登录时,将用户原本想访问的 URL 存入 Session(key 为 redirectAfterLogin),登录成功后读取并跳转
  • response.sendRedirect() 发送 302 重定向,浏览器地址栏会变为 /login
  • 注意 request.getContextPath(),防止应用部署在子路径时重定向地址错误

小崔的 WebMvcConfigurer 配置

// WebConfig.java
package com.feixiang.web.config;

import com.feixiang.web.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.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")                    // 拦截所有路径
                .excludePathPatterns("/login")             // 排除登录页
                .excludePathPatterns("/register")          // 排除注册页
                .excludePathPatterns("/css/**")          // 排除 CSS
                .excludePathPatterns("/js/**")             // 排除 JS
                .excludePathPatterns("/images/**")         // 排除图片
                .excludePathPatterns("/api/public/**");  // 排除公开 API
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 配置静态资源映射,确保 /css/style.css 能正确找到文件
        registry.addResourceHandler("/css/**")
                .addResourceLocations("classpath:/static/css/");
        registry.addResourceHandler("/js/**")
                .addResourceLocations("classpath:/static/js/");
        registry.addResourceHandler("/images/**")
                .addResourceLocations("classpath:/static/images/");
    }
}

配置分析:

  • addPathPatterns("/**") 表示拦截所有请求
  • excludePathPatterns 按顺序排除不需要拦截的路径
  • 静态资源映射确保 /css/style.css 被 DispatcherServlet 正确处理(或绕过 DispatcherServlet 由容器直接服务)
  • 拦截器注册顺序很重要:如果有多个 Interceptor,先注册的先执行 preHandle,后执行 postHandle/afterCompletion

小崔的 LoginController

// LoginController.java
package com.feixiang.web;

import com.feixiang.model.User;
import com.feixiang.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.servlet.http.HttpSession;

@Controller
public class LoginController {

    @Autowired
    private UserService userService;

    /**
     * 显示登录页
     */
    @GetMapping("/login")
    public String loginPage() {
        return "login";  // 返回 login.html 视图
    }

    /**
     * 处理登录提交
     */
    @PostMapping("/login")
    public String doLogin(@RequestParam String username,
                          @RequestParam String password,
                          HttpSession session,
                          Model model) {
        
        // 验证用户名密码
        User user = userService.authenticate(username, password);
        
        if (user == null) {
            // 登录失败:回到登录页,显示错误信息
            model.addAttribute("error", "用户名或密码错误");
            return "login";
        }
        
        // 登录成功:写入 Session
        session.setAttribute("currentUser", user);
        
        // 如果有登录前想访问的页面,跳转回去;否则去首页
        String redirectUrl = (String) session.getAttribute("redirectAfterLogin");
        if (redirectUrl != null) {
            session.removeAttribute("redirectAfterLogin");
            return "redirect:" + redirectUrl;
        }
        
        return "redirect:/home";
    }

    /**
     * 登出
     */
    @GetMapping("/logout")
    public String logout(HttpSession session) {
        // 清除整个 Session,包括 currentUser 和 redirectAfterLogin
        session.invalidate();
        return "redirect:/login";
    }
}

代码分析:

  • userService.authenticate() 封装了用户名密码验证逻辑(本教程不展开 Service 层)
  • 登录失败时返回 "login"(逻辑视图名),配合 model.addAttribute("error", ...) 在页面显示错误
  • 登录成功时优先跳转到 redirectAfterLogin(用户原本想访问的页面),提升用户体验
  • session.invalidate() 比 session.removeAttribute("currentUser") 更彻底,清除所有 Session 数据防止残留

黄俪的登录页面

<!-- login.html (Thymeleaf) -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>登录 - 员工管理系统</title>
    <link rel="stylesheet" th:href="@{/css/login.css}"/>
</head>
<body>
    <div class="login-box">
        <h1>员工管理系统</h1>
        
        <!-- 错误提示 -->
        <div th:if="${error}" class="error-msg" th:text="${error}">
            用户名或密码错误
        </div>
        
        <form th:action="@{/login}" method="post">
            <div class="form-group">
                <label>用户名</label>
                <input type="text" name="username" required/>
            </div>
            <div class="form-group">
                <label>密码</label>
                <input type="password" name="password" required/>
            </div>
            <button type="submit">登录</button>
        </form>
        
        <p class="register-link">
            还没有账号?<a th:href="@{/register}">立即注册</a>
        </p>
    </div>
</body>
</html>

黄俪的设计说明:

  • 登录页引用了 /css/login.css,由于 WebConfig 中排除了 /css/** 的拦截,未登录用户也能正常加载样式
  • 表单提交到 /login,POST 请求同样被 Interceptor 排除(因为 excludePathPatterns 包含 /login)
  • 错误提示使用 Thymeleaf 的条件渲染 th:if="${error}",登录失败时才显示

李眉的 Session 配置

# application.yml
server:
  servlet:
    session:
      timeout: 30m  # Session 30 分钟无操作后过期

spring:
  mvc:
    static-path-pattern: /static/**

李眉的运维笔记:

  • 生产环境建议 Session 超时设为 30 分钟,平衡安全性与用户体验
  • 如果部署集群,需要配置 Spring Session + Redis,实现 Session 共享(否则负载均衡后用户可能频繁掉线)
  • 登录密码必须加密存储(BCrypt),本示例为简化未展示,生产环境严禁明文比对

易错场景与面试考点

误区一:Interceptor 里直接返回视图名

错误代码:

@Override
public boolean preHandle(HttpServletRequest request, 
                         HttpServletResponse response, 
                         Object handler) throws Exception {
    if (session.getAttribute("user") == null) {
        // ❌ 错误:Interceptor 里不能返回视图名,只能操作 response
        request.setAttribute("error", "请登录");
        request.getRequestDispatcher("/login").forward(request, response);
        return false;
    }
    return true;
}

问题:虽然 forward 能工作,但 Interceptor 的职责是"拦截或放行",不是"渲染视图"。如果登录页本身需要 Controller 准备数据(如显示验证码),forward 会跳过 Controller 直接找视图,导致页面数据缺失。

纠正:Interceptor 只负责重定向到登录页,登录页的数据准备由 LoginController 负责:

// Interceptor:只做判断和重定向
response.sendRedirect("/login");
return false;

// LoginController:负责准备登录页所需的所有数据
@GetMapping("/login")
public String loginPage(Model model) {
    model.addAttribute("captcha", generateCaptcha());
    return "login";
}

误区二:excludePathPatterns 配置遗漏

错误配置:

registry.addInterceptor(loginInterceptor)
        .addPathPatterns("/**")
        .excludePathPatterns("/login");
// ❌ 遗漏了 /css/**、/js/**、/images/**

后果:未登录用户访问登录页时,页面样式全部丢失(因为 /css/login.css 被拦截,重定向到 /login,形成循环)。

纠正:必须完整排除所有静态资源和公开路径。建议按功能模块分组管理排除规则:

private static final String[] EXCLUDE_PATHS = {
    "/login", "/register", "/logout",
    "/css/**", "/js/**", "/images/**", "/fonts/**",
    "/api/public/**", "/error"
};

registry.addInterceptor(loginInterceptor)
        .addPathPatterns("/**")
        .excludePathPatterns(EXCLUDE_PATHS);

误区三:Session 属性名不一致

错误代码:

// LoginController 里写入
session.setAttribute("user", user);

// LoginInterceptor 里读取
Object user = session.getAttribute("currentUser");  // ❌ key 不一致,永远读不到

纠正:定义常量统一管理 Session key:

public final class SessionKeys {
    public static final String CURRENT_USER = "currentUser";
    public static final String REDIRECT_AFTER_LOGIN = "redirectAfterLogin";
    
    private SessionKeys() {}  // 禁止实例化
}

面试高频:Interceptor 和 Filter 都能做登录验证,有什么区别?该选哪个?

标准回答:

维度Filter(Servlet 规范)Interceptor(Spring MVC)
执行时机DispatcherServlet 之前DispatcherServlet 之后,Controller 之前
能否访问 Spring 容器不能直接注入 Bean可以 @Autowired 注入 Service
能否获取 Controller 信息不能,只能看到 URL能,通过 HandlerMethod 获取类名、方法名、注解
配置方式web.xml 或 @WebFilterWebMvcConfigurer.addInterceptors()
执行链控制通过 FilterChain 控制通过返回值 true/false 控制

选择建议:

  • 如果验证逻辑需要调用 Spring 管理的 Service(如查询数据库验证 Token),用 Interceptor
  • 如果验证逻辑是纯 Servlet 层操作(如检查请求头签名、IP 白名单),用 Filter 性能更好
  • 登录验证通常需要查询用户表、权限表,属于业务逻辑,因此推荐用 Interceptor

小结

登录验证实战展示了 HandlerInterceptor 在真实项目中的完整落地路径:Interceptor 检查 Session → 未登录则重定向 → LoginController 验证并写入 Session → 已登录则放行 → Logout 清除 Session。整个流程中,Interceptor 负责"把关",Controller 负责"办事",Session 负责"记住状态",三者分工明确。

实现时务必注意三个易错点:excludePathPatterns 必须完整排除静态资源和公开路径;Interceptor 里只做重定向,不做视图渲染;Session key 必须全局统一。同时,生产环境还需要考虑 Session 超时、集群共享、密码加密等运维问题。

本章与全局的关系:本章是 HandlerInterceptor 机制的实战落地。下一章"过滤器(Filter)与跨域处理"将对比讲解 Servlet Filter 的使用场景,以及 CORS 跨域配置的实现方式。

上一页
CrossOrigin