Spring框架系列(五)--Spring AOP以及实现用户登录权限控制

一、背景:

当需要为多个不具有继承关系的对象引入一个公共行为,例如日志、权限验证等功能时。

如果使用OOP,需要为每个Bean引入这些公共行为。会产生大量重复代码,并且不利用维护。

AOP就是为了解决这个问题。

二、AOP:

AOP可以理解一种思想,不是Java独有的,作用是对方法进行拦截处理或增强处理。而在Java中我们使用Spring AOP和AspectJ。

1、Spring AOP:

基于动态代理实现,如果目标对象有实现接口,使用jdk proxy,如果目标对象没有实现接口,使用cglib。

然后从容器获取代理后的对象,在运行期植入“切面”类的方法。Spring AOP需要依赖于IOC容器来管理,只能作用于Spring容器中的Bean。

Spring AOP可以使用注解或者XML配置的方式,而类似@Aspect、@Pointcut等都是AspectJ的注解,但是通过Spring去实现,只是沿用AspectJ的概念。

通常Spring AOP足够日常开发,一般使用不到AspectJ。

Spring AOP在运行时生成代理对象来织入的,还可以在编译期、类加载期织入,比如AspectJ。

2、AspectJ:

AspectJ在实际代码运行前完成了织入,所以它生成的类是没有额外运行时开销的。

而Spring AOP基于动态代理,生成一个代理类,这样栈深度更深,效率理论上要差于AspectJ。

AspectJ功能更强大,是AOP的完整解决方案。

AspectJ除了注解,个人不太了解,这里就不细讲了。

动态代理请参考:https://www.cnblogs.com/huigelaile/p/10980045.html

三、Spring AOP应用:

1、参数校验。

2、MySQL读写分离。

3、权限校验。

4、日志记录。

5、信息过滤,页面转发等功能。

四、Spring AOP术语:

在一个或多个连接点上,可以把切面的功能(通知)织入到程序的执行过程中

1、增强Advice:

方法层面的增强。对某个方法进行增强的方法,分为:Before、After、After-returning、After-throwing、Around。

try {
   //@Before
   result = method.invoke(target, args);
   //@After
   return result;
} catch (InvocationTargetException e) {
   Throwable targetException = e.getTargetException();
   //@AfterThrowing
   throw targetException;
} finally {
   //@AfterReturning
}

2、连接点Join Point:

可以被拦截到的点。也就是可以被增强的方法都是连接点。

3、切点Pointcut:

joint point的组合,通常使用类和方法名称或者正则表达式匹配来指定切点。

4、切面Aspect:

切面是Advice和Pointcut的结合,他们共同定义了在何时和何处完成其功能。

5、引入Introduction:

向现有的类添加新方法或属性。

6、织入Weaving:

把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。

在目标对象的生命里有多个点可以进行织入:编译器、类加载期、运行期。

7、目标对象Target:

织入 Advice 的目标对象。

Spring对AOP的支持:由于基于动态代理实现,所以只支持方法级别的连接点。

1、基于代理的经典Spring AOP。

2、纯POJO切面。

3、@AspectJ注解驱动的切面。

4、注入式AspectJ切面(适用于Spring各版本)。

五、Spring AOP实现:

1、添加Maven依赖:

  1).aspectjweaver

  2).如果使用Spring Boot:spring-boot-starter-aop

2、首先开启Spring AOP支持:

XML方式:<aop:aspectj-autoproxy/>

注解方式:@EnableAspectJAutoProxy,所有被@aspect配置的Bean,都是Aspect

3、基于XML(schema-based)

Spring AOP要使用AspectJ的切点表达式定义切点:

@execution:上面使用了execution来正则匹配方法,是最常用的。也可以使用其他的指示器。

execution表达式以*开始,表示不关心方法返回值的类型,两个点号(..)表名切点要选择任意的perform()方法,无论方法的参数是什么。

@within:指定所在类或所在包下面的方法

例如:@Pointcut("within(com.it.aop.AService..*)")

@annotation:方法上具有特定的注解,如@Subscribe用于订阅特定的事件。

例如:@Pointcut("execution(* .(..)) && @annotation(com.javadoop.annotation.Subscribe)")

