鉴权
假设现有需求,要求如下:
-
可以定制地为某些指定的 HTTP RESTful api 提供权限验证功能.
-
当调用方的权限不符时, 返回错误.
根据上面所提出的需求, 我们可以进行如下设计:
-
提供一个特殊的注解
AuthChecker
, 这个是一个方法注解, 有此注解所标注的 Controller 需要进行调用方权限的认证. -
利用 Spring AOP, 以 @annotation 切点标志符来匹配有注解
AuthChecker
所标注的 joinpoint. -
在 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. 这应该是一个很常见的功能了.
首先假设我们有如下需求:
-
某个服务下的方法的调用需要有 log: 记录调用的参数以及返回结果,执行时间.
-
当方法调用出异常时, 有特殊处理, 例如打印异常 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, 发挥它的强大功能!