Spring拦截器实战:统一登录校验与异常处理

本节目标

  1. 掌握拦截器的使用, 及其原理

  2. 学习统一数据返回格式和统一异常处理的操作

  3. 了解一些Spring的源码

1. 拦截器

上个章节我们完成了强制登录的功能, 后端程序根据Session来判断用户是否登录, 但是实现方法是比较麻烦的

• 需要修改每个接口的处理逻辑

• 需要修改每个接口的返回结果

• 接口定义修改, 前端代码也需要跟着修改

1.1 拦截器快速入门

什么是拦截器?

拦截器是Spring框架提供的核心功能之一, 主要用来拦截用户的请求, 在指定方法前后, 根据业务需要执行预先设定的代码

  1. 定义拦截器

  2. 注册配置拦截器

java 复制代码
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {

 @Override
 public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws Exception {
 log.info("LoginInterceptor 目标方法执行前执行..");
 return true;
 }
 @Override
 public void postHandle(HttpServletRequest request, HttpServletResponse
response, Object handler, ModelAndView modelAndView) throws Exception {
 log.info("LoginInterceptor 目标方法执行后执行");
 }
 @Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) throws Exception {
 log.info("LoginInterceptor 视图渲染完毕后执行,最后执行");
 }
}

• preHandle()方法:目标方法执行前执行. 返回true: 继续执行后续操作; 返回false: 中断后续操作.

• postHandle()方法:目标方法执行后执行

• afterCompletion()方法:视图渲染完毕后执行,最后执行(后端开发现在几乎不涉及视图, 暂不了 解)

**注册配置拦截器:**实现WebMvcConfigurer接口,并重写addInterceptors方法

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
 //自定义的拦截器对象
 @Autowired
 private LoginInterceptor loginInterceptor;
 @Override
 public void addInterceptors(InterceptorRegistry registry) {
 //注册自定义拦截器对象
 registry.addInterceptor(loginInterceptor)
 .addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所
有请求)
 }
}

可以看到preHandle 方法执行之后就放行了, 开始执行目标方法, 目标方法执行完成之后执行

postHandle和afterCompletion⽅法.

我们把拦截器中preHandle方法的返回值改为false, 再观察运行结果

1.2 拦截器详解

拦截器的入门程序完成之后,接下来我们来介绍拦截器的使用细节。拦截器的使用细节我们主要介绍两个部分:

  1. 拦截器的拦截路径配置

  2. 拦截器实现原理

1.2.1 拦截路径

拦截路径是指我们定义的这个拦截器, 对哪些请求生效

我们在注册配置拦截器的时候, 通过 addPathPatterns() 方法指定要拦截哪些请求. 也可以通过