@bean:匹配bean的名字

例如:@Pointcut("bean(*Service)")

PS:通常 "." 代表一个包名,".." 代表包及其子包,方法参数任意匹配使用两个点 ".."。

举个栗子:

public class AService {

    public void add() {
        System.out.println("add");
    }
}
public class LogRecord {

    public void log() {
        System.out.println("record log");
    }

    public void transaction() {
        System.out.println("transaction");
    }

    public void permission() {
        System.out.println("permission");
    }
}
<aop:config>    <!--顶层的AOP配置元素。大多数aop元素都在这内部 -->
        <!--声明一个切面 -->
        <aop:aspect ref="logRecord">
            <!--前置通知 -->
            <aop:before pointcut="execution(** com.it.aop.AService.add(..))" method="log" />
            <aop:before pointcut="execution(** com.it.aop.AService.add(..))" method="permission" />
            <!--返回通知 -->
            <aop:after-returning pointcut="execution(** com.it.aop.AService.add(..))" method="log" />
            <!--异常通知 -->
            <aop:after-throwing pointcut="execution(** com.it.aop.AService.add(..))" method="transaction" />
        </aop:aspect>
    </aop:config>
上述代码中,Pointcut都是相同的我们就可以声明<aop:pointcut>,如果把<aop:pointcut>作为<aop:config>的直接子元素,将作为全局Pointcut

<aop:config>    <!--顶层的AOP配置元素。大多数aop元素都在这内部 -->
        <!--声明一个切面 -->
        <aop:aspect ref="logRecord">
            <aop:pointcut id="add" expression="execution(** com.it.aop.AService.add(..))" />
            <aop:before pointcut-ref="add" method="log" />
        </aop:aspect>
    </aop:config>
