【Spring Boot 快速入门】八、登录认证(二)统一拦截
目录
- 统一拦截
- Filter 过滤器
- 快速入门
- 详解
- Filter 进行登录校验
- Interceptor 拦截器
- 快速入门
- 详解
- Interceptor 进行登录校验
- 全局异常处理
统一拦截
Filter 过滤器
快速入门
Filter 是 Java Web 三大组件之一,另外两个是 Servlet 和 Listener(监听器),目前 Filter 使用较多。
过滤器(Filter)可拦截对资源的请求,执行特殊逻辑操作后再放行,能完成登录校验、统一编码处理、敏感字符处理等通用性操作,避免重复编写相同逻辑。
Filter 使用步骤:
- 定义 Filter:定义一个类,实现 FIlter 接口,并重写其所有方法
- 配置 Filter:Filter 类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启 Servlet 组件支持
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;import java.io.IOException;@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {System.out.println("DemoFilter doFilter");chain.doFilter(request, response);}@Overridepublic void init(FilterConfig filterConfig) throws ServletException {System.out.println("DemoFilter init");}@Overridepublic void destroy() {System.out.println("DemoFilter destroy");}
}
init 方法在 web 服务器启动时创建 Filter 对象后调用,仅一次,用于资源和环境准备;destroy 方法在关闭服务器时调用,仅一次,用于资源释放和环境清理;doFilter 方法在每次拦截到请求时调用,是核心方法。
详解
过滤器的执行流程:拦截到请求后,先执行放行前的逻辑(写在调用 doFilter 方法之前),然后调用 FilterChain 对象的 doFilter 方法执行放行,访问对应的 web 资源,之后会回到过滤器执行放行后的逻辑(写在 doFilter 方法之后),最后给浏览器响应数据。
过滤器拦截路径的配置方式常见的有三种:
- 拦截具体路径(如拦截 /log,只拦截该路径请求)
- 目录拦截(如拦截 /dpts/,拦截以 /dpts 开头的路径请求)
- 拦截所有(用 /* 表示)
在 web 应用中配置多个过滤器形成过滤器链。执行时按顺序依次执行每个过滤器的放行前逻辑,全部放行后访问 web 资源;web 资源访问完毕后,按相反顺序执行各过滤器放行后的逻辑,最后响应浏览器。
过滤器执行优先级的决定因素:以注解方式配置的过滤器,其执行优先级由类名的自然排序决定,类名越靠前,优先级越高。通过修改类名可改变执行顺序。
Filter 进行登录校验
登录校验基本流程:访问后台管理系统需先登录,登录成功后服务端生成 JWT 令牌返回给前端,前端后续每次请求都会携带该令牌,服务端通过过滤器校验令牌有效性,有效则放行,无效则返回错误信息。
登录校验过滤器实现思路:拦截所有请求后,先判断是否为登录请求,是则直接放行;不是则获取请求头中的 JWT 令牌,判断其是否存在,不存在则返回未登录结果;存在则解析令牌,解析失败返回未登录结果,成功则放行。
Filter 过滤器完整实现代码:
import com.alibaba.fastjson.JSONObject;
import com.example.demo.responed.Result;
import com.example.demo.utils.JwtUtils;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.util.StringUtils;import java.io.IOException;
import lombok.extern.slf4j.Slf4j;@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {HttpServletRequest req = (HttpServletRequest) request;HttpServletResponse resp = (HttpServletResponse) response;// 1.获取请求urlString url = req.getRequestURL().toString();// 2.判断请求url是否包含login,如果包含,放行if(url.contains("login")){chain.doFilter(request, response);return;}// 3.获取请求头中的令牌(token)String jwt = req.getHeader("token");// 4.判断令牌是否存在,如果不存在,返回错误结果(未登录)if(!StringUtils.hasLength(jwt)){Result error = Result.error("NOT_LOGIN");String notLogin = JSONObject.toJSONString( error);resp.getWriter().write(notLogin);return;}// 5.如果令牌存在,需要查询数据库,查询用户是否存在try {JwtUtils.parseJwt(jwt);} catch (Exception e) {e.printStackTrace();log.info("解析令牌失败");Result error = Result.error("未登录");String notLogin = JSONObject.toJSONString( error);resp.getWriter().write(notLogin);return;}// 6.放行chain.doFilter(request, response);}
}
需要在 pom.xml 文件中引入 fastjson 工具包将结果转为 json 字符串并响应:
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.83</version>
</dependency>
当未登录(没有 JWT 令牌)时,显示 NOT_LOGIN:
而当登录后(携带 JWT 令牌),显示出数据:
Interceptor 拦截器
快速入门
拦截器是 Spring 框架提供的动态拦截控制器方法执行的机制,类似过滤器,可在指定方法运行前后执行预先定义的代码,常用于登录校验等通用性操作,如校验用户是否携带合法的 gwt 令牌,决定是否放行或响应未登录信息。
Interceptor 使用步骤:
-
定义拦截器需创建类实现 handler interceptor 接口,重写其中的三个方法
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView;@Component public class LoginCheckInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("preHandle");return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("postHandle");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println("afterCompletion");} }
-
注册拦截器
import com.example.demo.interceptor.LoginCheckInterceptor; 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;@Configuration public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginCheckInterceptor loginCheckInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**") // 拦截所有请求.excludePathPatterns("/login"); // 登录接口不拦截WebMvcConfigurer.super.addInterceptors(registry);} }
详解
拦截器拦截路径配置:
拦截路径配置 | 含义 | 示例匹配情况 |
---|---|---|
/* | 仅匹配一级路径 | - 可匹配:/depts 、/emps/log - 不可匹配:/depts/1 (两级路径) |
/** | 匹配任意级路径 | - 可匹配:/depts (一级)、/depts/1 (两级)、/depts/1/2 (三级) |
/depts/* | 匹配以/depts 开头的一级子路径 | - 可匹配:/depts/1 - 不可匹配:/depts/1/2 (两级子路径)、/emps (非/depts 前缀) |
/depts/** | 匹配以/depts 开头的任意级子路径 | - 可匹配:/depts/1 、/depts/1/2 - 不可匹配:/emps (非/depts 前缀) |
拦截器执行流程:浏览器访问 web 应用时,过滤器先拦截请求,执行放行前逻辑并放行,请求进入 Spring 环境后经前端控制器 DispatcherServlet 转发,拦截器在执行 Controller 接口方法前拦截,先执行 preHandle 方法,返回 true 则放行,Controller 方法执行后执行 postHandle 和 afterCompletion方法,最后执行过滤器放行后逻辑并响应浏览器。
拦截器与过滤器的区别:
对比维度 | 过滤器(Filter) | 拦截器(Interceptor) |
---|---|---|
接口规范 | 实现javax.servlet.Filter 接口 | 实现org.springframework.web.servlet.HandlerInterceptor 接口 |
拦截范围 | 拦截所有资源(包括静态资源、非 Spring 管理的请求等) | 仅拦截进入 Spring 环境的资源(如 Controller 接口请求) |
执行时机 | 在请求进入 Servlet 容器后、Spring 处理前执行 | 在 Spring 的DispatcherServlet 转发请求后、Controller 方法执行前后执行 |
Interceptor 进行登录校验
Interceptor 拦截器与 Filter 过滤器的基本流程和实现思路一致,只有代码上的细微区分:
将 LoginCheckInterceptor 改为以下:
import com.alibaba.fastjson.JSONObject;
import com.example.demo.responed.Result;
import com.example.demo.utils.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;@Component
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求urlString url = request.getRequestURL().toString();// 2.判断请求url是否包含login,如果包含,放行if(url.contains("login")){return true;}// 3.获取请求头中的令牌(token)String jwt = request.getHeader("token");// 4.判断令牌是否存在,如果不存在,返回错误结果(未登录)if(!StringUtils.hasLength(jwt)){Result error = Result.error("NOT_LOGIN");String notLogin = JSONObject.toJSONString( error);response.getWriter().write(notLogin);return false;}// 5.如果令牌存在,需要查询数据库,查询用户是否存在try {JwtUtils.parseJwt(jwt);} catch (Exception e) {e.printStackTrace();log.info("解析令牌失败");Result error = Result.error("未登录");String notLogin = JSONObject.toJSONString( error);response.getWriter().write(notLogin);return false;}// 6.放行return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("postHandle");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println("afterCompletion");}
}
全局异常处理
目前案例中未对异常做任何处理,在三层架构调用(control 调用 service,service 调用 map)过程中,异常会逐层上抛,最终由框架返回不符合规范的错误信息 JSON 数据。
异常处理有两种方案,一是在 control 的每个方法中进行 try catch 捕获异常,但该方案操作繁琐、代码臃肿,不推荐;二是定义全局异常处理器,可捕获整个项目中的所有异常,更简单优雅,是项目推荐方式。
定义一个类,在类上添加@RestControllerAdvice
注解,代表这是全局异常处理器。在类中定义方法,方法上添加@ExceptionHandler
注解,通过其 value 属性指定要捕获的异常类型(如Exception.class
代表捕获所有异常)。方法中可输出异常堆栈信息,并返回符合规范的Result
对象封装错误信息。@RestControllerAdvice
等同于@ControllerAdvice
加@ResponseBody
,能将方法返回值转为 JSON 响应给前端
import com.example.demo.responed.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(Exception.class)public Result error(Exception e) {e.printStackTrace();return Result.error("操作失败");}
}
这样就完成了基础的全局异常处理