excludePathPatterns() 指定不拦截哪些请求

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
 //自定义的拦截器对象
 @Autowired
 private LoginInterceptor loginInterceptor;
 @Override
 public void addInterceptors(InterceptorRegistry registry) {
 //注册自定义拦截器对象
 registry.addInterceptor(loginInterceptor)
 .addPathPatterns("/**")
 .excludePathPatterns("/user/login");//设置拦截器拦截的请求路径
(/** 表示拦截所有请求)
 }
}

在拦截器中除了可以设置 /** 拦截所有资源外,还有一些常见拦截路径设置

1.2.2 拦截器执行流程

有了拦截器之后,会在调⽤Controller之前进⾏相应的业务处理,执⾏的流程如下图

  1. 添加拦截器后, 执行Controller的方法之前, 请求会先被拦截器拦截住. 执行 preHandle() 方法, 这个方法需要返回一个布尔类型的值. 如果返回true, 就表示放行本次操作, 继续访问controller中的 方法. 如果返回false,则不会放行(controller中的方法也不会执行).

  2. controller当中的方法执行完毕后,再回过来执行 postHandle() 这个方法以及 afterCompletion() 方法,执行完毕之后,最终给浏览器响应数据.

1.3 登录校验

学习拦截器的基本操作之后,接下来我们需要完成最后一步操作:通过拦截器来完成图书管理系统中的登录校验功能

1.3.1 定义拦截器

从session中获取用户信息, 如果session中不存在, 则返回false,并设置http状态码为401, 否则返回true

java 复制代码
public class LoginInterceptor implements HandlerInterceptor {
 @Override
 public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws Exception {
 HttpSession session = request.getSession(false);
 if (session != null &&
session.getAttribute(Constants.SESSION_USER_KEY) != null) {
 return true;
 }
 response.setStatus(401);
 return false;
 }
}

1.3.2 注册配置拦截器

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
 //自定义的拦截器对象
 @Autowired
 private LoginInterceptor loginInterceptor;
 @Override
 public void addInterceptors(InterceptorRegistry registry) {
 //注册自定义拦截器对象
 registry.addInterceptor(loginInterceptor)
 .addPathPatterns("/**")//设置拦截器拦截的请求路径(/**表示拦截所有请
求)
 .excludePathPatterns("/user/login")//设置拦截器排除拦截的路径
 .excludePathPatterns("/**/*.js") //排除前端静态资源
 .excludePathPatterns("/**/*.css")
 .excludePathPatterns("/**/*.png")
 .excludePathPatterns("/**/*.html");
 }
}

也可以改成

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
 //自定义的拦截器对象
 @Autowired
 private LoginInterceptor loginInterceptor;
 private List<String> excludePaths = Arrays.asList(
 "/user/login",
 "/**/*.js",
 "/**/*.css",
 "/**/*.png",
 "/**/*.html"
 );
 @Override
 public void addInterceptors(InterceptorRegistry registry) {
 //注册自定义拦截器对象
 registry.addInterceptor(loginInterceptor)
 .addPathPatterns("/**")//设置拦截器拦截的请求路径(/** 表示拦截所有请
求)
 .excludePathPatterns(excludePaths);//设置拦截器排除拦截的路径
 }
}

删除之前的登录校验代码

java 复制代码
@RequestMapping("/getListByPage")
public Result getListByPage(PageRequest pageRequest, HttpSession session) {
log.info("获取图书列表, pageRequest:{}", pageRequest);
// //判断用户是否登录
// if (session.getAttribute(Constants.SESSION_USER_KEY)==null){
// return Result.unlogin();
// }
// UserInfo userInfo = (UserInfo)
session.getAttribute(Constants.SESSION_USER_KEY);
// if (userInfo==null || userInfo.getId()<0 ||
"".equals(userInfo.getUserName())){
// return Result.unlogin();
// }
 //用户登录, 返回图书列表
 PageResult<BookInfo> pageResult =
bookService.getBookListByPage(pageRequest);
 log.info("获取图书列表222, pageRequest:{}", pageResult);
 return Result.success(pageResult);
}

1.4.3 适配器模式

HandlerAdapter 在 Spring MVC 中使用了适配器模式

适配器模式定义

适配器模式, 也叫包装器模式. 将一个类的接口,转换成客户期望的另一个接口, 适配器让原本接口不兼容的类可以合作无间

简单来说就是目标类不能直接使用, 通过一个新类进行包装一下, 适配调用方使用. 把两个不兼容的接口通过一定的方式使之兼容

比如下面两个接口, 本身是不兼容的(参数类型不一样, 参数个数不一样等等)

java 复制代码
interface Slf4jApi{
 void log(String message);
}
/**
 * log4j 接口
 */
class Log4j{
 void log4jLog(String message){
 System.out.println("Log4j打印:"+message);
 }
}
/**
 * slf4j和log4j适配器
 */
class Slf4jLog4JAdapter implements Slf4jApi{
 private Log4j log4j;
 public Slf4jLog4JAdapter(Log4j log4j) {
 this.log4j = log4j;
 }
 @Override
 public void log(String message) {
 log4j.log4jLog(message);
 }
}
/**
 * 客户端调用
 */
