Статьи

Пример Spring Security Run-As с использованием аннотаций и конфигурации пространства имен

Spring Security предлагает функцию замены проверки подлинности, часто называемую Run-As, которая может заменить проверку подлинности текущего пользователя (и, следовательно, разрешения) при вызове одного защищенного объекта. Использование этой функции имеет смысл, когда бэкэнд-система, вызванная во время обработки запроса, требует других привилегий, чем текущее приложение. 
Например, приложению может потребоваться открыть журнал финансовых транзакций для текущего пользователя, вошедшего в систему, но внутренняя система, предоставляющая его, разрешает это действие только членам специальной роли «аудитор». Приложение не может просто назначить эту роль пользователю, поскольку это может позволить ему выполнять другие ограниченные действия. Вместо этого пользователю может быть предоставлено это право исключительно для просмотра его журнала транзакций.

Только два класса используются для реализации этой функции. Экземплярам RunAsManagerпоручено создать фактические токены аутентификации для замены. Разумная реализация по умолчанию уже предоставлена ​​Spring Security. Как и в случае других типов аутентификации, необходимо также зарегистрировать соответствующий экземпляр AuthenticationProvider.

<bean id="runAsManager"  
    class="org.springframework.security.access.intercept.RunAsManagerImpl">  
  <property name="key" value="my_run_as_key"/>  
</bean>  
  
<bean id="runAsAuthenticationProvider"  
    class="org.springframework.security.access.intercept.RunAsImplAuthenticationProvider">  
  <property name="key" value="my_run_as_key"/>  
</bean>  
Токены, которые были созданы
runAsManager
, подписываются предоставленным ключом (
my_run_as_key
в приведенном выше примере), а затем сверяются с тем же ключом
runAsAuthenticationProvider
, чтобы снизить риск получения поддельных токенов. Эти ключи могут иметь любое значение, но они должны быть одинаковыми в обоих объектах. В противном случае,
runAsAuthenticationProvider
полученные токены будут отклонены как недействительные.

Если экземпляр зарегистрирован, RunAsManagerон будет вызываться AbstractSecurityInterceptorдля каждого перехваченного вызова объекта, для которого пользователю уже был предоставлен доступ. Если RunAsManagerвозвращает токен, этот токен будет использоваться вместо исходного в течение всего времени вызова, предоставляя пользователю различные привилегии. Здесь есть два ключевых момента. Чтобы функция замены аутентификации могла что-либо сделать, вызов должен быть фактически защищен (и, таким образом, перехвачен), и пользователю уже должен быть предоставлен доступ.

Чтобы зарегистрировать RunAsManagerэкземпляр с помощью метода-перехватчика безопасности, требуется нечто похожее на следующее:

<global-method-security secured-annotations="enabled" run-as-manager-ref="runAsManager"/>  

Now, all methods secured by the @Secured annotation will be able to trigger RunAsManager. One important point here is that global-method-security will only work in the Spring context in which it is defined. In Spring MVC applications, there usually are two Spring contexts: the parent context, attached to ContextLoaderListener, and the child context, attached toDispatcherServlet. To secure Controller methods in this way, global-method-security must be added to DispatcherServlet‘s context. To secure methods in beans not in this context, global-method-security should also be added to ContextLoaderListener‘s context. Otherwise, security annotations will be ignored.

The default implementation of RunAsManager (RunAsManagerImpl) will inspect the secured object’s configuration and if it finds any attributes prefixed with RUN_AS_, it will create a token identical to the original, with the addition of one new GrantedAuthorty per RUN_AS_ attribute found. The new GrantedAuthority will be a role (prefixed by ROLE_ by default) named like the found attribute without the RUN_AS_ prefix.

So, if a user with a role ROLE_REGISTERED_USER invokes a method annotated with @Secured({"ROLE_REGISTERED_USER","RUN_AS_AUDITOR"}), e.g.

@Controller  
public class TransactionLogController {  
  
