目录
1. 用户登录权限校验
1.1 最初用户登录验证
1.2 Spring AOP 用户统一登陆验证
1.3 Spring 拦截器
1.3.1 创建自定义拦截器
1.3.2 将自定义拦截器加入系统配置
1.4 练习:登录拦截器
1.5 拦截器实现原理
1.6 统一访问前缀添加
2. 统一异常处理
3. 统一数据返回格式
3.1 为什么需要统⼀数据返回格式?
3.2 统一数据返回格式的实现
学习了 Spring 的框架后,接下来,我们来学习 Spring Boot 的统一功能处理模块,也是 AOP 的实战环节。我们主要学习以下三个部分:
- 统一用户登录验证
- 统一数据格式返回
- 统一异常处理
接下来,我们逐一来看。
1. 用户登录权限校验
用户登录权限的发展从之前每个方法中自己验证用户登录权限,到现在统⼀的用户登录验证处理,它是⼀个逐渐完善和逐渐优化的过程。
1.1 最初用户登录验证
我们先来看一下最初的用户登录验证是如何实现的。
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("method1")
public Object method1(HttpServletRequest request){
// 有session就获取,没有session不会创建
HttpSession session = request.getSession(false);
if(session != null && session.getAttribute("userinfo") != null){
// 说明已经登录,业务处理
return true;
}else {
// 未登录
return false;
}
}
@RequestMapping("method2")
public Object method2(HttpServletRequest request){
// 有session就获取,没有session不会创建
HttpSession session = request.getSession(false);
if(session != null && session.getAttribute("userinfo") != null){
// 说明已经登录,业务处理
return true;
}else {
// 未登录
return false;
}
}
// 其他方法
}
运行结果如下:
- 每个方法中都要单独写用户登录验证的方法,即使封装成公共方法,也⼀样要传参调用和在方法中进行判断。
- 添加控制器越多,调用用户登录验证的方法也越多,这样就增加了后期的修改成本和维护成本。
- 这些用户登录验证的方法和接下来要实现的业务几乎没有任何关联,但每个方法中都要写⼀遍。
1.2 Spring AOP 用户统一登陆验证
首先,我们可以通过 Spring AOP 前置通知或环绕通知来实现:
@Slf4j
@Component
@Aspect
public class LoginAspect {
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointcut(){}
@Before("pointcut()")
public void doBefore(){
log.info("do berore...");
}
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object oj = null;
log.info("环绕通知执行之前...");
try {
oj = joinPoint.proceed(); // 调用目标方法
} catch (Throwable e) {
throw new RuntimeException(e);
}
log.info("环绕通知执行之后...");
return oj;
}
- 没办法获取到 HttpSession 对象
- 要对⼀部分方法进行拦截,而另⼀部分方法不拦截,如注册方法和登录方法是不拦截的,这样的话排除方法的规则很难定义,甚至没办法定义。
1.3 Spring 拦截器
- 创建自定义拦截器,实现 HandlerInterceptor 接口的 preHandle(执行具体方法之前的预处理)方法。
- 将自定义拦截器加入 WebMvcConfigurer 的 addInterceptors 方法中。
1.3.1 创建自定义拦截器
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;
}
}
1.3.2 将自定义拦截器加入系统配置
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
// 添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")// 拦截所有接口
.excludePathPatterns("/login")
.excludePathPatterns("/reg");// 排除接口
}
}
其中:
-
addPathPatterns:表示需要拦截的 URL,“**”表示拦截任意方法(也就是所有方法)
-
excludePathPatterns:表示需要排除的 URL
1.4 练习:登录拦截器
-
登录、注册页面不拦截,其他页面都拦截。
-
当登录成功写入 session 之后,拦截的页面可正常访问。
/**
* 自定义拦截器
*/
@Component
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("username") != null){
// 通过,不进行拦截
return true; // 返回 true 表示不拦截,继续执行后续代码
}
response.setStatus(401);
return false;
}
}
/**
* 添加拦截器
*/
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
// 添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")// 拦截所有接口
.excludePathPatterns("/user/login")
.excludePathPatterns("/user/reg");// 排除接口
}
}
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
// 获取用户信息
@RequestMapping("/getInfo")
public String getInfo(){
log.info("get info...");
return "get info...";
}
// 注册
@RequestMapping("/reg")
public String reg(){
log.info("reg...");
return "reg...";
}
// Login
@RequestMapping("/login")
public boolean login(HttpServletRequest request,String username,String password){
// 判断 username && password 是否为空
if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)){
// 未登录
return false;
}
if(!"admin".equals(username) || !"admin".equals(password)){
return false;
}
HttpSession session = request.getSession(true);// 不存在则创建
session.setAttribute("username",username);
return true;
}
}
运行以上代码后,可以看到 http://127.0.0.1:8080/user/reg 可以正常访问:
http://127.0.0.1:8080/user/info 界面被拦截,且状态码为:401
http://127.0.0.1:8080/user/login 界面在没有输入用户信息之前返回 true:
在成功写入 session 之后,界面返回 true,表示成功登录:
在成功登录后,再访问 http://127.0.0.1:8080/user/info 界面时,可以看到能够正常访问了:
1.5 拦截器实现原理
1.6 统一访问前缀添加
所有请求地址添加 api 前缀:
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
// 添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")// 拦截所有接口
.excludePathPatterns("/api/user/login")
.excludePathPatterns("/api/user/reg");// 排除接口
}
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("api",c -> true);
}
}
此时可以看到没有 api 前缀则无法访问:
加上 api 前缀后,则可以访问:
其中第二个参数是⼀个表达式,设置为 true 表示启动前缀。
2. 统一异常处理
将发生的异常进行统一处理。
@RestController
public class ExceptionController {
@RequestMapping("/test1")
public boolean test1(){
int a = 10/0;
return true;
}
@RequestMapping("/test2")
public boolean test2(){
String str = null;
System.out.println(str.length());
return true;
}
@RequestMapping("/test3")
public String test3(){
throw new RuntimeException("test3手动创建异常");
}
}
@ControllerAdvice
public class ErrorHandler {
@ExceptionHandler
public Object error(Exception e){
HashMap<String,Object> result = new HashMap<>();
result.put("success",0);
result.put("code",-1);
result.put("msg","内部异常");
return result;
}
}
运行后,可以看到出现错误且状态码均为500:
此时,我们需要加上注解:@ResponseBody:
@ControllerAdvice
public class ErrorHandler {
@ResponseBody
@ExceptionHandler
public Object error(Exception e){
HashMap<String,Object> result = new HashMap<>();
result.put("success",0);
result.put("code",-1);
result.put("msg","内部异常");
return result;
}
}
此时,我们可以看到均正常返回:
还可以将异常信息分的更详细:
@ControllerAdvice
public class ErrorHandler {
@ResponseBody
@ExceptionHandler
public Object error(Exception e){
HashMap<String,Object> result = new HashMap<>();
result.put("success",0);
result.put("code",-1);
result.put("msg","内部异常");
return result;
}
@ResponseBody
@ExceptionHandler
public Object error(ArithmeticException e){
HashMap<String,Object> result = new HashMap<>();
result.put("success",0);
result.put("code",-2);
result.put("msg","ArithmeticException 异常");
return result;
}
@ResponseBody
@ExceptionHandler
public Object error(NullPointerException e){
HashMap<String,Object> result = new HashMap<>();
result.put("success",0);
result.put("code",-3);
result.put("msg","NullPointerException 异常");
return result;
}
}
运行结果如下:
3. 统一数据返回格式
3.1 为什么需要统⼀数据返回格式?
- 方便前端程序员更好的接收和解析后端数据接口返回的数据。
- 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以,因为所有接口都是这样返回的。
- 有利于项目统一数据的维护和修改。
- 有利于后端技术部门的统一规范的标准制定,不会出现稀奇古怪的返回内容。
3.2 统一数据返回格式的实现
@Slf4j
@ControllerAdvice
public class ErrorHandler {
@ResponseBody
@ExceptionHandler
public Object error(Exception e){
HashMap<String,Object> result = new HashMap<>();
result.put("success",0);
result.put("code",-1);
result.put("msg","内部异常");
log.info("Exception:",e);
return result;
}
@ResponseBody
@ExceptionHandler
public Object error(ArithmeticException e){
HashMap<String,Object> result = new HashMap<>();
result.put("success",0);
result.put("code",-2);
result.put("msg","ArithmeticException 异常");
log.info("ArithmeticException:",e);
return result;
}
@ResponseBody
@ExceptionHandler
public Object error(NullPointerException e){
HashMap<String,Object> result = new HashMap<>();
result.put("success",0);
result.put("code",-3);
result.put("msg","NullPointerException 异常");
log.info("NullPointerException:",e);
return result;
}
}
@ControllerAdvice
public class ResponseHandller 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) {
// 在向 Body 写之前要进行的操作
HashMap<String,Object> result = new HashMap<>();
result.put("success",1);
result.put("data",body);
result.put("errMsg","");
return result;
}
}
可以看到在没有进行统一数据返回格式之前,运行结果如下图所示:
在进行统一数据返回格式之后,运行结果如下图所示:
但是需要注意的是:如果方法返回的结果类型是 String,会出现以下错误:
java.lang.ClassCastException: java.util.HashMap cannot be cast to java.lang.String
解决方法如下:
@ControllerAdvice
public class ResponseHandller 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) {
// 在向 ResponseBody 写之前要进行的操作
HashMap<String,Object> result = new HashMap<>();
result.put("success",1);
result.put("data",body);
result.put("errMsg","");
// 如果返回的数据类型是字符串类型
if(body instanceof String){
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(result);
}
return result;
}
}
此时,我们可以看到运行结果如下:
查看 class 文件可以发现,@SneakyThrows 注解会自动生成 try catch 语句:
本介绍了统一用户登录权限的效验使用 WebMvcConfigurer+ HandlerInterceptor来实现,统⼀异常处理使用 @ControllerAdvice + @ExceptionHandler 来实现,统一返回值处理使用@ControllerAdvice + ResponseBodyAdvice 来处理。