В следующем посте будет показано, как в одном из проектов, в которых я принимал участие, мы использовали 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" ?> 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/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. Во-первых, у нас есть все необходимые схемы — я не думаю, что это нужно объяснить более подробно. Тогда мы имеем:
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 для создания простой инфраструктуры, которая проверяла бы, может ли пользователь создать данный компонент или нет. Благодаря этому после программирования аспектов не нужно помнить о написании кода, связанного с безопасностью, поскольку это будет сделано для него.