public class Slf4jDemo {
 public static void main(String[] args) {
 Slf4jApi slf4jApi = new Slf4jLog4JAdapter(new Log4j());
 slf4jApi.log("使用slf4j打印日志");
 }
}

2. 统一数据返回格式

强制登录案例中, 我们共做了两部分工作

  1. 通过Session来判断用户是否登录

  2. 对后端返回数据进行封装, 告知前端处理的结果

java 复制代码
@Data
public class Result<T> {
    private ResultCodeEnum code;  //-1 未登录   200正常 -2出错
    private String errMsg;
    private T data;

    public static <T> Result success(T data){
        Result result=new Result();
        result.setCode(ResultCodeEnum.SUCCESS);
        result.setErrMsg("");
        result.setData(data);
        return result;
    }

    public static <T> Result unlogin(){
        Result result = new Result();
        result.setCode(ResultCodeEnum.UNLOGIN);
        result.setErrMsg("用户未登录");
        return result;
    }

    public static <T> Result fail(String errMsg, T data){
        Result result = new Result();
        result.setCode(ResultCodeEnum.FAIL);
        result.setErrMsg(errMsg);
        result.setData(data);
        return result;
    }}

后端逻辑处理

java 复制代码
@RequestMapping("/getListByPage")
public Result getListByPage(PageRequest pageRequest) {
 log.info("获取图书列表, pageRequest:{}", pageRequest);
 //用户登录, 返回图书列表
 PageResult<BookInfo> pageResult =
bookService.getBookListByPage(pageRequest);
 log.info("获取图书列表222, pageRequest:{}", pageResult);
 return Result.success(pageResult);
}

2.1 快速入门

统一的数据返回格式使用 @ControllerAdvice 和 ResponseBodyAdvice 的方式实现 @ControllerAdvice 表示控制器通知类

java 复制代码
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
 @Override
 public boolean supports(MethodParameter returnType, Class converterType) {
 return true;
 }

 @Override
 public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest
request, ServerHttpResponse response) {
 return Result.success(body);
 }
}

• supports方法: 判断是否要执行beforeBodyWrite方法. true为执行, false不执行. 通过该方法可以 选择哪些类或哪些方法的response要进行处理, 其他的不进行处理.

2.2 存在问题

问题现象:

我们继续测试修改图书的接口

结果显示, 发生内部错误

查看数据库, 发现数据操作成功

多测试⼏种不同的返回结果,发现只有返回结果为String类型时才有这种错误发⽣.

解决方案:

java 复制代码
@Slf4j
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice{
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return false;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof String){
            return objectMapper.writeValueAsString(Result.success(body));
        }
        return Result.success(body);
    }
}

2.3 案例代码修改

java 复制代码
@SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof Result){
            return body;
        }
        if (body instanceof String){
            return objectMapper.writeValueAsString(Result.success(body));
        }
        return Result.success(body);
    }

2.4 优点

  1. 方便前端程序员更好的接收和解析后端数据接口返回的数据

  2. 降低前端程序员和后端程序员的沟通成本, 按照某个格式实现就可以了, 因为所有接口都是这样返回的.

  3. 有利于项目统一数据的维护和修改.

  4. 有利于后端技术部门的统一规范的标准制定, 不会出现稀奇古怪的返回内容.

3. 统一异常处理

java 复制代码
@ControllerAdvice
@ResponseBody
public class ErrorAdvice {
 @ExceptionHandler
 public Object handler(Exception e) {
 return Result.fail(e.getMessage());
 }
}

以上代码表示,如果代码出现Exception异常(包括Exception的子类), 就返回一个 Result的对象, Result 对象的设置参考 Result.fail(e.getMessage())

java 复制代码
public static <T> Result fail(String errMsg){
        Result result = new Result();
        result.setCode(ResultCodeEnum.FAIL);
        result.setErrMsg(errMsg);
        return result;
    }

