Статьи

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
 
 <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. Во-первых, у нас есть все необходимые схемы — я не думаю, что это нужно объяснить более подробно. Тогда мы имеем:

1
<aop:aspectj-autoproxy/>

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

1
2
3
4
<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. Почему? Потому как…

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

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

UserServiceImpl.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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

01
02
03
04
05
06
07
08
09
10
package pl.grzejszczak.marcin.aop.ui;
 
public abstract class UIComponent {
 protected String componentName;
 
 protected String getComponentName() {
  return componentName;
 }
 
}

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

SomeComponentForAdminAndGuest.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
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

01
02
03
04
05
06
07
08
09
10
11
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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);
 }
}

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

AopTest.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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));
 }
}

И журналы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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 для создания простой инфраструктуры, которая проверяла бы, может ли пользователь создать данный компонент или нет. Благодаря этому после программирования аспектов не нужно помнить о написании кода, связанного с безопасностью, поскольку это будет сделано для него.