Spring之AOP在鉴权和日志记录中的应用

版权声明: https://blog.csdn.net/qq_24313635/article/details/82527342

鉴权

假设现有需求,要求如下:

  1. 可以定制地为某些指定的 HTTP RESTful api 提供权限验证功能.

  2. 当调用方的权限不符时, 返回错误.

根据上面所提出的需求, 我们可以进行如下设计:

  1. 提供一个特殊的注解 AuthChecker, 这个是一个方法注解, 有此注解所标注的 Controller 需要进行调用方权限的认证.

  2. 利用 Spring AOP, 以 @annotation 切点标志符来匹配有注解 AuthChecker 所标注的 joinpoint.

  3. 在 aspect中, 简单地检查调用者请求中的 Cookie 中是否有我们指定的 token, 如果有, 则认为此调用者权限合法, 允许调用, 反之权限不合法, 范围错误.

根据上面的设计, 我们来看一下具体的源码吧.

首先是定义一个 AuthChecker 注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthChecker {
}

AuthChecker 注解是一个方法注解, 它用于标注在需要进行鉴权的服务方法上

有了注解的定义, 那我们再来看一下 aspect 的实现吧:

AuthAspect.java

@Component
@Aspect
public class AuthAspect {

	private static final Log LOG = LogFactory.getLog(AuthAspect.class);
	
	@Pointcut("@annotation(com.yj.aspect.AuthChecker)")
	public void Authpointcut() {
	}

	@Before("Authpointcut()")
	public void doBefore(JoinPoint joinPoint) throws Throwable {
		HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
				.getRequest();
		String token = CommUtils.getCookie(request, "userToken");
		LOG.info("==开始进行token校验,token值:"+token+"==");
		if (null==token || !"abc".equals(token)) {
			throw new Throwable("用户token不合法");
		}
	}
}

CommUtils中的getCookie方法

public static String getCookie(HttpServletRequest request, String name) {
		Cookie cookies[] = request.getCookies();
		Cookie sCookie = null;
		String sid = null;
		if (cookies != null && cookies.length > 0) {
			for (int i = 0; i < cookies.length; i++) {
				sCookie = cookies[i];
				if (name.equals(sCookie.getName())) {
					sid = sCookie.getValue();
					break;
				}
			}
		}
		return sid;
}

当被 AuthChecker 注解所标注的方法调用前, 会执行我们的这个 Aspect, 而这个 Aspect的处理逻辑很简单, 即从 HTTP 请求中获取名为 userToken 的 cookie 的值, 如果它的值是 abc, 则我们认为此 HTTP 请求合法 而如果userToken cookie 的值不是 abc, 或为空, 则认为此 HTTP 请求非法, 返回错误.

接下来我们来写一个模拟的 HTTP 接口:

BizController

package com.yj.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.yj.service.BizService;

@RestController
public class BizController {
	
	@Autowired
	private BizService bizService;

	@RequestMapping("/doBiz")
	public void doBiz(){
		bizService.doBiz();
	}
	
	@RequestMapping("/doOtherBiz")
	public void doOtherBiz(){
		bizService.doOtherBiz();
	}
}

BizService

@Service
public class BizService {
	
	private static final Log LOG = LogFactory.getLog(BizService.class);
	
	@AuthChecker
	public void doBiz(){
		LOG.info("==开始处理关键业务==");
	}
	
	public void doOtherBiz(){
		LOG.info("==开始处理非关键业务==");
	}
}

注意到上面我们提供了两个 HTTP 接口, 其中 接口 /doOtherBiz 是没有 AuthChecker 标注的, 而 /doBiz 接口则用到了 @AuthChecker 标注. 那么自然地, 当请求了/doBiz 接口时, 就会触发我们所设置的权限校验逻辑.

接下来我们来验证一下, 我们所实现的功能是否有效吧.
首先在 Postman 中, 调用 /doOtherBiz 接口, 请求头中不加任何参数:

==开始处理非关键业务==

可以看到, 我们的 HTTP 请求完全没问题.

那么再来看一下请求 /doBiz 接口会怎样呢:

2018-09-08 10:59:20.004  INFO 11272 --- [nio-8080-exec-7] com.yj.aspect.AuthAspect                 : ==开始进行token校验,token值:null==
2018-09-08 10:59:20.007 ERROR 11272 --- [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.reflect.UndeclaredThrowableException] with root cause

java.lang.Throwable: 用户token不合法
	at com.yj.aspect.AuthAspect.doBefore(AuthAspect.java:34) ~[classes/:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_171]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_171]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_171]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_171]
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:629) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:611) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.aop.aspectj.AspectJMethodBeforeAdvice.before(AspectJMethodBeforeAdvice.java:43) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:51) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:168) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:656) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at com.yj.service.BizService$$EnhancerBySpringCGLIB$$58cca3de.doBiz(<generated>) ~[classes/:na]
	at com.yj.controller.BizController.doBiz(BizController.java:16) ~[classes/:na]
	at com.yj.controller.BizController$$FastClassBySpringCGLIB$$1408b36d.invoke(<generated>) ~[classes/:na]
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:721) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:52) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.aop.aspectj.AspectJAfterAdvice.invoke(AspectJAfterAdvice.java:47) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:656) ~[spring-aop-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at com.yj.controller.BizController$$EnhancerBySpringCGLIB$$bc912417.doBiz(<generated>) ~[classes/:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_171]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_171]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_171]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_171]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133) ~[spring-web-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:116) ~[spring-webmvc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827) ~[spring-webmvc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738) ~[spring-webmvc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:963) ~[spring-webmvc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:897) ~[spring-webmvc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970) ~[spring-webmvc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872) ~[spring-webmvc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:648) ~[tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846) ~[spring-webmvc-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:729) ~[tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:230) ~[tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) ~[tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) ~[tomcat-embed-websocket-8.5.11.jar:8.5.11]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) ~[tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) ~[tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) ~[spring-web-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) ~[tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) ~[tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:105) ~[spring-web-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) ~[tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) ~[tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:81) ~[spring-web-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) ~[tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) ~[tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197) ~[spring-web-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.7.RELEASE.jar:4.3.7.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) ~[tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) ~[tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198) ~[tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:474) [tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140) [tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79) [tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) [tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:349) [tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:783) [tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:798) [tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1434) [tomcat-embed-core-8.5.11.jar:8.5.11]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.11.jar:8.5.11]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_171]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_171]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.11.jar:8.5.11]
	at java.lang.Thread.run(Thread.java:748) [na:1.8.0_171]

