Статьи

Spring AOP в области безопасности — управление созданием компонентов пользовательского интерфейса с помощью аспектов

 


В следующем посте будет показано, как в одном из проектов, в которых я принимал участие, мы использовали Spring AOP для представления некоторых функций, связанных с безопасностью.
Концепция была такова, что для того, чтобы пользователь мог видеть некоторые компоненты пользовательского интерфейса, ему нужно было иметь определенный уровень привилегий безопасности. Если это требование не было выполнено, то UIComponent не был представлен. Давайте посмотрим на структуру проекта:

Затем были также
aopApplicationContext.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:context="http://www.springframework.org/schema/context"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:aop="http://www.springframework.org/schema/aop"
 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.1.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
 
 <aop:aspectj-autoproxy />
 <context:annotation-config />
 <context:component-scan base-package="pl.grzejszczak.marcin.aop">
  <context:exclude-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/>
 </context:component-scan>
 <bean class="pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor" factory-method="aspectOf"/>
 
</beans>

Теперь давайте посмотрим на наиболее интересные строки контекста приложения Spring.

Во-первых, у нас есть все необходимые схемы — я не думаю, что это нужно объяснить более подробно.

Тогда мы имеем:

<aop:aspectj-autoproxy/>

который включает
поддержку
@AspectJ .

Далее идет

<context:annotation-config />
<context:component-scan base-package="pl.grzejszczak.marcin.aop">
    <context:exclude-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/>
</context:component-scan>

Сначала мы включаем конфигурацию Spring с помощью аннотаций. Затем мы намеренно исключаем аспекты из инициализации как бинов самой Spring. Зачем? Потому как…

<bean class="pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor" factory-method="aspectOf"/>

мы хотим создать аспект самостоятельно и предоставить factory-method = «aspectOf». Таким образом, наш аспект будет включен в процесс автоматического подключения наших bean-компонентов — таким образом, все поля, помеченные аннотацией @Autowired, будут вводить bean-компоненты.

Теперь давайте перейдем к коду:

UserServiceImpl.java

package pl.grzejszczak.marcin.aop.service;
 
import org.springframework.stereotype.Service;
 
import pl.grzejszczak.marcin.aop.type.Role;
import pl.grzejszczak.marcin.aop.user.UserHolder;
 
@Service
public class UserServiceImpl implements UserService {
 private UserHolder userHolder;
 
 @Override
 public UserHolder getCurrentUser() {
  return userHolder;
 }
 
 @Override
 public void setCurrentUser(UserHolder userHolder) {
  this.userHolder = userHolder;
 }
 
 @Override
 public Role getUserRole() {
  if (userHolder == null) {
   return null;
  }
  return userHolder.getUserRole();
 }
}

Класс UserServiceImpl имитирует сервис, который будет получать информацию о текущем пользователе из БД или из текущего контекста приложения.

UserHolder.java

package pl.grzejszczak.marcin.aop.user;
 
import pl.grzejszczak.marcin.aop.type.Role;
 
public class UserHolder {
 private Role userRole;
 
 public UserHolder(Role userRole) {
  this.userRole = userRole;
 }
 
 public Role getUserRole() {
  return userRole;
 }
 
 public void setUserRole(Role userRole) {
  this.userRole = userRole;
 }
}

Это простой класс-держатель, содержащий информацию о текущей роли пользователя.

Role.java

package pl.grzejszczak.marcin.aop.type;
 
 
public enum Role {
 ADMIN("ADM"), WRITER("WRT"), GUEST("GST");
 
 private String name;
 
 private Role(String name) {
  this.name = name;
 }
 
 public static Role getRoleByName(String name) {
 
  for (Role role : Role.values()) {
 
   if (role.name.equals(name)) {
    return role;
   }
  }
 
  throw new IllegalArgumentException("No such role exists [" + name + "]");
 }
 
 public String getName() {
  return this.name;
 }
 
 @Override
 public String toString() {
  return name;
 }
}

Роль — это перечисление, определяющее роль человека, являющегося
администратором ,
писателем или
гостем .

UIComponent.java

package pl.grzejszczak.marcin.aop.ui;
 
public abstract class UIComponent {
 protected String componentName;
 
 protected String getComponentName() {
  return componentName;
 }
 
}

Абстракция над конкретными реализациями некоторых компонентов пользовательского интерфейса.

SomeComponentForAdminAndGuest.java

package pl.grzejszczak.marcin.aop.ui;
 
