Spring MVC 的拦截器(Interceptor)与 Java Servlet 的过滤器(Filter)类似,它主要用于拦截用户的请求并做相应的处理,通常应用在权限验证、记录请求信息的日志、判断用户是否登录等功能上。
Spring Boot 同样提供了拦截器功能,Spring Boot 拦截器的详细说明参考 “Spring基础知识(30)- Spring Boot (十一)” 的 “2. 拦截器(Interceptor)”。
本文将结合实例讲讲如何在 Spring Boot 项目里使用 Thymeleaf、JQuery、Bootstrap 的基础上,开发基于基于拦截器的 Login 实例。
后续的实例,默认在使用 Thymeleaf、JQuery、Bootstrap 的基础上(这里称为 Spring Boot 基础项目)开发,文档里将不再说明和强调。
Windows版本:Windows 10 Home (20H2)
IntelliJ IDEA (https://www.jetbrains.com/idea/download/):Community Edition for Windows 2020.1.4
Apache Maven (https://maven.apache.org/):3.8.1
注:Spring 开发环境的搭建,可以参考 “ Spring基础知识(1)- Spring简介、Spring体系结构和开发环境配置 ”。
项目实例名称:SpringbootExample03
创建步骤:
(1) 创建 Maven 项目实例 SpringbootExample03;
(2) Spring Boot Web 配置;
(3) 导入 Thymeleaf 依赖包;
(4) 配置静态资源(jQuery、Bootstrap、Images);
具体操作请参考 “Spring 系列 (2) - 在 Spring Boot 项目里使用 Thymeleaf、JQuery+Bootstrap 和国际化” 里的项目实例 SpringbootExample02 (它基本等同于一个基础项目)。
SpringbootExample03 和 SpringbootExample02 相比,SpringbootExample03 不包含国际化。
为了避免代码混淆,所以本文创建了一个新的项目实例 SpringbootExample03,本文后面代码的添加和修改都在项目实例 SpringbootExample03 上进行。
1) 定义拦截器
创建 src/main/java/com/example/interceptor/LoginInterceptor.java 文件
1 package com.example.interceptor; 2 3 import javax.servlet.http.HttpServletRequest; 4 import javax.servlet.http.HttpServletResponse; 5 import org.springframework.web.servlet.HandlerInterceptor; 6 import org.springframework.web.servlet.ModelAndView; 7 8 public class LoginInterceptor implements HandlerInterceptor { 9 10 @Override 11 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 12 Object handler) throws Exception { 13 Object loginUser = request.getSession().getAttribute("loginUser"); 14 if (loginUser == null) { 15 //request.setAttribute("message", "No authentication, try login"); 16 //request.getRequestDispatcher("/login").forward(request, response); 17 response.sendRedirect("/login"); 18 return false; 19 } else { 20 return true; 21 } 22 } 23 24 @Override 25 public void postHandle(HttpServletRequest request, HttpServletResponse response, 26 Object handler, ModelAndView modelAndView) throws Exception { 27 System.out.println("LoginInterceptor -> postHandle(): modelAndView = " + modelAndView); 28 } 29 30 @Override 31 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 32 Object handler, Exception ex) throws Exception { 33 System.out.println("LoginInterceptor -> afterCompletion()"); 34 } 35 }
2) 注册拦截器和指定拦截规则
创建 src/main/java/com/example/config/ExtendMvcConfigurer.java 文件
1 package com.example.config; 2 3 import org.springframework.context.annotation.Configuration; 4 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 5 import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; 6 import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 7 8 import com.example.interceptor.LoginInterceptor; 9 10 @Configuration 11 public class ExtendMvcConfigurer implements WebMvcConfigurer { 12 13 @Override 14 public void addViewControllers(ViewControllerRegistry registry) { 15 16 registry.addViewController("/").setViewName("home"); 17 registry.addViewController("/login.html").setViewName("login"); 18 registry.addViewController("/home.html").setViewName("home"); 19 } 20 21 @Override 22 public void addInterceptors(InterceptorRegistry registry) { 23 24 registry.addInterceptor(new LoginInterceptor()) 25 .addPathPatterns("/**") // 拦截所有请求,包括静态资源文件 26 .excludePathPatterns("/login", "/lib/**", "/css/**", 27 "/images/**", "/js/**"); // 放行登录页,登陆操作,静态资源 28 } 29 30 }
1) 创建 src/main/resources/templates/common.html 文件
1 <div th:fragment="header(var)"> 2 <meta charset="UTF-8"> 3 <title th:text="${var}">Title</title> 4 <link rel="stylesheet" th:href="@{/lib/bootstrap-4.2.1-dist/css/bootstrap.min.css}" href="/lib/bootstrap-4.2.1-dist/css/bootstrap.min.css"> 5 <script language="javascript" th:src="@{/lib/jquery/jquery-3.6.0.min.js}" src="/lib/jquery/jquery-3.6.0.min.js"></script> 6 <script language="javascript" th:src="@{/lib/bootstrap-4.2.1-dist/js/bootstrap.min.js}" src="/lib/bootstrap-4.2.1-dist/js/bootstrap.min.js"></script> 7 </div> 8 9 <div th:fragment="content-header" class="container" id="content-header-id"> 10 <nav class="navbar navbar-light bg-light"> 11 <a class="navbar-brand" href="#"> 12 <img th:src="@{/images/bootstrap-solid.svg}" src="/images/bootstrap-solid.svg" width="30" height="30" class="d-inline-block align-top" alt=""> 13 Thymeleaf Demo 14 </a> 15 16 <a class="nav-link" th:href="@{/logout}" th:if="${session.loginUser} != null">Logout</a> 17 </nav> 18 </div> 19 20 <div th:fragment="content-footer(var)" class="container" id="content-footer-id"> 21 <p th:text="${var}">Content Footer</p> 22 </div>
2) 创建 src/main/resources/templates/login.html 文件
1 <!DOCTYPE html> 2 <html lang="en" xmlns:th="http://www.thymeleaf.org"> 3 <head th:include="common::header(var='Login')"> 4 </head> 5 <body> 6 <div th:replace="common::content-header"></div> 7 8 <div class="container" id="content" th:style="'min-height: 480px;'"> 9 <h4>Login</h4> 10 11 <p> </p> 12 <div class="alert alert-info" role="alert" th:text="${message}" th:if ="${message} != null"></div> 13 14 <form method="POST"> 15 <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> 16 <div class="form-group"> 17 <label for="username">Username</label> 18 <input type="text" class="form-control" id="username" name="username" value="admin" /> 19 </div> 20 <div class="form-group"> 21 <label for="password">Password</label> 22 <input type="password" class="form-control" id="password" name="password" value="123456" /> 23 </div> 24 <button type="submit" class="btn btn-primary">Submit</button> 25 </form> 26 27 </div> 28 29 <div th:replace="common::content-footer(var='Copyright © 2020')"></div> 30 31 <script type="text/javascript"> 32 $(document).ready(function(){ 33 console.log("jQuery is running"); 34 }); 35 </script> 36 </body> 37 </html>
3) 创建 src/main/resources/templates/home.html 文件
1 <!DOCTYPE html> 2 <html lang="en" xmlns:th="http://www.thymeleaf.org"> 3 <head th:include="common::header(var='Home')"> 4 </head> 5 <body> 6 <div th:replace="common::content-header"></div> 7 8 <div class="container" id="content" th:style="'min-height: 480px;'"> 9 <h4>Home Page</h4> 10 11 <p> </p> 12 <div class="alert alert-info" role="alert" th:text="${message}" th:if ="${message} != null"></div> 13 </div> 14 15 <div th:replace="common::content-footer(var='Copyright © 2020')"></div> 16 17 <script type="text/javascript"> 18 $(document).ready(function(){ 19 console.log("jQuery is running"); 20 }); 21 </script> 22 </body> 23 </html>
4) 创建 src/main/java/com/example/controller/UserController.java 文件
1 package com.example.controller; 2 3 import javax.servlet.http.HttpServletRequest; 4 import javax.servlet.http.HttpSession; 5 6 import org.springframework.ui.Model; 7 import org.springframework.stereotype.Controller; 8 import org.springframework.web.bind.annotation.RequestMapping; 9 10 @Controller 11 public class UserController { 12 13 @RequestMapping("/login") 14 public String login(HttpServletRequest request, Model model) { 15 16 Object loginUser = request.getSession().getAttribute("loginUser"); 17 if (loginUser != null) 18 return "redirect:/home"; 19 20 if ("POST".equals(request.getMethod())) { 21 String username = request.getParameter("username"); 22 String password = request.getParameter("password"); 23 24 if ("admin".equals(username) && 25 "123456".equals(password)) { 26 27 request.getSession().setAttribute("loginUser", username); 28 29 request.getSession().setAttribute("message", "Welcome " + username); 30 return "redirect:/home"; 31 } 32 33 model.addAttribute("message", "Invalid username or password"); 34 } 35 36 return "login"; 37 } 38 39 @RequestMapping("/logout") 40 public String login(HttpSession session) { 41 session.removeAttribute("loginUser"); 42 return "redirect:/login"; 43 } 44 45 }
访问:http://localhost:9090/
CSRF 全称是 Cross-site request forgery,跨站请求伪造。
用户访问了带木马或类似脚本的网站页面后,如果电脑或手机的Web浏览器被控制,浏览器之前访问过的登录、注册、支付等网站接口就可能被非法利用。
CSRF Token 就是用来防止网站的接口被非法利用,token 是一个随机字符串,在浏览器的页面里保存为 hidden 或 cookies 值,服务端保存在 Session(或 Redis),token 有时效性,保存在 session 的,就是 session 的 timeout 值。
CSRF Token 原理,以登录页面为例,显示登录页面是一个 GET 操作,点击 "登录"按钮后,就是提交一个 POST 操作。
在 GET 操作里一个随机 token1 隐藏在登录页面里 (服务器也保存着这个 token2), POST 操作时要带上登录页面里隐藏的 token1,POST 接口会检查 token1 是不是超时了,是不是和 token2 相等,如果不满足条件,拒绝登录操作。
Spring Boot 提供的 Spring security 包 (spring-security-web) 里包含了防止 CSRF 攻击的功能,具体配置如下。
1) 导入 spring-security-web 依赖包
访问 http://www.mvnrepository.com/,查询 spring-security-web
修改 pom.xml:
1 <project ... > 2 ... 3 <dependencies> 4 ... 5 6 <dependency> 7 <groupId>org.springframework.security</groupId> 8 <artifactId>spring-security-web</artifactId> 9 </dependency> 10 11 ... 12 </dependencies> 13 14 ... 15 </project>
在IDE中项目列表 -> SpringbootExample03 -> 点击鼠标右键 -> Maven -> Reload Project
2) 配置 CsrfFilter
修改 src/main/java/com/example/config/ExtendMvcConfigurer.java 文件
1 package com.example.config; 2 3 import org.springframework.context.annotation.Configuration; 4 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 5 import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; 6 import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 7 8 import org.springframework.context.annotation.Bean; 9 import org.springframework.boot.web.servlet.FilterRegistrationBean; 10 import org.springframework.security.web.csrf.CsrfFilter; 11 import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; 12 13 import com.example.interceptor.LoginInterceptor; 14 15 @Configuration 16 public class ExtendMvcConfigurer implements WebMvcConfigurer { 17 18 @Bean 19 public FilterRegistrationBean csrfFilter() { 20 FilterRegistrationBean registration = new FilterRegistrationBean(); 21 registration.setFilter(new CsrfFilter(new HttpSessionCsrfTokenRepository())); 22 registration.addUrlPatterns("/*"); 23 return registration; 24 } 25 26 ... 27 28 }
3) 修改 src/main/resources/templates/login.html 文件,在 form 里添加一个 hidden
1 ... 2 3 <form method="POST"> 4 <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> 5 6 ... 7 </form> 8 9 ...
注:AJAX 处理 CSRF 时,使用 xhr.setRequestHeader("${_csrf.headerName}", "${_csrf.token}") 。
访问:http://localhost:9090/