사용자 로그인 권한 개발은 이전의 각 방법의 사용자 로그인 권한 자체 검증에서 통합된 사용자 로그인 검증 프로세스로 전환되었습니다.
먼저 초기 사용자 로그인 확인 구현 방법을 검토해 보겠습니다.
@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; } } // 其他⽅法... }
위 코드에서 볼 수 있듯이 각 방법에는 동일한 사용자 로그인 확인 권한이 있으며 단점은 다음과 같습니다.
각 메소드는 사용자 로그인 확인을 위해 별도의 메소드를 작성해야 합니다. 공개 메소드로 캡슐화하더라도 호출을 위한 매개변수를 전달하고 메소드에서 판단을 내려야 합니다.
컨트롤러를 더 많이 추가할수록 사용자 로그인 확인을 위해 호출해야 하는 방법이 많아지고, 이로 인해 추후 수정 비용과 유지 관리 비용이 늘어납니다.
이러한 사용자 로그인 인증 방법은 다음에 구현하려는 업무와는 아무런 관련이 없으나 각 방법은 반드시 한번씩 작성해야 합니다.
따라서 통합된 사용자 로그인 권한 확인을 위한 공개 AOP 방식 제공이 시급합니다.
통합 사용자 로그인 확인에 관해 우리가 생각하는 첫 번째 구현 솔루션은 Spring AOP 사전 알림 또는 주변 알림입니다. 구체적인 구현 코드는 다음과 같습니다.
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; } }
원하는 경우 위의 Spring AOP 측면에서 사용자 로그인 권한 확인 기능을 구현하려면 두 가지 문제가 있습니다.
1 HttpSession 객체를 얻을 수 있는 방법이 없습니다.
2. 일부 메소드는 차단해야 하지만 다른 메소드는 차단하지 않습니다. 예를 들어 등록 메소드와 로그인 메소드는 차단하지 않습니다. 이 경우 메소드 제외 규칙을 정의하기 어렵고 심지어 차단할 방법도 없습니다. 그것을 정의하십시오.
이 문제를 해결하는 방법은 무엇입니까?
위 문제에 대해 Spring은 특정 구현 인터셉터인 HandlerInterceptor를 제공합니다. 인터셉터의 구현은 다음 두 단계로 나뉩니다.
1 정의 인터셉터를 만들고 HandlerInterceptor의 preHandle 인터페이스를 구현합니다. 특정 메소드를 실행하기 전 전처리) 방법.
2. WebMvcConfigurer의 addInterceptors 메서드에 사용자 정의 인터셉터를 추가합니다.
구체적인 구현은 다음과 같습니다.
보조 필터:
필터는 웹 컨테이너에서 제공됩니다. 트리거링 시간이 인터셉터보다 빠르므로 Spring이 초기화되기 전에 실행되므로 사용자 로그인 권한 유효성 검사와 같은 문제를 처리할 수 없습니다.
1.3.1 준비
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 사용자 정의 인터셉터
다음으로 코드를 사용하여 사용자 로그인의 권한 확인을 구현합니다. 인터셉터를 공통 클래스로 정의합니다. 구체적으로 구현 코드는 다음과 같습니다.
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; } }
부울 유형을 반환합니다.
은 보안 계층과 동일합니다.
이 false이면 계속 실행할 수 없으며, true이면 계속 실행할 수 있습니다.
1.3.3 시스템 구성에 사용자 정의 인터셉터를 추가합니다.
이전 단계에서 정의한 인터셉터를 시스템 구성 정보에 추가합니다. 구체적인 구현 코드는 다음과 같습니다.
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 } }
또는
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); } }
객체가 삽입되지 않은 경우 addInterceptor()의 매개변수는 새 객체를 직접 생성할 수도 있습니다:
@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 } }
그 중:
addPathPatterns: 가로채야 하는 URL을 나타내고 "**"는 모든 메서드를 가로채는 것을 나타냅니다(즉, , 모든 방법).
excludePathPatterns: 제외해야 하는 URL을 나타냅니다.
참고: 위 차단 규칙은 정적 파일(이미지 파일, JS, CSS 등)을 포함하여 이 프로젝트에 사용되는 URL을 차단할 수 있습니다.
일반적인 상황에서의 호출 순서:
그러나 인터셉터를 사용하면 Controller를 호출하기 전에 해당 비즈니스 처리가 수행됩니다. 실행 프로세스는 아래 그림과 같습니다.
1.4.1 구현 원리 소스 코드 분석
모든 컨트롤러 실행은 DispatcherServlet을 통해 구현됩니다. 이는 아래와 같이 Spring Boot 콘솔의 인쇄 정보에서 확인할 수 있습니다.⽽ 모든 메소드는 DispatcherServlet에서 doDispatch 스케줄링 메소드를 실행합니다. doDispatch 소스 코드는 다음과 같습니다.
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); } } }
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; }
此时⽤户登录权限的验证⽅法就会执⾏,这就是拦截器的实现原理。
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 ⽅法,查询此⽅法的源码如下:
이 메서드가 실행되면 모든 @ControllerAdvice 클래스를 검색하여 사용하는 것으로 나타났습니다. 이러한 클래스는 컨테이너에 있지만 이벤트가 발생하면 데이터를 반환하기 전에 호출하는 등 해당 Advice 메서드가 호출됩니다. 데이터 캡슐화. 예를 들어 예외가 발생하면 해당 예외의 Advice 메서드 구현이 호출됩니다.
위 내용은 SpringBoot에서 통합 기능 처리를 구현하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!