在我们写SpringBoot项目的时候,有些功能模块几乎每个项目都是一样,这些功能模块有:
1.统一用户登录权限验证;
2.统一数据格式返回;
3.统一异常处理;
统一用户登录权限验证
在我们利用Spring AOP来做统一登录验证的时候会遇到两个问题:
1.没有办法获取到HTTPSession对象;
2.很难做到只对某一部分拦截,对某一部分不拦截;
Spring拦截器
对于上面的问题可以使用Spring拦截器来解决,Spring拦截器HandlerInterceptor的实现分为一下两个步骤:
1. 创建自定义拦截器(判断用户是否登陆):该拦截器必须实现HandlerInterceptor并重写preHandle(执行具体方法之前的预处理)方法。
/*
* 登录拦截器
* */
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("userinfo")!=null){
//说明已经登录
return true;
}
response.setStatus(401);
return false;
}
}
2.配置拦截器和拦截规则
/*
* 全局配置文件
* */
@Configuration
public class AppConfig implements WebMvcConfigurer {
//配置拦截器和拦截规则
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")//拦截所有请求
.excludePathPatterns("/**/*.html")//排除所有的html文件
.excludePathPatterns("/**/*.css")//排除所有的css文件
.excludePathPatterns("/**/*.js")//排除所有的js文件
.excludePathPatterns("/user/login");//排除登录接口
}
}
结果:
因为拦截了.jpg文件,随意页面中的图片加载不出来,下面是我排除.jpg文件的接口以后的效果:
下面看一个具体的用户登录权限验证的操作:
首先用户是通过输入网址到达某个页面,这个页面可能是系统内部的某个页面(不是登录页面),这个时候因为没有登陆,所以就不能访问:
一:创建两个前端页面
- index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1> index 页面</h1>
</body>
</html>
- login.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>登录页面</h1>
</body>
</html>
二:写一个拦截器,拦截index页面,不拦截login页面
public class LoginIntercept implements HandlerInterceptor {
/*
* 返回true表示拦截判断通过,可以访问后面的接口
* 如果返回false表示拦截未通过,直接返回结果给前端
* */
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.得到HttpSession对象
HttpSession session = request.getSession(false);
if(session!=null&&session.getAttribute("userinfo")!=null){
//表示已经登录
return true;
}
//执行到此行代码表示未登录,未登录就跳转到登录页面
return false;
}
}
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginIntercept())
.addPathPatterns("/**")//拦截所有的url
.excludePathPatterns("/user/login")//不拦截登录接口
.excludePathPatterns("/user/reg")//不拦截注册接口
.excludePathPatterns("/login.html")//不拦截登录页面
.excludePathPatterns("/reg.html")//不拦截注册页面
.excludePathPatterns("/**/*.js")//不拦截所有的js信息
.excludePathPatterns("/**/*.css")//不拦截所有的css信息
.excludePathPatterns("/**/*.jpg");//不拦截所有的图片信息
}
}
三:验证
四:让未登录的用户跳转到登录页面
输入http://localhost:8080/index.html以后因为没有登陆,它会自动的跳转到登录页面:
五:在url中输入session信息(输入以后相当于就登录了)
没有输入session信息之前的状态(访问):
我在浏览器输入框中输入localhost:8080/user/index,如果是登录状态,它就会显示Hello,index,但是现在我是未登录状态(也就是浏览器中没有我的登录信息——session) ,因为之前重定向到了登录页面,所以它会让我先登录:
在这个页面中,会让我输入登录信息,验证通过后浏览器就会自动的保存我的session,然后我就可以访问其他页面了 ,下面是我没有输入正确session的状态:
下面是我输入正确session之后的状态:
然后我在访问其他页面:
当我的登录信息正确以后,拦截器就不拦了,上面整个过程就是用户统一登录的验证。
Spring拦截器的原理
之前我们没有设置拦截器的时候,用户请求数据的流程是下面这样的:
添加拦截器以后,用户请求数据的流程:
也就是说在我们请求数据的时候,所有的请求否要通过拦截器这个关卡,如果拦截器返回false,请求就不能通过,返回true才能访问到要访问的数据。
关于统一访问前缀的添加
在我们使用拦截器拦截目标文件的时候,可以在相应的url地址前面加上前缀:
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
//在所有的请求地址前面加上api前缀
configurer.addPathPrefix("api",c->true);
}
代码里面的c表示所有的controller
例如:在所有的请求地址前面加上api前缀
没加之前:
加了之后:
再来看一个例子,深度理解拦截器:
拦截器:
@Component
public class Interceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("在controller控制器之前执行");
String requestURI = request.getRequestURI();
//判断是否是需要拦截的url
// 判断是否需要拦截的请求
if (needIntercept(requestURI)) {
// 需要拦截的逻辑处理
System.out.println("拦截器:拦截请求 " + requestURI);
return false; // 返回 false 表示拦截该请求
}
// 不需要拦截的请求继续执行
System.out.println("拦截器:放行请求 " + requestURI);
return true; // 返回 true 表示放行该请求
}
private boolean needIntercept(String requestPath) {
// 根据实际业务需求,判断是否需要拦截的请求
// 可以根据 URL 路径、请求方法、请求参数等进行判断
if (requestPath.startsWith("/list")) {
return true; // 需要拦截以 "/list" 开头的请求
}
return false; // 其他请求不需要拦截
}
}
拦截范围:
@Configuration
public class ConfigInterceptor implements WebMvcConfigurer {
@Autowired
private Interceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//添加拦截器并配置拦截规则
registry.addInterceptor(interceptor)
.addPathPatterns("/**"); //拦截所有请求
}
}
打开浏览器访问登录页面:
控制台:
输入登录账号和密码以后:
当我在拦截范围/规则里面将拦截规则设定为:除了不拦截html页面,其他的所有文件都要拦截以后:
//添加拦截器并配置拦截规则
registry.addInterceptor(interceptor)
.addPathPatterns("/**") //拦截所有请求
.excludePathPatterns("/**/*.html"); //不拦截所有的html文件
登录login.html页面控制台输出的日志:
可以发现上面控制台中所有有关.html文件的日志都没有输出。
总结:拦截器和拦截规则在Spring MVC中共同使用。拦截器定义了拦截器的逻辑和处理过程,而拦截规则用于指定拦截器的应用范围,也就是那些请求应该被拦截。拦截逻辑在拦截器里面,当拦截器返回true时就不拦截,返回false就要拦截。
统一的异常处理
在我们写程序的时候,难免会遇到异常,这个异常可能是我们自己导致的也有可能是用户提交的数据导致的,虽然异常还可以try-catch一下处理一下,但是异常多了以后不可能每一个地方都去处理,而且在Spring事务中try-catch会导致事务不能回滚。为了解决这一普遍现象造成的问题,这里就可以Spring里面用统一异常处理机制来处理。
统一异常处理机制:
1.给异常处理类加上@controllerAdvice注解或者@RestControllerAdvice注解。
@controllerAdvice表示它是一个全局的异常处理类,而@RestControllerAdvice是一个组合注解,它相当于@controllerAdervice+@ResponseBody,使用了这个注解以后,可以不再类上面加@ResponseBody注解。
@RestControllerAdvice//当前针对controller的通知类
public class MyExceptionAdervice {
}
2.在方法上面加上@ExceptionHandler(XXX.class),用于标记处理特定异常类型的方法。
在controller里面写一个错误逻辑:
执行结果:
返回统一的数据格式
返回统一数据格式的好处:
- 方便前端程序员更好的接收和解析后端端口返回的数据;
- 有利于项目统一数据的维护和修改;
- 能够降低前后端程序员之间的沟通成本;
返回统一数据格式的实现也是分为两步:
1.在返回统一数据格式的类上面加上@ControllerAdvice注解;
2.让这个类实现ResponseBodyAdvice接口,并重写它里面的两个方法
@ControllerAdvice
public class MyResponseAdvice implements ResponseBodyAdvice {
/*
* 返回一个boolean值,true表示返回数据之前对数据进行重写,也就是会进入beforeBodyWrite方法,再返回
* 如果返回false表示对结果不进行任何处理,直接返回
* */
@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) {
HashMap<String,Object> result = new HashMap<>();
result.put("state",1);
result.put("data",body);
result.put("msg","");
return result;
}
}
结果: