Le développement des autorisations de connexion des utilisateurs est passé de l'auto-vérification précédente des autorisations de connexion des utilisateurs dans chaque méthode au processus de vérification de connexion des utilisateurs unifié.
Regardons d'abord la méthode de mise en œuvre de la vérification initiale de la connexion utilisateur :
@RestController @RequestMapping("/user") public class UserController { /** * 某⽅法 1 */ @RequestMapping("/m1") public Object method(HttpServletRequest request) { // 有 session 就获取,没有不会创建 HttpSession session = request.getSession(false); if (session != null && session.getAttribute("userinfo") != null) { // 说明已经登录,业务处理 return true; } else { // 未登录 return false; } } /** * 某⽅法 2 */ @RequestMapping("/m2") public Object method2(HttpServletRequest request) { // 有 session 就获取,没有不会创建 HttpSession session = request.getSession(false); if (session != null && session.getAttribute("userinfo") != null) { // 说明已经登录,业务处理 return true; } else { // 未登录 return false; } } // 其他⽅法... }
Comme le montre le code ci-dessus, chaque méthode a les mêmes autorisations de vérification de la connexion utilisateur, ses inconvénients sont :
Chaque méthode doit écrire une méthode distincte pour la vérification de la connexion de l'utilisateur. Même si elle est encapsulée dans une méthode publique, elle doit toujours transmettre des paramètres pour l'appel et porter des jugements dans la méthode.
Plus vous ajoutez de contrôleurs, plus vous devez faire appel à de méthodes pour la vérification de la connexion de l'utilisateur, ce qui augmente les coûts de modification ultérieure et les coûts de maintenance.
Ces méthodes de vérification de connexion des utilisateurs n'ont rien à voir avec l'activité à mettre en œuvre ensuite, mais chaque méthode doit être écrite une fois.
Il est donc urgent de fournir une méthode AOP publique pour la vérification unifiée des autorisations de connexion des utilisateurs.
En parlant de vérification de connexion utilisateur unifiée, la première solution d'implémentation à laquelle nous pensons est la pré-notification Spring AOP ou la notification surround. Le code d'implémentation spécifique est le suivant :
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect @Component public class UserAspect { // 定义切点⽅法 controller 包下、⼦孙包下所有类的所有⽅法 @Pointcut("execution(* com.example.demo.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 throwable) { throwable.printStackTrace(); } System.out.println("Around ⽅法结束执⾏"); return obj; } }
Si vous le souhaitez. pour implémenter la fonction de vérification des autorisations de connexion utilisateur dans l'aspect Spring AOP ci-dessus, il y a deux problèmes :
1 Il n'y a aucun moyen d'obtenir l'objet HttpSession.
2. Nous devons intercepter certaines méthodes, mais pas d'autres méthodes. Par exemple, la méthode d'enregistrement et la méthode de connexion ne sont pas interceptées. Dans ce cas, les règles d'exclusion des méthodes sont difficiles à définir, et même il n'y a aucun moyen de le faire. le définir.
Alors comment résoudre ça ?
Pour les problèmes ci-dessus, Spring fournit un intercepteur d'implémentation spécifique : HandlerInterceptor. L'implémentation de l'intercepteur est divisée en deux étapes suivantes :
1. Créer un intercepteur de définition et implémenter l'interface preHandle de HandlerInterceptor (. Prétraitement avant d'exécuter des méthodes spécifiques).
2. Ajoutez l'intercepteur personnalisé à la méthode addInterceptors de WebMvcConfigurer.
La mise en œuvre spécifique est la suivante.
Filtres supplémentaires :
Les filtres sont fournis par le conteneur Web. L'heure de déclenchement est antérieure à celle de l'intercepteur. Il est exécuté avant l'initialisation de Spring, il ne peut donc pas gérer des problèmes tels que la validation des autorisations de connexion de l'utilisateur.
1.3.1 Préparation
package com.example.demo.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; @RestController @RequestMapping("/user") @Slf4j public class UserController { @RequestMapping("/login") public boolean login(HttpServletRequest request, String username, String password) { // // 1.非空判断 // if (username != null && username != "" && // password != null && username != "") { // // 2.验证用户名和密码是否正确 // } // 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("/getinfo") public String getInfo() { log.debug("执行了 getinfo 方法"); return "执行了 getinfo 方法"; } @RequestMapping("/reg") public String reg() { log.debug("执行了 reg 方法"); return "执行了 reg 方法"; } }
1.3.2 Intercepteur personnalisé
Ensuite, utilisez le code pour implémenter la vérification des autorisations d'une connexion utilisateur. Définissez l'intercepteur en tant que classe commune. Le code d'implémentation est le suivant :
package com.example.demo.config; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * 登录拦截器 */ @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; } }
Renvoie le type booléen.
équivaut à une couche de sécurité : si
est faux, il ne peut pas continuer à s'exécuter si c'est vrai, il le peut ;
1.3.3 Ajouter l'intercepteur personnalisé à la configuration du système
Ajouter l'intercepteur défini à l'étape précédente aux informations de configuration du système Le code d'implémentation spécifique est le suivant :
package com.example.demo.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @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("/**/*.html") // .excludePathPatterns("/**/*.js") // .excludePathPatterns("/**/*.css") .excludePathPatterns("/user/reg"); // 排除不拦截的 url } }
ou :
package com.example.demo.common; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.ArrayList; import java.util.List; @Configuration public class AppConfig implements WebMvcConfigurer { // 不拦截的 url 集合 List<String> excludes = new ArrayList<String>() {{ add("/**/*.html"); add("/js/**"); add("/editor.md/**"); add("/css/**"); add("/img/**"); // 放行 static/img 下的所有文件 add("/user/login"); // 放行登录接口 add("/user/reg"); // 放行注册接口 add("/art/detail"); // 放行文章详情接口 add("/art/list"); // 放行文章分页列表接口 add("/art/totalpage"); // 放行文章分页总页数接口 }}; @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { // 配置拦截器 InterceptorRegistration registration = registry.addInterceptor(loginInterceptor); registration.addPathPatterns("/**"); registration.excludePathPatterns(excludes); } }
. Si aucun objet n'est injecté, les paramètres de addInterceptor() peuvent aussi créer directement un nouvel objet :
@Configuration // 一定不要忘记 public class MyConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") // 拦截所有请求 .excludePathPatterns("/user/login") // 排除不拦截的 url // .excludePathPatterns("/**/*.html") // .excludePathPatterns("/**/*.js") // .excludePathPatterns("/**/*.css") // .excludePathPatterns("/**/*.jpg") // .excludePathPatterns("/**/login") .excludePathPatterns("/user/reg"); // 排除不拦截的 url } }
Parmi eux :
addPathPatterns : indique l'URL qui doit être interceptée, "**" indique l'interception de n'importe quelle méthode (c'est-à-dire , toutes les méthodes ).
excludePathPatterns : indique les URL qui doivent être exclues.
Remarque : Les règles d'interception ci-dessus peuvent intercepter les URL utilisées dans ce projet, y compris les fichiers statiques (fichiers image, JS, CSS, etc.).
Séquence d'appel dans des circonstances normales :
Cependant, avec l'intercepteur, le traitement métier correspondant sera effectué avant d'appeler le contrôleur. Le processus d'exécution est comme indiqué dans la figure ci-dessous.
1.4.1 Analyse du code source du principe de mise en œuvre
Toutes les exécutions du contrôleur seront implémentées via un répartiteur DispatcherServlet. Cela peut être vu à partir des informations d'impression de la console Spring Boot, comme indiqué ci-dessous Comme indiqué :.
⽽ Toutes les méthodes exécuteront la méthode de planification doDispatch dans DispatcherServlet. Le code source de doDispatch est le suivant :protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { try { ModelAndView mv = null; Object dispatchException = null; try { processedRequest = this.checkMultipart(request); multipartRequestParsed = processedRequest != request; mappedHandler = this.getHandler(processedRequest); if (mappedHandler == null) { this.noHandlerFound(processedRequest, response); return; } HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.g etHandler()); String method = request.getMethod(); boolean isGet = HttpMethod.GET.matches(method); if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mapped Handler.getHandler()); if ((new ServletWebRequest(request, response)).checkNo tModified(lastModified) && isGet) { return; } } // 调⽤预处理【重点】 if (!mappedHandler.applyPreHandle(processedRequest, respon se)) { return; } // 执⾏ Controller 中的业务 mv = ha.handle(processedRequest, response, mappedHandler.g etHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } this.applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception var20) { dispatchException = var20; } catch (Throwable var21) { dispatchException = new NestedServletException("Handler di spatch failed", var21); } this.processDispatchResult(processedRequest, response, mappedH andler, mv, (Exception)dispatchException); } catch (Exception var22) { this.triggerAfterCompletion(processedRequest, response, mapped Handler, var22); } catch (Throwable var23) { this.triggerAfterCompletion(processedRequest, response, mapped Handler, new NestedServletException("Handler processing failed", var23)); } } finally { if (asyncManager.isConcurrentHandlingStarted()) { if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processe dRequest, response); } } else if (multipartRequestParsed) { this.cleanupMultipart(processedRequest); } } }À partir du code source ci-dessus, on peut voir qu'avant de commencer à exécuter le Controller, le prétraitement sera appelé en premier. La méthode applyPreHandle et le code source d'implémentation de la méthode applyPreHandle sont les suivants :
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) { // 获取项⽬中使⽤的拦截器 HandlerInterceptor HandlerInterceptor interceptor = (HandlerInterceptor)this.intercep torList.get(i); if (!interceptor.preHandle(request, response, this.handler)) { this.triggerAfterCompletion(request, response, (Exception)null ); return false; } } return true; }Comme le montre le code source ci-dessus, tous les intercepteurs HandlerInterceptor seront obtenus dans applyPreHandle et la méthode preHandle dans l'intercepteur sera exécutée, ce qui sera ce que nous avons défini précédemment. L'intercepteur correspond, comme le montre la figure ci-dessous :
此时⽤户登录权限的验证⽅法就会执⾏,这就是拦截器的实现原理。
1.4.2 拦截器小结
通过上⾯的源码分析,我们可以看出,Spring 中的拦截器也是通过动态代理和环绕通知的思想实现的,⼤体的调⽤流程如下:
所有请求地址添加 api 前缀:
@Configuration public class AppConfig implements WebMvcConfigurer { // 所有的接⼝添加 api 前缀 @Override public void configurePathMatch(PathMatchConfigurer configurer) { configurer.addPathPrefix("api", c -> true); } }
其中第⼆个参数是⼀个表达式,设置为 true 表示启动前缀。
统⼀异常处理使⽤的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执⾏某个通知,也就是执⾏某个⽅法事件,具体实现代码如下:
package com.example.demo.config; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import java.util.HashMap; /** * 统一处理异常 */ @ControllerAdvice public class ErrorAdive { @ExceptionHandler(Exception.class) @ResponseBody public HashMap<String, Object> exceptionAdvie(Exception e) { HashMap<String, Object> result = new HashMap<>(); result.put("code", "-1"); result.put("msg", e.getMessage()); return result; } @ExceptionHandler(ArithmeticException.class) @ResponseBody public HashMap<String, Object> arithmeticAdvie(ArithmeticException e) { HashMap<String, Object> result = new HashMap<>(); result.put("code", "-2"); result.put("msg", e.getMessage()); return result; } }
方法名和返回值可以⾃定义,重要的是 @ControllerAdvice 和 @ExceptionHandler 注解。
以上⽅法表示,如果出现了异常就返回给前端⼀个 HashMap 的对象,其中包含的字段如代码中定义的那样。
我们可以针对不同的异常,返回不同的结果,⽐以下代码所示:
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import java.util.HashMap; @ControllerAdvice @ResponseBody public class ExceptionAdvice { @ExceptionHandler(Exception.class) public Object exceptionAdvice(Exception e) { HashMap<String, Object> result = new HashMap<>(); result.put("success", -1); result.put("message", "总的异常信息:" + e.getMessage()); result.put("data", null); return result; } @ExceptionHandler(NullPointerException.class) public Object nullPointerexceptionAdvice(NullPointerException e) { HashMap<String, Object> result = new HashMap<>(); result.put("success", -1); result.put("message", "空指针异常:" + e.getMessage()); result.put("data", null); return result; } }
当有多个异常通知时,匹配顺序为当前类及其子类向上依次匹配,案例演示:
在 UserController 中设置⼀个空指针异常,实现代码如下:
@RestController @RequestMapping("/u") public class UserController { @RequestMapping("/index") public String index() { Object obj = null; int i = obj.hashCode(); return "Hello,User Index."; } }
以上程序的执⾏结果如下:
此时若出现异常就不会报错了,代码会继续执行,但是会把自定义的异常信息返回给前端!
统一完数据返回格式后:
package com.example.demo.common; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; /** * 异常类的统一处理 */ @ControllerAdvice @ResponseBody public class ExceptionAdvice { @ExceptionHandler(Exception.class) public Object exceptionAdvice(Exception e) { return AjaxResult.fail(-1, e.getMessage()); } }
统一异常处理不用配置路径,是拦截整个项目中的所有异常。
统⼀数据返回格式的优点有很多,比如以下几个:
⽅便前端程序员更好的接收和解析后端数据接⼝返回的数据。
降低前端程序员和后端程序员的沟通成本,按照某个格式实现就⾏了,因为所有接⼝都是这样返回的。
有利于项⽬统⼀数据的维护和修改。
有利于后端技术部⻔的统⼀规范的标准制定,不会出现稀奇古怪的返回内容。
统⼀的数据返回格式可以使用 @ControllerAdvice + ResponseBodyAdvice接口 的方式实现,具体实现代码如下:
import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyA dvice; import java.util.HashMap; /** * 统一返回数据的处理 */ @ControllerAdvice public class ResponseAdvice implements ResponseBodyAdvice { /** * 内容是否需要重写(通过此⽅法可以选择性部分控制器和⽅法进⾏重写) * 返回 true 表示重写 */ @Override public boolean supports(MethodParameter returnType, Class converterTyp e) { return true; } /** * ⽅法返回之前调⽤此⽅法 */ @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpR equest request, ServerHttpResponse response) { // 构造统⼀返回对象 HashMap<String, Object> result = new HashMap<>(); result.put("state", 1); result.put("msg", ""); result.put("data", body); return result; } }
统一处理后,此时所有返回的都是 json 格式的数据了。
若方法的返回类型为 String,统一数据返回格式封装后,返回会报错!?
转换器的问题,解决方案:
实际开发中这种统一数据返回格式的方式并不常用。因为它会将所有返回都再次进行封装,过于霸道了 ~
而通常我们会写一个统一封装的类,让程序猿在返回时统一返回这个类 (软性约束),例如:
package com.example.demo.common; import java.util.HashMap; /** * 自定义的统一返回对象 */ 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 * @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; } }
同时搭配统一数据返回格式:
package com.example.demo.common; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import java.util.HashMap; /** * 统一数据返回封装 */ @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) { if (body instanceof HashMap) { // 本身已经是封装好的对象 return body; } if (body instanceof String) { // 返回类型是 String(特殊) ObjectMapper objectMapper = new ObjectMapper(); return objectMapper.writeValueAsString(AjaxResult.success(body)); } return AjaxResult.success(body); } }
通过对 @ControllerAdvice 源码的分析我们可以知道上⾯统⼀异常和统⼀数据返回的执⾏流程,我们先从 @ControllerAdvice 的源码看起,点击 @ControllerAdvice 实现源码如下:
从上述源码可以看出 @ControllerAdvice 派⽣于 @Component 组件,⽽所有组件初始化都会调用 InitializingBean 接⼝。
所以接下来我们来看 InitializingBean 有哪些实现类?在查询的过程中我们发现了,其中 Spring MVC中的实现⼦类是 RequestMappingHandlerAdapter,它⾥⾯有⼀个⽅法 afterPropertiesSet() ⽅法,表示所有的参数设置完成之后执⾏的⽅法,如下图所示:
⽽这个⽅法中有⼀个 initControllerAdviceCache ⽅法,查询此⽅法的源码如下:
Nous avons constaté que lorsque cette méthode est exécutée, elle recherchera et utilisera toutes les classes @ControllerAdvice. Ces classes seront dans le conteneur, mais lorsqu'un événement se produit, la méthode Advice correspondante sera appelée, par exemple en appelant avant de renvoyer des données. encapsulation des données, par exemple, si une exception se produit, l'implémentation de la méthode Advice de l'exception est appelée.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!