1.统一用户登录权限效验
统一用户登录权限效验使用传统的 AOP 能否解决问题呢 ?
-
@Component
-
@Aspect
// 标识当前类为一个切面
-
public
class
LoginAOP {
-
// 定义切点 (拦截的规则) - 拦截 UserController 中的所有方法
-
@Pointcut("execution(* com.example.demo.controller.TestController.*(..))")
-
public
void
pointcut
() {
-
}
-
-
// 环绕通知
-
@Around("pointcut()")
-
public Object
doAround
(ProceedingJoinPoint joinPoint) {
-
Object
obj
=
null;
-
// 前置业务代码
-
System.out.println(
"环绕通知的前置执行方法");
-
try {
-
// 执行目标方法
-
obj = joinPoint.proceed();
-
}
catch (Throwable e) {
-
e.printStackTrace();
-
}
-
// 后置业务代码
-
System.out.println(
"环绕通知的后置执行方法");
-
return obj;
-
}
-
}
能解决, 但是相对来说, 比较麻烦:
首先, 环绕通知没有内置 HttpServletRequest 对象, 就不好拿到 session 对象.
其次, 对于一些特殊的场景: 我们要对一部分方法进行拦截, 而另一部分方法不拦截时, 切点中的拦截规则很难定义, 甚至没办法定义.
1.1 Spring 拦截器
对于上述问题, Spring 提供的拦截器就可以很好地解决.
Spring 拦截器和传统 AOP的区别就类似 Servlet 和 Spring 的区别, 拦截器也是将传统 AOP 进行了封装, 内置了 reuqest, response 对象, 提供了更加方便的功能.
一个项目里面实现统一用户验证登录的处理, 一般有三种解决方案:
使用传统的 AOP,
使用拦截器,
使用过滤器.
既然有三种解决方案, 为什么要选择使用拦截器呢 ?
1. 对于传统的 AOP, 功能比较简单, 写法过于复杂, 所以不使用.
2. 对于过滤器 (web容器提供的), 因为它的执行时机太靠前了, Spring 框架还没初始化, 也就是说触发过滤器的时候, request, response 对象还没有实例化. 所以过滤器用的也比较少.
🍁实现拦截器的两大步骤
创建自定义拦截器, 实现 HandlerInterceptor 接口并重写preHandle (执行方法前的预处理) 方法.
将自定义拦截器加入 WebMvcConfigurer 的 addInterceptors 方法中. 【配置拦截规则】
1.1.1 创建自定义拦截器
-
@Component
-
@Slf4j
-
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;
-
}
-
log.error(
"当前用户没有访问权限");
-
response.setStatus(
401);
-
return
false;
-
}
-
}
自定义的拦截器是一个普通的类, 如果返回 true, 才会继续执行后续代码.
1.1.2 将自定义拦截器加入到系统配置中
前面写的自定义拦截器, 只是一个普通的类, 需要把它加入到系统配置中, 并配置拦截规则, 才是一个真正有用的拦截器.
-
@Configuration
// 将拦截器加入到框架当中
-
public
class
MyConfig
implements
WebMvcConfigurer {
-
@Autowired
-
private LoginInterceptor loginInterceptor;
-
-
@Override
-
public
void
addInterceptors
(InterceptorRegistry registry) {
-
registry.addInterceptor(loginInterceptor)
// 添加拦截器, 可以添加多个
-
.addPathPatterns(
"/**")
// 拦截所有请求
-
.excludePathPatterns(
"/user/login")
// 排除不拦截的 url
-
.excludePathPatterns(
"/user/reg");
// 排除不拦截的 url
-
}
-
}
1. addInterceptor 方法的作用 : 将自定义拦截器添加到系统配置中.
2. addPathPatterns : 表示需要拦截的 URL.
3. excludePathPatterns : 表示不拦截, 需要排除的 URL.
4. 拦截器不仅可以拦截方法, 还可以拦截静态文件 (.png, .js, .css)
业务代码:
-
@RestController
-
@RequestMapping("/user")
-
@Slf4j
-
public
class
UserController {
-
@RequestMapping("/login")
-
public
boolean
login
(HttpServletRequest request,
-
String username, String password) {
-
// 1. 非空判断
-
if(StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
-
// 2. 验证用户名和密码是否正确
-
if(
"admin".equals(username) &&
"admin".equals(password)) {
-
// 登陆成功
-
HttpSession
session
= request.getSession();
-
// 存储用户信息
-
session.setAttribute(
"userinfo",
"admin");
-
return
true;
-
}
else {
-
// 用户名或密码错误
-
return
false;
-
}
-
}
-
return
false;
-
}
-
-
@RequestMapping("/get_info")
-
public String
getInfo
() {
-
log.debug(
"执行了 getInfo 方法");
-
return
"执行了 getInfo 方法";
-
}
-
-
@RequestMapping("/reg")
-
public String
reg
() {
-
log.debug(
"执行了 reg 方法");
-
return
"执行了 reg 方法";
-
}
-
}
前两个步骤我们已经做好了准具工作, 并配置好了拦截规则, 规定除了登录和注册功能不拦截外, 拦截其他所有 URL (getInfo). 下面来进行验证一下拦截器是否生效.
🍁验证拦截器是否生效
访问注册方法: 127.0.0.1:8080/user/reg
通过浏览器结果来看, 我们自定义的拦截器并没有拦截注册功能, 这符合我们的预期.
再来看看控制台的日志打印:
此处的日志打印的确实有点莫名其妙, 但是不是拦截器的锅, 这只是网页加载图标时报的错, 因为拦截器拦截不了 favicon.ico 【不重要】. 我们可以通过开发者工具抓包进行查看: 发现并不是代码得问题, reg 返回的状态码是 200 , 所以符合预期.【重要】
访问 getInfo() 方法: 127.0.0.1:8080/user/get_info
通过浏览器结果来看, 我们自定义的拦截器确实拦截了 getInfo() 方法, 并且设置了状态码 401, 这也符合我们的预期. 通过控制台查看, 此时就可以看到 "当前用户没有访问权限" 的日志信息了, 并且这是由访问 getInfo 方法触发的 (抓包查看就知道了).
访问登录方法
🍃当我直接通过访问 127.0.0.1:8080/user/login 的时候, 浏览器上会显示一个 false, 这和注册方法的效果一样, 只不过尚未登录 >>
🍃当我给上面的 URL 加上正确的参数时 (admin), 这时候浏览器上就能显示一个 true, 并且此时再次访问 getInfo() 方法时, 就不会出现 401 了>>
再次访问 getInfo() 时, 就不会出现 401 了 >>
由此可得, 以上的自定义拦截器实现了统一登陆验证功能. 但是要注意的点是: 我的用户信息只是挂在 session 上了, 那么它的作用域就是浏览器作用域, 你如果使用其他的浏览器访问, 依旧还是会出现 401.
1.2 拦截器的实现原理
首先我们要知道 Controller 的执行都会通过一个调度器 (DispatcherServlet) 来实现.
随便访问 controller 中的一个方法就能在控制台的打印信息就能看到, 这个可以类比到线程的调度上.
然后所有 Controller 中方法都会执行 DispatcherServlet 中的调度方法 doDispatch().
我们通过分析源码, 发现源码中的这两个主要步骤.预处理的过程就 和 前边代码 LoginInterceptor 拦截器做的事情差不多,判断拦截的方法是否符合要求, 如果符合要求, 就返回 true,然后继续执行后续业务代码, 否则, 后面的代码都不执行.
进入 applyPreHandle() 方法继续分析:
我们发现源码中就是通过遍历存放拦截器的 List, 然后不断判断每一个拦截器是否都返回 true 了, 但凡其中有一个拦截器返回 false, 后面的拦截器都不要走了, 并且后面的业务代码也不执行了. 看到这, 我们恍然大悟了.
添加拦截器前后程序执行流程:
通过前面的分析, 我们就能发现 Spring 中的拦截器其实就是封装了传统的 AOP , 它也是通过 动态代理的和环绕通知的思想来实现的
2. 统一异常的处理
为什么要统一异常的处理呢 ??
就拿用户在银行取钱这件事来说, 如果用户在办理业务的时候, 后端程序报错了, 它不返回任何信息, 或者它返回的信息不统一, 这都会让前端程序猿不知道咋办, 他不知道咋办, 那么就无法给用户提供相应的提示. 此时用户见程序没反应, 他自己也会怀疑是自己没点到, 还是程序出 bug 了. 所以需要进行统一异常的处理.
实现统一异常的处理是需要两个注解来实现的:
@ControllerAdvice : 控制通知类.
@ExceptionHandler : 异常处理器
二者结合表示, 当出现异常的时候执行某个通知 (执行某个方法事件)
【代码实现】
对于前面的 reg 方法, 我们写一个除0 异常.
-
@RequestMapping("/reg")
-
public String
reg
() {
-
int
number
=
1 /
0;
-
log.debug(
"执行了 reg 方法");
-
return
"执行了 reg 方法";
-
}
异常处理类:
-
@ControllerAdvice
-
public
class
ErrorAdvice {
-
-
@ExceptionHandler(Exception.class)
// 异常类型
-
@ResponseBody
-
public HashMap<String, Object>
exceptionAdvice
(Exception e) {
-
HashMap<String, Object> res =
new
HashMap<>();
-
res.put(
"code",
"-1");
-
res.put(
"msg", e.getMessage());
-
return res;
-
}
-
}
程序运行, 浏览器访问结果:
这样处理之后, 前端程序猿就知道什么状况了, 这时候就可以友好的告诉用户 "系统繁忙, 请稍后再试"
如果没有做相应的处理, 程序就会报错, 并且啥都不返回, 此时前端程序猿就会懵逼.
上面的异常处理使用了一个大的异常类来处理, 我们还可以更加细化:
-
@ControllerAdvice
-
public
class
ErrorAdvice {
-
-
@ExceptionHandler(ArithmeticException.class)
// 异常类型
-
@ResponseBody
-
public HashMap<String, Object>
arithmeticAdvice
(Exception e) {
-
HashMap<String, Object> res =
new
HashMap<>();
-
res.put(
"code",
"-1");
-
res.put(
"msg", e.getMessage());
-
return res;
-
}
-
}
3.统一数据返回格式
为什么要统一数据返回格式 ?? 【优点】
1. 方便前端程序猿更好的接收和解析后端数据接口返回的数据, 降低前后端程序猿沟通成本!!
2. 有利于项目统一数据的维护和修改 等等...
【代码实现】
-
@ControllerAdvice
-
public
class
ResponseAdvice
implements
ResponseBodyAdvice {
-
@Override
-
public
boolean
supports
(MethodParameter returnType, Class converterType) {
-
// 返回 true, 表示走底下的方法
-
return
true;
-
}
-
-
// 方法返回之前调用此方法
-
@Override
-
public Object
beforeBodyWrite
(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
-
HashMap<String, Object> res =
new
HashMap<>();
-
res.put(
"code",
200);
-
res.put(
"msg",
"");
-
res.put(
"data", body);
-
return res;
-
}
-
}
实现统一数据格式的关键点:
1. 添加@ ControllerAdvice 注解;
2. 实现 ResponseBodyAdvice 接口, 并重写 supports() 和 beforeBodyWrite() 两个方法.
使用拦截器的业务代码, 验证功能是否正确 >>>
访问 login() 方法:
经过统一格式的处理之后, 返回的数据都是统一的 Json 格式.
3.1 针对返回 String 类型的特殊处理
上述代码貌似没有问题了, 但是当我们去访问 reg() 方法时 :
-
@RequestMapping("/reg")
-
public String
reg
() {
-
log.debug(
"执行了 reg 方法");
-
return
"执行了 reg 方法";
-
}
发现程序竟然报错了, 错误原因是 HashMap 不能转换为 String >>
🍔报错的原因
首先, HashMap 在转换为 Json格式的 String时, 框架使用的是转换器. 通过打断点调试, 发现出现这个错误的原因就是转换器不同导致的:
返回值为 String , 使用的转换器 :
返回值为其他类型使用的换器:
返回 String 类型, 封装成 HashMap , 转换成 Json 格式的字符串时, 使用的是 org.springframework.http.converter.StringHttpMessageConverter 转换器, 而返回其他类型时, 使用的是 org.springframework.http.converter.json.MappingJackson2HttpMessageConverter 转换器.
为什么使用 StringHttpMessage 转换器就会报错呢 >>
因为 StringHttpMessage 转换器的执行时机比较晚, 在进行类型转换的时候, 该转换器还没加载好, 所以就会报错, 而 MappingJackson2 转换器的执行时机比较早, 所以不会报错.
如何解决>>
-
@SneakyThrows
-
@Override
-
public Object
beforeBodyWrite
(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
-
HashMap<String, Object> res =
new
HashMap<>();
-
res.put(
"code",
200);
-
res.put(
"msg",
"");
-
res.put(
"data", body);
-
// 处理返回类型为 String 的
-
if(body
instanceof String) {
-
ObjectMapper
objectMapper
=
new
ObjectMapper();
-
return objectMapper.writeValueAsString(res);
-
}
-
return res;
-
}
浏览器访问 reg 方法:
当我们单独处理 String 类型时,遇到 String类型, 就使用 ObjectMapper 对象将其转换成 Json 字符串, 此时就不会报错了.
3.2 企业级统一数据返回
之前的统一数据返回的 beforeBodyWrite() 存在的问题:
之前的统一数据返回, 太过笼统了, 相当于把除异常之外的所有的数据的返回的状态码都设为 200 了. 这样非常不利于业务的分类.
之前的统一数据返回, 如果 本身就是封装好的数据, 返回时调用 beforeBodyWrite() 方法, 就还会再被封装一次, 这不符合预期.
【正确做法】
创建自定义统一数据返回类型的类:
-
public
class
AjaxResult {
-
/**
-
* 业务执行成功时进行返回的方法
-
* @param data
-
* @return
-
*/
-
public
static HashMap<String, Object>
success
(Object data) {
-
HashMap<String, Object> result =
new
HashMap<>();
-
result.put(
"code",
200);
-
result.put(
"msg",
"");
-
result.put(
"data", data);
-
return result;
-
}
-
-
/**
-
* 业务执行成功时进行返回的方法
-
* @param data
-
* @return
-
*/
-
public
static HashMap<String, Object>
success
(String msg, Object data) {
-
HashMap<String, Object> result =
new
HashMap<>();
-
result.put(
"code",
200);
-
result.put(
"msg", msg);
-
result.put(
"data", data);
-
return result;
-
}
-
-
/**
-
* 业务执行失败时进行返回的方法
-
* @param code
-
* @param msg
-
* @return
-
*/
-
public
static HashMap<String, Object>
fail
(int code, String msg) {
-
HashMap<String, Object> result =
new
HashMap<>();
-
result.put(
"code", code);
-
result.put(
"msg", msg);
-
result.put(
"data",
"");
-
return result;
-
}
-
-
/**
-
* 业务执行失败时进行返回的方法
-
* @param code
-
* @param msg
-
* @param data
-
* @return
-
*/
-
public
static HashMap<String, Object>
fail
(int code, String msg, Object data) {
-
HashMap<String, Object> result =
new
HashMap<>();
-
result.put(
"code", code);
-
result.put(
"msg", msg);
-
result.put(
"data", data);
-
return result;
-
}
-
}
修改 beforeBodyWrite() 方法:
-
@ControllerAdvice
-
public
class
ResponseAdvice
implements
ResponseBodyAdvice {
-
-
@Override
-
public
boolean
supports
(MethodParameter returnType, Class converterType) {
-
return
true;
-
}
-
-
@SneakyThrows
-
@Override
-
public Object
beforeBodyWrite
(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
-
// 1.本身已经是封装好的对象
-
if(body
instanceof HashMap) {
-
return body;
-
}
-
// 2.返回类型是 String (特殊)
-
if(body
instanceof String) {
-
ObjectMapper
objectMapper
=
new
ObjectMapper();
-
return objectMapper.writeValueAsString(AjaxResult.success(body));
-
}
-
return AjaxResult.success(body);
-
}
-
}
这样处理之后, 以上的两个问题就都得到了解决, 这才是企业级的统一数据返回格式的处理.
【其他代码的变化】
此时 reg() 方法完全可以这样写了:
-
@RequestMapping("/reg")
-
public Object
reg
(String username, String password) {
-
// return AjaxResult.success("注册成功!", 1);
-
return AjaxResult.fail(-
1,
"数据库添加出错!");
-
}
我想指定状态码为 -1, 就传 -1, 想指定状态码为 -2, 就传 -2, 变得更加灵活了. 而且此处就算方法本身返回的就是一个封装好的对象, 也能得到有效的处理了.
统一异常的处理也可以变得简单了:
-
@ControllerAdvice
-
@ResponseBody
-
public
class
ExceptionAdvice {
-
-
@ExceptionHandler(Exception.class)
// 异常类型
-
public Object
exceptionAdvice
(Exception e) {
-
return AjaxResult.fail(-
1, e.getMessage());
-
}
-
}
本篇文章就到这里了, 谢谢观看!!
转载:https://blog.csdn.net/xaiobit_hl/article/details/128588925