import pl.grzejszczak.marcin.aop.annotation.SecurityAnnotation;
import pl.grzejszczak.marcin.aop.type.Role;
 
@SecurityAnnotation(allowedRole = { Role.ADMIN, Role.GUEST })
public class SomeComponentForAdminAndGuest extends UIComponent {
 
 public SomeComponentForAdminAndGuest() {
  this.componentName = "SomeComponentForAdmin";
 }
 
 public static UIComponent getComponent() {
  return new SomeComponentForAdminAndGuest();
 }
}

Этот компонент является примером расширения компонента пользовательского интерфейса, которое могут видеть только пользователи, имеющие роли
администратора или
гостя .

SecurityAnnotation.java

package pl.grzejszczak.marcin.aop.annotation;
 
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
 
import pl.grzejszczak.marcin.aop.type.Role;
 
@Retention(RetentionPolicy.RUNTIME)
public @interface SecurityAnnotation {
 Role[] allowedRole();
}

Аннотация, определяющая роли, в которых может быть создан этот компонент.

UIFactoryImpl.java

package pl.grzejszczak.marcin.aop.ui;
 
import org.apache.commons.lang.NullArgumentException;
import org.springframework.stereotype.Component;
 
@Component
public class UIFactoryImpl implements UIFactory {
 
 @Override
 public UIComponent createComponent(Class<? extends UIComponent> componentClass) throws Exception {
  if (componentClass == null) {
   throw new NullArgumentException("Provide class for the component");
  }
  return (UIComponent) Class.forName(componentClass.getName()).newInstance();
 }
}

Фабричный класс, который дал класс объекта, который расширяет UIComponent, возвращает новый экземпляр данного UIComponent.

SecurityInterceptor.java

package pl.grzejszczak.marcin.aop.interceptor;
 
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.Arrays;
import java.util.List;
 
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
 
import pl.grzejszczak.marcin.aop.annotation.SecurityAnnotation;
import pl.grzejszczak.marcin.aop.service.UserService;
import pl.grzejszczak.marcin.aop.type.Role;
import pl.grzejszczak.marcin.aop.ui.UIComponent;
 
@Aspect
public class SecurityInterceptor {
 private static final Logger LOGGER = LoggerFactory.getLogger(SecurityInterceptor.class);
 
 public SecurityInterceptor() {
  LOGGER.debug("Security Interceptor created");
 }
 
 @Autowired
 private UserService userService;
 
 @Pointcut("execution(pl.grzejszczak.marcin.aop.ui.UIComponent pl.grzejszczak.marcin.aop.ui.UIFactory.createComponent(..))")
 private void getComponent(ProceedingJoinPoint thisJoinPoint) {
 }
 
 @Around("getComponent(thisJoinPoint)")
 public UIComponent checkSecurity(ProceedingJoinPoint thisJoinPoint) throws Throwable {
  LOGGER.info("Intercepting creation of a component");
 
  Object[] arguments = thisJoinPoint.getArgs();
  if (arguments.length == 0) {
   return null;
  }
 
  Annotation annotation = checkTheAnnotation(arguments);
  boolean securityAnnotationPresent = (annotation != null);
 
  if (securityAnnotationPresent) {
   boolean userHasRole = verifyRole(annotation);
   if (!userHasRole) {
    LOGGER.info("Current user doesn't have permission to have this component created");
    return null;
   }
  }
  LOGGER.info("Current user has required permissions for creating a component");
  return (UIComponent) thisJoinPoint.proceed();
 }
 
 /**
  * Basing on the method's argument check if the class is annotataed with
  * {@link SecurityAnnotation}
  *
  * @param arguments
  * @return
  */
 private Annotation checkTheAnnotation(Object[] arguments) {
  Object concreteClass = arguments[0];
  LOGGER.info("Argument's class - [{}]", new Object[] { arguments });
  AnnotatedElement annotatedElement = (AnnotatedElement) concreteClass;
  Annotation annotation = annotatedElement.getAnnotation(SecurityAnnotation.class);
  LOGGER.info("Annotation present - [{}]", new Object[] { annotation });
  return annotation;
 }
 
 /**
  * The function verifies if the current user has sufficient privilages to
  * have the component built
  *
  * @param annotation
  * @return
  */
 private boolean verifyRole(Annotation annotation) {
  LOGGER.info("Security annotation is present so checking if the user can use it");
  SecurityAnnotation annotationRule = (SecurityAnnotation) annotation;
  List<Role> requiredRolesList = Arrays.asList(annotationRule.allowedRole());
  Role userRole = userService.getUserRole();
  return requiredRolesList.contains(userRole);
 }
}