我们可以针对不同的异常, 返回不同的结果

java 复制代码
@ControllerAdvice
@ResponseBody
@Slf4j
public class ErrorAdvice {
    @Autowired
    private Result result;
    @ExceptionHandler
    public Result handler(Exception e){
        log.error("发生异常,e:", e);
        return Result.fail("内部错误,请联系管理员");
    }

    @ExceptionHandler
    public Result handler(NullPointerException e){
        log.error("发生异常,e:", e);
        return Result.fail("发生空指针异常,请联系管理员");
    }

    @ExceptionHandler
    public Result handler(IndexOutOfBoundsException e){
        log.error("发生异常,e:", e);
        return Result.fail("数组越界异常,请联系管理员");
    }
}

模拟制造异常:

java 复制代码
@RequestMapping("/test")
@RestController
public class TestController {
 @RequestMapping("/t1")
 public String t1(){
 return "t1";
 }
 @RequestMapping("/t2")
 public boolean t2(){
 int a = 10/0; //抛出ArithmeticException
 return true;
 }
 @RequestMapping("/t3")
 public Integer t3(){
 String a =null;
 System.out.println(a.length()); //抛出NullPointerException
 return 200;
 }
}

当有多个异常通知时,匹配顺序为当前类及其子类向上依次匹配

5.1 登录页面

登录界面没有拦截, 只是返回结果发生了变化, 所以只需要根据返回结果修改对应代码即可

1.登录结果代码修改

java 复制代码
function login() {
 $.ajax({
 type: "post",
 url: "/user/login",
 data: {
 name: $("#userName").val(),
 password: $("#password").val()
 },
 success: function (result) {
 console.log(result);
 if (result.status=="SUCCESS" && result.data==true) {
 location.href = "book_list.html";
 } else {
 alert("账号或密码不正确!");
 }
 }
 });
}

2,返回结果改变,前端代码需要进行改变(不止上述一个)

总结

本章节主要介绍了SpringBoot 对一些统一功能的处理支持.

  1. 拦截器的实现主要分两部分: 1. 定义拦截器(实现HandlerInterceptor 接口) 2. 配置拦截器

  2. 统一数据返回格式通过@ControllerAdvice + ResponseBodyAdvice 来实现

  3. 统一异常处理使用@ControllerAdvice + @ExceptionHandler 来实现, 并且可以分异常来处理

  4. 学习了DispatcherServlet的一些源码

相关推荐
弹简特1 个月前
【JavaEE24-后端部分】 从“手动锁门”到“保安统一站岗”:Spring Boot 拦截器轻松搞定登录校验
java·spring boot·拦截器
Mr Aokey1 个月前
快速入门 Spring Boot 拦截器、统一响应格式和全局异常处理
java·开发语言·aop·拦截器
独断万古他化1 个月前
【抽奖系统开发实战】Spring Boot 项目的用户模块设计:注册登录、权限管控与敏感数据加密
java·spring boot·redis·后端·mvc·jwt·拦截器
树码小子2 个月前
统一功能处理(1)拦截器
spring·拦截器
神云瑟瑟3 个月前
spring boot拦截器获取requestBody的最佳实践
spring boot·拦截器·requestbody
正儿八经的少年3 个月前
拦截器和切面(AOP)
aop·拦截器
€8113 个月前
Java入门级教程26——序列化和反序列化,Redis存储Java对象、查询数据库与实现多消费者消息队列
java·拦截器·序列化和反序列化·数据库查询·redis存储java对象·多消费者消息队列
小肖爱笑不爱笑3 个月前
登录认证-会话技术、JWT令牌、过滤器Filter、拦截器Interceptor
java·开发语言·过滤器·拦截器·登录认证
清风徐来QCQ4 个月前
SpringMvc(Interceptor,Filter)
过滤器·拦截器