This is the correct posture of SpringBoot unified login authentication, exception handling, and data format

This article will learn Spring Boot’s unified function processing module, which is also the actual part of AOP

  • User login permission verification implementation interface  HandlerInterceptor + WebMvcConfigurer

  • Exception handling uses annotations  @RestControllerAdvice + @ExceptionHandler

  • The data format returns using annotations  @ControllerAdvice and implements the interface @ResponseBodyAdvice

1. Unified user login authority verification

The development and improvement process of user login authority

  • Initial user login verification: Get the session and user information in the session in each method. If there is a user, then the login is considered successful, otherwise the login fails

  • The second version of user login verification: provide a unified method, call the unified user login identity verification method in each method that needs to be verified to judge

  • The third version of user login validation: use Spring AOP to perform unified user login validation

  • The fourth version of user login verification: using Spring interceptors to achieve unified login verification for users

1.1 The initial user login authority verification

 

@RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/a1")
    public Boolean login (HttpServletRequest request) {
        // 有 Session 就获取,没有就不创建
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("userinfo") != null) {
            // 说明已经登录,进行业务处理
            return true;
        } else {
            // 未登录
            return false;
        }
    }

    @RequestMapping("/a2")
    public Boolean login2 (HttpServletRequest request) {
        // 有 Session 就获取,没有就不创建
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("userinfo") != null) {
            // 说明已经登录,进行业务处理
            return true;
        } else {
            // 未登录
            return false;
        }
    }
}

The code written in this way has the same user login verification authority in each method. The disadvantages are:

  • Each method must write the method of user login verification separately, even if it is encapsulated into a public method, it still needs to pass parameters and call and judge in the method

  • The more controllers are added, the more methods are called for user login verification, which increases the success of later modification and maintenance

  • These methods of user login verification have almost nothing to do with the business to be implemented now, but each method must be written again, so it is a very good solution to provide a public AOP method for unified user login authority verification Method.

1.2 Spring AOP unified user login authentication

Unified user login authentication, the first thought of implementation method is to use Spring AOP pre-notification or surround notification to achieve

 

@Aspect // 当前类是一个切面
@Component
public class UserAspect {
    // 定义切点方法 Controller 包下、子孙包下所有类的所有方法
    @Pointcut("execution(* com.example.springaop.controller..*.*(..))")
    public void  pointcut(){}
    
    // 前置通知
    @Before("pointcut()")
    public void doBefore() {}
    
    // 环绕通知
    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        Object obj = null;
        System.out.println("Around 方法开始执行");
        try {
            obj = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("Around 方法结束执行");
        return obj;
    }
}

However, if the function of user login permission verification is only implemented in the aspect of the above code Spring AOP, there are two problems:

  • There is no way to get  HttpSession and Request object

  • We need to intercept some methods, but not other methods. For example, the registration method and login method are not intercepted, that is, the actual interception rules are very complicated, and simple aspectJ expressions cannot meet the interception requirements.

1.3 Spring Interceptors

In response to the problem of Spring AOP in the above code, Spring provides a specific implementation interceptor: HandlerInterceptor, the implementation of the interceptor has two steps:

1. Create a custom interceptor and implement  HandlerInterceptor the preHandle method in the interface in Spring

2. Add the custom interceptor to the configuration of the framework, and set the interception rules

  • @Configuration Annotate the current class 

  • implement  WebMvcConfigurer the interface

  • override  addInterceptors method

Note: Multiple interceptors can be configured in a project at the same time 

(1) Create a custom interceptor

/**
 * @Description: 自定义用户登录的拦截器
 * @Date 2023/2/13 13:06
 */
@Component
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;
        }
        // 执行到此代码表示未登录,未登录就跳转到登录页面
        response.sendRedirect("/login.html");
        return false;
    }
}

(2) Add the custom interceptor to the system configuration and set the interception rules

  • addPathPatterns: indicates the URL that needs to be intercepted, and **indicates that all methods are intercepted

  • excludePathPatterns: Indicates the URL that needs to be excluded

Description: Intercepting rules can intercept URLs used in this project, including static files (image files, JS and CSS files, etc.).

/**
 * @Description: 将自定义拦截器添加到系统配置中,并设置拦截的规则
 * @Date 2023/2/13 13:13
 */
@Configuration
public class AppConfig implements WebMvcConfigurer {

    @Resource
    private LoginIntercept loginIntercept;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
//        registry.addInterceptor(new LoginIntercept());//可以直接new 也可以属性注入
        registry.addInterceptor(loginIntercept).
                addPathPatterns("/**").    // 拦截所有 url
                excludePathPatterns("/user/login"). //不拦截登录注册接口
                excludePathPatterns("/user/reg").
                excludePathPatterns("/login.html").
                excludePathPatterns("/reg.html").
                excludePathPatterns("/**/*.js").
                excludePathPatterns("/**/*.css").
                excludePathPatterns("/**/*.png").
                excludePathPatterns("/**/*.jpg");
    }
}

1.4 Exercise: Login Interceptor

Require

  • Login and registration pages are not blocked, other pages are blocked

  • After the login is successfully written to the session, the intercepted page can be accessed normally

In 1.3, a custom interceptor has been created and added to the system configuration, and the rules for interception have been set

(2) Create  controller a package, create it in the package  UserController, and write the business code of the login page and the home page 

 

@RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/login")
    public boolean login(HttpServletRequest request,String username, String password) {
        boolean result = false;
        if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
            if(username.equals("admin") && password.equals("admin")) {
                HttpSession session = request.getSession();
                session.setAttribute("userinfo","userinfo");
                return true;
            }
        }
        return result;
    }

    @RequestMapping("/index")
    public String index() {
        return "Hello Index";
    }
}

(3) Run the program, visit the page, and compare the effect before and after login

 

1.5 Implementation Principle of Interceptor

With the interceptor, the corresponding business processing will be performed before calling the Controller. The execution process is shown in the figure below

Implementation principle source code analysis

All  execution will  be achieved Controller through a scheduler DispatcherServlet

 

 And all methods will be executed  DispatcherServlet in  doDispatch the scheduling method, doDispatch the source code analysis is as follows:

 

 Through source code analysis, it can be seen that the interceptor in Sping is also realized through the idea of ​​dynamic proxy and surround notification

1.6 Adding a unified access prefix

Add api prefix to all request addresses, c means all

 

@Configuration
public class AppConfig implements WebMvcConfigurer {
    // 所有的接口添加 api 前缀
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.addPathPrefix("api", c -> true);
    }
}

2. Unified exception handling

Add  @ControllerAdvice the controller notification class to the current class

Add to the method  @ExceptionHandler(xxx.class), indicating the exception handler, and add the business code returned by the exception

 

@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/index")
    public String index() {
        int num = 10/0;
        return "Hello Index";
    }
}

In the config package, create  MyExceptionAdvice the class

@RestControllerAdvice // 当前是针对 Controller 的通知类(增强类)
public class MyExceptionAdvice {
    @ExceptionHandler(ArithmeticException.class)
    public HashMap<String,Object> arithmeticExceptionAdvice(ArithmeticException e) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("state",-1);
        result.put("data",null);
        result.put("msg" , "算出异常:"+ e.getMessage());
        return result;
    }
}

 

You can also write like this, the effect is the same

 

@ControllerAdvice
public class MyExceptionAdvice {
    @ExceptionHandler(ArithmeticException.class)
    @ResponseBody
    public HashMap<String,Object> arithmeticExceptionAdvice(ArithmeticException e) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("state",-1);
        result.put("data",null);
        result.put("msg" , "算数异常:"+ e.getMessage());
        return result;
    }
}

If there is another null pointer exception, then the above code will not work, and a handler for null pointer exceptions must be written

@ExceptionHandler(NullPointerException.class)
public HashMap<String,Object> nullPointerExceptionAdvice(NullPointerException e) {
    HashMap<String, Object> result = new HashMap<>();
    result.put("state",-1);
    result.put("data",null);
    result.put("msg" , "空指针异常异常:"+ e.getMessage());
    return result;
}
@RequestMapping("/index")
public String index(HttpServletRequest request,String username, String password) {
    Object obj = null;
    System.out.println(obj.hashCode());
    return "Hello Index";
}

But one thing to consider is that if every exception is written like this, the workload will be very large, and there are custom exceptions, so it is definitely not good to write like this above. Since it is an exception, just write Exception directly. It is the parent class of all exceptions. If you encounter two exceptions that are not written above, it will directly match Exception

When there are multiple exception notifications, the matching order is for the current class and its subclasses to match upwards

@ExceptionHandler(Exception.class)
public HashMap<String,Object> exceptionAdvice(Exception e) {
    HashMap<String, Object> result = new HashMap<>();
    result.put("state",-1);
    result.put("data",null);
    result.put("msg" , "异常:"+ e.getMessage());
    return result;
}

 

You can see that the priority match is still the null pointer exception written earlier

3. Unified data format return

3.1 Realization of unified data format return

(1) Add to the current class @ControllerAdvice

(2) Implement  ResponseBodyAdvice rewriting its method

  • supports Method, this method indicates whether the content needs to be rewritten (through this method, some controllers and methods can be selectively rewritten), and if it is rewritten, return true

  • beforeBodyWrite method, this method is called before the method returns

 

@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;
    }
}
@RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/login")
    public boolean login(HttpServletRequest request,String username, String password) {
        boolean result = false;
        if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
            if(username.equals("admin") && password.equals("admin")) {
                HttpSession session = request.getSession();
                session.setAttribute("userinfo","userinfo");
                return true;
            }
        }
        return result;
    }

    @RequestMapping("/reg")
    public int reg() {
        return 1;
    }
}

 

3.2 @ControllerAdvice source code analysis

Through  @ControllerAdvice the analysis of the source code, we can know the execution process of the above unified exception and unified data return

(1) First look at the source code of @ControllerAdvice

 

You can see  @ControllerAdvice that it is derived from  @Component the component and all component initialization will call  InitializingBean the interface

(2) Let's check which implementation classes initializeBean has

During the query process, it was found that the implementation subclass in Spring MVC is that  RequestMappingHandlerAdapterthere is a method in it  afterPropertiesSet(), which indicates the method to be executed after all parameter settings are completed.

 

 

(3) And there is an initControllerAdviceCache method in this method, query this method

 

 

It is found that this method will search and use all  @ControllerAdvice classes during execution. When an event is sent, the corresponding Advice method will be called, such as calling unified data encapsulation before returning data. For example, when an exception occurs, it is implemented by calling the abnormal Advice method

Guess you like

Origin blog.csdn.net/zhangjiaming_zjm/article/details/130422871