Это
аспект определяется на
срезе точек на
выполнение функции createComponent из UIFactory интерфейса. В
совете
Around
есть логика, которая сначала проверяет, какой тип аргумента был передан методу createComponent (например, SomeComponentForAdminAndGuest.class). Затем он проверяет, аннотирован ли этот класс SecurityAnnotation, и если это так, он проверяет, какие роли необходимы для создания компонента. Затем он проверяет, имеет ли текущий пользователь (от UserService до ролей UserHolder) необходимую роль для представления компонента. Если это так, то вызывается thisJoinPoint.proceed (), который фактически возвращает объект класса, расширяющего UIComponent.

Теперь давайте проверим это — вот идет SpringJUnit4ClassRunner

AopTest.java

package pl.grzejszczak.marcin.aop;
 
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import pl.grzejszczak.marcin.aop.service.UserService;
import pl.grzejszczak.marcin.aop.type.Role;
import pl.grzejszczak.marcin.aop.ui.SomeComponentForAdmin;
import pl.grzejszczak.marcin.aop.ui.SomeComponentForAdminAndGuest;
import pl.grzejszczak.marcin.aop.ui.SomeComponentForGuest;
import pl.grzejszczak.marcin.aop.ui.SomeComponentForWriter;
import pl.grzejszczak.marcin.aop.ui.UIFactory;
import pl.grzejszczak.marcin.aop.user.UserHolder;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:aopApplicationContext.xml" })
public class AopTest {
 
 @Autowired
 private UIFactory uiFactory;
 
 @Autowired
 private UserService userService;
 
 @Test
 public void adminTest() throws Exception {
  userService.setCurrentUser(new UserHolder(Role.ADMIN));
  Assert.assertNotNull(uiFactory.createComponent(SomeComponentForAdmin.class));
  Assert.assertNotNull(uiFactory.createComponent(SomeComponentForAdminAndGuest.class));
  Assert.assertNull(uiFactory.createComponent(SomeComponentForGuest.class));
  Assert.assertNull(uiFactory.createComponent(SomeComponentForWriter.class));
 }
}

И журналы:

pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:26 Security Interceptor created
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:38 Intercepting creation of a component
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:48 Argument's class - [[class pl.grzejszczak.marcin.aop.ui.SomeComponentForAdmin]]
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:54 Annotation present - [@pl.grzejszczak.marcin.aop.annotation.SecurityAnnotation(allowedRole=[ADM])]
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:57 Security annotation is present so checking if the user can use it
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:70 Current user has required permissions for creating a component
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:38 Intercepting creation of a component
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:48 Argument's class - [[class pl.grzejszczak.marcin.aop.ui.SomeComponentForAdminAndGuest]]
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:54 Annotation present - [@pl.grzejszczak.marcin.aop.annotation.SecurityAnnotation(allowedRole=[ADM, GST])]
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:57 Security annotation is present so checking if the user can use it
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:70 Current user has required permissions for creating a component
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:38 Intercepting creation of a component
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:48 Argument's class - [[class pl.grzejszczak.marcin.aop.ui.SomeComponentForGuest]]
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:54 Annotation present - [@pl.grzejszczak.marcin.aop.annotation.SecurityAnnotation(allowedRole=[GST])]
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:57 Security annotation is present so checking if the user can use it
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:66 Current user doesn't have permission to have this component created
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:38 Intercepting creation of a component
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:48 Argument's class - [[class pl.grzejszczak.marcin.aop.ui.SomeComponentForWriter]]
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:54 Annotation present - [@pl.grzejszczak.marcin.aop.annotation.SecurityAnnotation(allowedRole=[WRT])]
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:57 Security annotation is present so checking if the user can use it
pl.grzejszczak.marcin.aop.interceptor.SecurityInterceptor:66 Current user doesn't have permission to have this component created

Модульный тест показывает, что для данной роли администратора создаются только первые два компонента, тогда как для двух других возвращаются нулевые значения (из-за того, что у пользователя нет соответствующих прав).

Именно так в нашем проекте мы использовали AOP Spring для создания простой инфраструктуры, которая проверяла бы, может ли пользователь создать данный компонент или нет. Благодаря этому после программирования аспектов не нужно помнить о написании кода, связанного с безопасностью, поскольку это будет сделано для него.

Если у вас есть предложения, связанные с этим постом, пожалуйста, не стесняйтесь комментировать ?