 @Secured({"ROLE_REGISTERED_USER","RUN_AS_AUDITOR"}) //Authorities needed for method access and authorities added by RunAsManager prefixed with RUN_AS_  
 @RequestMapping(value = "/transactions",  method = RequestMethod.GET) //Spring MVC configuration. Not related to security  
 @ResponseBody //Spring MVC configuration. Not related to security  
 public List<Transaction> getTransactionLog(...) {  
  ... //Invoke something in the backend requiring ROLE_AUDITOR  
 {  
  
 ...  //User does not have ROLE_AUDITOR here  
}  

the resulting token created by RunAsManagerImpl with be granted ROLE_REGISTERED_USER and ROLE_AUDITOR. Thus, the user will also be allowed actions, normally reserved for ROLE_AUDITOR members, during the current invocation, permitting them, in this case, to access the transaction log.To enable runAsAuthenticationProvider, register it as usual:

<authentication-manager alias="authenticationManager">  
    <authentication-provider ref="runAsAuthenticationProvider"/>  
    ... other authentication-providers used by the application ...  
</authentication-manager>  

This is all that is necessary to have the default implementation activated.

Still, this setting will not work for methods secured by @PreAuthorize and @PostAuthorize annotations as their configuration attributes are differently evaluated (they are SpEL expressions and not a simple list or required authorities like with @Secured) and will not be recognized by RunAsManagerImpl. For this scenario to work, a custom RunAsManager implementation is required, as, at least at the time of writing, no applicable implementation is provided by Spring.

A custom RunAsManager implementation for use with @PreAuthorize/@PostAuthorize

A convenient implementation relying on a custom annotation is provided below:

public class AnnotationDrivenRunAsManager extends RunAsManagerImpl {  
  
    @Override  
    public Authentication buildRunAs(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {  
        if(!(object instanceof ReflectiveMethodInvocation) || ((ReflectiveMethodInvocation)object).getMethod().getAnnotation(RunAsRole.class) == null) {  
            return super.buildRunAs(authentication, object, attributes);  
        }  
  
        String roleName = ((ReflectiveMethodInvocation)object).getMethod().getAnnotation(RunAsRole.class).value();  
         
        if (roleName == null || roleName.isEmpty()) {  
            return null;  
        }  
  
        GrantedAuthority runAsAuthority = new SimpleGrantedAuthority(roleName);  
        List<GrantedAuthority> newAuthorities = new ArrayList<GrantedAuthority>();  
        // Add existing authorities  
        newAuthorities.addAll(authentication.getAuthorities());  
        // Add the new run-as authority  
        newAuthorities.add(runAsAuthority);  
  
        return new RunAsUserToken(getKey(), authentication.getPrincipal(), authentication.getCredentials(),  
                newAuthorities, authentication.getClass());  
    }  
}  

This implementation will look for a custom @RunAsRole annotation on a protected method (e.g. @RunAsRole("ROLE_AUDITOR")) and, if found, will add the given authority (ROLE_AUDITOR in this case) to the list of granted authorities. RunAsRole itself is just a simple custom annotation:

@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.METHOD)  
public @interface RunAsRole {  
    String value();  
}  

This new implementation would be instantiated in the same way as before:

<bean id="runAsManager"  
    class="org.springframework.security.access.intercept.RunAsManagerImpl">  
  <property name="key" value="my_run_as_key"/>  
</bean>  

And registered in a similar fashion:

<global-method-security pre-post-annotations="enabled" run-as-manager-ref="runAsManager">  
    <expression-handler ref="expressionHandler"/>  
</global-method-security> 

The expression-handler is always required for pre-post-annotations to work. It is a part of the standard Spring Security configuration, and not related to the topic described here. Both pre-post-annotations and secured-annotations can be enabled at the same time, but should never be used in the same class. The protected controller method from above could now look like this:

@Controller  
public class TransactionLogController {  
  
 @PreAuthorize("hasRole('ROLE_REGISTERED_USER')") //Authority needed to access the method  
 @RunAsRole("ROLE_AUDITOR") //Authority added by RunAsManager  
 @RequestMapping(value = "/transactions",  method = RequestMethod.GET) //Spring MVC configuration. Not related to security  
 @ResponseBody //Spring MVC configuration. Not related to security  
 public List<Transaction> getTransactionLog(...) {  
  ... //Invoke something in the backend requiring ROLE_AUDITOR  
 {  
  
 ... //User does not have ROLE_AUDITOR here  
}