The Spring Framework Decision System
After a more thorough analysis of the Spring framework I solved the issue with a Spring Bean that implements the AccessDecisionVoter
. A Voter is a class that Spring invokes to tell if a given credential can access a given resource / em>.
Spring allows you to chain several Voters to make a decision. Each Voter votes in favor, against or abstains from the decision. The final decision is AccessDecisionManager
, which has the following implementations:
-
AffirmativeBased
: allows access if at least one Voter voted in favor.
-
UnanimousBased
: allows access only if everyone voted in favor, ignoring abstention.
-
ConsensusBased
: allows or denies access according to the majority.
In this way, I can implement different Voters that respond to different situations and abstain from others. In addition, I choose the decision strategy that best fits my system.
The above statements can be best understood with the following diagram:
Note:IwasgiventhesuggestiontoextendAccessDecisionManager
,whichwouldsolvetheproblem,butatthesametimewouldoverwritepracticallycompletelySpringSecurity.Soitseemedtomebesttoworkonamorespecificpoint.
Implementation
Voterisnotacomplextask,butittookseveralteststounderstandtheoperation,thevaluesreceivedintheparameters,andthegeneralbehavioroftheframework,asthereisnotadetaileddocumentationonthat.BelowisasimplifiedimplementationofwhatIdid,withoutmydomain-specificcode:
public class CustomVoter implements AccessDecisionVoter<Object> {
final protected Logger logger = LoggerFactory.getLogger(getClass());
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
logger.info("### Controle de Acesso ###");
//verifica se as credenciais são do tipo esperado
if (authentication.getPrincipal() instanceof CustomUserDetails) {
CustomUserDetails user = (CustomUserDetails) authentication.getPrincipal();
GerenciadorPermissao gerenciadorPermissao = user.getGerenciadorPermissao();
//variável de sessão (HTTP) armazenada numa determinada tela via JSF
HttpSession session = (HttpSession) FacesContext.getCurrentInstance().getExternalContext().getSession(true);
Integer variavelSessao = (Integer) session.getAttribute("variavelSessao");
Boolean result = null;
for (ConfigAttribute configAttribute : attributes) {
String attr = configAttribute.getAttribute();
if (attr.equals("ROLE_USER")) {
//ROLE_USER é a ROLE de usuário, logado, então retorna true sempre
result = true;
} else {
//chama uma lógica específica que verifica se o usuário possui permissão no contexto atual
result = gerenciadorPermissao.verificarPermissao(variavelSessao, attr);
}
}
if (result == null || result == Boolean.FALSE) {
logger.info(" -> Acesso Negado!");
return ACCESS_DENIED;
} else {
logger.info(" -> Acesso Permitido!");
return ACCESS_GRANTED;
}
} else {
System.out.println(" -> Não é do tipo CustomUserDetails!");
return ACCESS_ABSTAIN;
}
}
}
Some relevant code points above:
- The
vote()
method is called by Spring whenever it needs to make a decision.
- The first parameter, of type
Authentication
, returns information about the current login.
- The
getPrincipal
method of the Authentication
instance returns the user information that I set at login time. In this case it is an instance of CustomUserDetails
, which implements the interface UserDetails
of Spring.
- As can be seen, it is not at all complicated to access session values (
HttpSession
) on web systems with JSF.
- The parameter of type
Object
returns the resource being accessed. Depending on the context, it could be the URL of a page, it could be a reference to a method, etc.
- The last parameter contains a list of the attributes, rules, or roles that should be checked.
Configuration
The following XML document is a simplification of the Spring Security configuration used in my application. I published it in full, because in some examples I found on other sites it was not clear how a bean related to the others.
In short, the configuration contains:
- General Spring Security settings.
- Voter declaration and its association with a
AccessDecisionManager
of type UnanimousBased
.
- Security of some pages associated with
AccessDecisionManager
declared.
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
<!-- Configura o servlet filter do Spring Security -->
<beans:bean id="springSecurityFilterChain" class="org.springframework.security.web.FilterChainProxy">
<beans:constructor-arg>
<beans:list>
<filter-chain filters="securityContextPersistenceFilter" pattern="/**" />
</beans:list>
</beans:constructor-arg>
</beans:bean>
<beans:bean id="securityContextPersistenceFilter" class="org.springframework.security.web.context.SecurityContextPersistenceFilter">
<beans:property name="forceEagerSessionCreation" value="true" />
</beans:bean>
<!-- Configura handlers para sucesso de autenticação, falha de autenticação e acesso negado a um determinado recurso -->
<beans:bean
id="customAuthenticationSuccessHandler"
class="br.com.starcode.commons.security.CustomAuthenticationSuccessHandler" />
<beans:bean
id="customAuthenticationFailureHandler"
class="br.com.starcode.commons.security.CustomAuthenticationFailureHandler" />
<beans:bean
id="customAccessDeniedHandler"
class="br.com.starcode.commons.security.CustomAccessDeniedHandler" />
<!-- Voter customizado -->
<beans:bean id="customVoter" class="br.com.starcode.commons.security.CustomVoter" />
<!-- Define AccessDesisionManager como UnanimousBased e coloca o Voter na lista -->
<beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased">
<beans:constructor-arg>
<beans:list>
<beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
<beans:ref bean="customVoter" />
</beans:list>
</beans:constructor-arg>
</beans:bean>
<!-- Configura a criptografia (hash) da senha -->
<beans:bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder">
<beans:constructor-arg>
<beans:value>11</beans:value>
</beans:constructor-arg>
</beans:bean>
<!-- Configura o AuthenticationManager com os beans da aplicação -->
<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref="accessControlService">
<password-encoder ref="passwordEncoder" />
</authentication-provider>
</authentication-manager>
<!-- Não coloca segurança em Javascript, CSS e outros recursos "estáticos" do JSF -->
<http pattern="/javax.faces.resource/**" security="none" />
<!-- Não coloca segurança na tela de login -->
<http pattern="/login.xhtml*" security="none" />
<!-- Define a segurança para os demais recursos -->
<http auto-config="true" use-expressions="false" access-decision-manager-ref="accessDecisionManager">
<!-- Referência ao controlador de Acesso Negado -->
<access-denied-handler ref="customAccessDeniedHandler" />
<!-- Informações da página de login e dos handlers de sucesso e falha -->
<form-login
login-page="/login.xhtml"
authentication-success-handler-ref="customAuthenticationSuccessHandler"
authentication-failure-handler-ref="customAuthenticationFailureHandler" />
<!-- Página de logout, desloga o usuário ao ser chamada -->
<logout logout-url="/logout.xhtml" />
<!-- Acesso à página inicial para qualquer usuário logado -->
<intercept-url access="ROLE_USER" pattern="/index.xhtml*" />
<!-- permissões específicas para as telas -->
<intercept-url access="permissao.tela.1" pattern="/tela1.xhtml" />
<intercept-url access="permissao.tela.2" pattern="/tela2.xhtml" />
<intercept-url access="permissao.tela.N" pattern="/telaN.xhtml" />
<!-- nega acesso a qualquer outra tela -->
<intercept-url access="NO_ACCESS" pattern="/**.xhtml" />
</http>
<!-- Permite anotar métodos -->
<global-method-security
secured-annotations="enabled"
jsr250-annotations="enabled"
pre-post-annotations="enabled" />
</beans:beans>