当我们请求 /doBiz  接口时, 服务返回一个权限异常的错误, 为什么会这样呢? 自然就是我们的权限认证系统起了作为: 当一个方法被调用并且这个方法有 AuthChecker 标注时, 那么首先会执行到我们的 AuthAspect, 在这个 Aspect中, 我们会校验 HTTP 请求的 cookie 字段中是否有携带 userToken 字段时, 如果没有,或者是不等于abc, 则返回权限错误.
那么为了能够正常地调用 /doBiz 接口, 我们可以在 Cookie 中添加 userToken=abc, 这样我们可以愉快的玩耍了:

2018-09-08 11:07:56.199  INFO 9076 --- [nio-8080-exec-2] com.yj.aspect.AuthAspect                 : ==开始进行token校验,token值:abc==
2018-09-08 11:07:56.208  INFO 9076 --- [nio-8080-exec-2] com.yj.service.BizService                : ==开始处理关键业务==

注意, Postman 中点击右上方的cookies可以为domain添加cookies

记录日志

第二个 AOP 实例是记录一个方法调用的log. 这应该是一个很常见的功能了.
首先假设我们有如下需求:

  1. 某个服务下的方法的调用需要有 log: 记录调用的参数以及返回结果,执行时间.

  2. 当方法调用出异常时, 有特殊处理, 例如打印异常 log, 报警等.

根据上面的需求, 我们可以使用 before advice 来在调用方法前打印调用的参数, 使用 after returning advice 在方法返回打印返回的结果. 而当方法调用失败后, 可以使用 after throwing advice 来做相应的处理.使用 around advice, 然后在方法调用前, 记录一下开始时间, 然后在方法调用结束后, 记录结束时间, 它们的时间差就是方法的调用耗时
那么我们来看一下 aspect 的实现:

MethodAspect

@Component
@Aspect
public class MethodAspect {

	private static final Log LOG = LogFactory.getLog(MethodAspect.class);

	@Pointcut("execution(public * com.yj.service..*.*(..))")
	public void methodPointcut() {
	}

	@Around("methodPointcut()")
	public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
		long start = System.currentTimeMillis();
		try {
			Object result = joinPoint.proceed();
			long end = System.currentTimeMillis();
			LOG.info("执行" + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName()
					+ "方法,耗时:" + (end - start) + " ms!");
			return result;
		} catch (Throwable e) {
			long end = System.currentTimeMillis();
			LOG.error(joinPoint + ",耗时:" + (end - start) + " ms,抛出异常 :" + e.getMessage());
			throw e;
		}
	}

	@Before("methodPointcut()")
	public void doBefore(JoinPoint joinPoint) throws Throwable {
		LOG.info("执行" + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName()
				+ "方法," + CommUtils.parseParames(joinPoint.getArgs()));
	}

	@AfterReturning(returning = "ret", pointcut = "methodPointcut()")
	public void doAfterReturning(Object ret) throws Throwable {
		LOG.info("返回值:" + ret);
	}
}

CommUtils的parseParames方法

public static String parseParames(Object[] parames) {

		if (null == parames || parames.length <= 0) {
			return "该方法没有参数";

		}
		StringBuffer param = new StringBuffer("请求参数 # 个:[ ");
		int i = 0;
		for (Object obj : parames) {
			i++;
			if (i == 1) {
				param.append(obj.toString());
				continue;
			}
			param.append(" ,").append(obj.toString());
		}
		return param.append(" ]").toString().replace("#", String.valueOf(i));
	}

这样当 SomeService 类下的方法调用时, 我们所提供的 aspect就会被执行, 因此就可以自动地为我们统计此方法执行参数,返回值和调用耗时

2018-09-08 16:49:34.390  INFO 9076 --- [nio-8080-exec-5] com.yj.aspect.MethodAspect               : 执行com.yj.service.UserService.getUser方法,请求参数 1 个:[ User [name=yj, age=18, orders=null] ]
2018-09-08 16:49:34.391  INFO 9076 --- [nio-8080-exec-5] com.yj.aspect.MethodAspect               : 执行com.yj.service.UserService.getUser方法,耗时:1 ms!
2018-09-08 16:49:34.391  INFO 9076 --- [nio-8080-exec-5] com.yj.aspect.MethodAspect               : 返回值:User [name=yj, age=18, orders=[Order [orderId=1, createTime=2018-09-08 16:49:34]]]

通过上面的两个简单例子, 我们对 Spring AOP 的使用应该有了一个更为深入的了解了. 其实 Spring AOP 的使用的地方不止这些, 例如 Spring 的 声明式事务 就是在 AOP 之上构建的. 读者朋友也可以根据自己的实际业务场景, 合理使用 Spring AOP, 发挥它的强大功能!

猜你喜欢

转载自blog.csdn.net/qq_24313635/article/details/82527342