环绕通知
public void around(MethodInvocationProceedingJoinPoint point) {
        try {
            System.out.println("record log");
            System.out.println("permission");
            point.proceed();
            System.out.println("record log");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
<aop:config>    <!--顶层的AOP配置元素。大多数aop元素都在这内部 -->
        <!--声明一个切面 -->
        <aop:aspect ref="logRecord">
            <aop:pointcut id="add" expression="execution(** com.it.aop.AService.add(..))" />
            <!--环绕通知 -->
            <aop:around pointcut-ref="add" method="around" />
        </aop:aspect>
    </aop:config>

4、基于注解(@AspectJ)实现:

@AspectJ和AspectJ没多大关系,仅仅是使用了AspectJ中的概念,注解来自于AspectJ的包,但是实现还是Spring AOP来的。

@Aspect
public class LogRecord {

    @Pointcut("execution(** com.it.aop.AService.add(..))")
    public void add() {}

    @Before("add()")
    public void log() {
        System.out.println("record log");
    }
    @AfterThrowing("add()")
    public void transaction() {
        System.out.println("transaction");
    }
    @Before("add()")
    public void permission() {
        System.out.println("permission");
    }
    @Around("add()")
    public void around(MethodInvocationProceedingJoinPoint point) {
        try {
            System.out.println("record log");
            System.out.println("permission");
            point.proceed();
            System.out.println("record log");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}

PS:Spring通常建议创建一个SystemArchitecture类,里面定义Pointcut,然后在需要的地方去引用。例如,在@Aspect的Bean中使用@Before("com.it.SystemArchitecture.A")

Spring AOP通过@annotation实现权限控制

PS:这里只是校验部分API的登录状态和用户权限,如果系统要求登录过后才能请求,肯定就选择拦截器了。

1、首先定义两个注解

//@CheckLogin通过Cookie是否包含X-Token验证用户是否登录
public @interface CheckLogin {

}

//@CheckAuthorization("**")校验用户是否登录,权限**是否满足
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckAuthorization {
    String value();
}

2、注解的AOP处理

/* *
 * Description: 通过校验jwt中token实现登录和用户权限校验
**/
@Aspect        //定义为一个切面
@Component    //必须声明为Bean
@RequiredArgsConstructor(onConstructor = @__(@Autowired))    //lombok实现IOC,相比直接通过@Autowired更有优势
public class AuthAspect {

    private final JwtOperator jwtOperator;    //jwt操作类

    //@CheckLogin注解操作
    @Around("@annotation(com.diamondshine.auth.CheckLogin)")
    public Object checkLogin(ProceedingJoinPoint point) throws Throwable {
        checkToken();
        return point.proceed();
    }

    private void checkToken() {
        try {
            // 1. 从header里面获取token
            HttpServletRequest request = getHttpServletRequest();

            Cookie[] cookies = request.getCookies();
            String token = "";
            for (Cookie cookie : cookies) {
                if (StringUtils.equals("X-Token", cookie.getName())) {
                    token = cookie.getValue();
                    break;
                }
            }

            // 2. 校验token是否合法&是否过期;如果不合法或已过期直接抛异常;如果合法放行
            Boolean isValid = jwtOperator.validateToken(token);
            if (!isValid) {
                throw new SecurityException("Token不合法!");
            }

            // 3. 如果校验成功,那么就将用户的信息设置到request的attribute里面
            Claims claims = jwtOperator.getClaimsFromToken(token);
            request.setAttribute("id", Long.valueOf(claims.get("id").toString()));
            request.setAttribute("role", claims.get("role"));
        } catch (Throwable throwable) {
            throw new SecurityException("Token不合法!");
        }
    }

    private HttpServletRequest getHttpServletRequest() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
        return attributes.getRequest();
    }

    //@CheckAuthorization注解操作
    @Around("@annotation(com.diamondshine.auth.CheckAuthorization)")
    public Object checkAuthorization(ProceedingJoinPoint point) throws Throwable {
        try {
            // 1. 验证token是否合法;
            this.checkToken();
            // 2. 验证用户角色是否匹配
            HttpServletRequest request = getHttpServletRequest();
            List<String> list = (List<String>) request.getAttribute("role");

            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();
            //获取@CheckAuthorization注解值
            CheckAuthorization annotation = method.getAnnotation(CheckAuthorization.class);

            String value = annotation.value();

            //ROLE_ADMIN,用户权限必须为admin。如果注解为ROLE_ADMIN_USER,用户权限为admin/user都可以
            list.forEach(role -> {
                if (!(("ROLE_ADMIN".equals(value) && "ROLE_ADMIN".contains(role)) || ("ROLE_ADMIN_USER".equals(value) && "ROLE_ADMIN_USER".contains(role)))) {
                    throw new SecurityException("用户无权访问!");
                }
            });

        } catch (Throwable throwable) {
            if (StringUtils.isNotBlank(throwable.getMessage())) {
                throw new SecurityException(throwable.getMessage(), throwable);
            } else {
                throw new SecurityException("用户无权访问!", throwable);
            }
        }
        return point.proceed();
    }
}

PS:内部使用jwt获取token中的信息,这段不重要,根据要求去实现代码,参考这种AOP实现方式。

3、简单使用

/* *
 * Description: 用户必须登录,权限为ROLE_ADMIN_USER
**/
@CheckAuthorization("ROLE_ADMIN_USER")
@PostMapping(value = "/hahaha")
@ResponseBody
public CustomizeResponse subscribeHouse(@RequestParam(value = "house_id") Long houseId) {
    //***
}

/* *
 * Description: 只需要用户登录状态
**/
@CheckLogin
@GetMapping("rent/house/show/{id}")
public String showHouseDetail(@PathVariable(value = "id") Long houseId, Model model) {
    //***
}

4、SecurityException异常处理,返回json数据

@Slf4j
@RestControllerAdvice
public class GlobalExceptionErrorHandler {
    @ExceptionHandler(SecurityException.class)
    public ResponseEntity<ErrorBody> error(SecurityException e) {
        log.warn("发生SecurityException异常", e);
        return new ResponseEntity<>(
                ErrorBody.builder()
                        .body(e.getMessage())
                        .status(HttpStatus.UNAUTHORIZED.value())
                        .build(),
                HttpStatus.UNAUTHORIZED
        );
    }
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class ErrorBody {
    private String body;
    private int status;
}

如果权限不合法,页面返回。

参考:https://mp.weixin.qq.com/s/KOV_lWOTPYMi8-2kFZCS9Q

posted @ 2019-06-05 22:02  Diamond-Shine  阅读(2053)  评论(0编辑  收藏  举报