Some time ago I came up with a reasonable solution, at some extent. I publish now because I think it might be useful for someone else.
(Note: the solution was implemented using SpringSecurity3.2.x.)
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><!-- -2.5 -3.2 -3.0.4 -->
<!-- <context:annotation-config/>-->
<context:component-scan base-package="br.com.vitoria.springSecJSR250" />
<beans:bean id="testingAuthenticationProvider"
class="org.springframework.security.authentication.TestingAuthenticationProvider" autowire-candidate="false">
<!-- <custom-authentication-provider />->>por estar http auto-config="true", este NÃO é necessário: AnonymousAuthenticationProvider é injetado em ser lugar-->
</beans:bean>
<beans:bean class="org.springframework.security.authentication.event.LoggerListener" />
<beans:bean class="org.springframework.security.access.event.LoggerListener" />
<beans:bean id="sessionRegistryimpl" class="org.springframework.security.core.session.SessionRegistryImpl" />
<beans:bean id="sessionRegistry" class="br.com.vitoria.springSecJSR250.config.SessionRegistryApplicationListener" />
<global-method-security jsr250-annotations='enabled' access-decision-manager-ref="accessDecisionManager" authentication-manager-ref="authManager"/>
<!-- Voter customizado -->
<beans:bean id="customVoter" class="br.com.vitoria.springSecJSR250.config.CustomVoter" />
<!-- Define AccessDesisionManager como UnanimousBased e coloca o Voter na lista -->
<beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased"><!-- access. -->
<beans:property name="allowIfAllAbstainDecisions"><beans:value>true</beans:value></beans:property>
<beans:constructor-arg><!-- SpringSec 3.X+ <beans:property name="decisionVoters">-->
<beans:list>
<beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter" /><!-- access.-->
<beans:bean class="org.springframework.security.access.vote.RoleVoter"/><!-- access.-->
<beans:ref bean="customVoter" />
</beans:list>
</beans:constructor-arg>
</beans:bean>
<authentication-manager alias='authManager' ><!--id="authManager" -->
<authentication-provider><!-- user-service-ref="" ref="testingAuthenticationProvider"->>AnonymousAuthenticationProvider gerada p/ auto-config="true" -->
<user-service>
<user name="Test" password="Password" authorities="ROLE_USER"/>
<user name="admin" password="admin" authorities="ROLE_ADMIN"/>
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
.. the class AuthenticationToken that keeps the User Session data:
public class StandardSessionAuthenticationToken/*<C extends SecurityContext, T extends SessionStandaloneInformation>*/
extends UsernamePasswordAuthenticationToken implements AuthenticationDetailsSource {
// private SessionStandaloneInformation details;
protected Collection</*? extends */GrantedAuthority> authorities;
public StandardSessionAuthenticationToken(Object principal, Object credentials, String sessionId, Date lastRequest) {
this(principal, credentials, sessionId, lastRequest, null);
}
public StandardSessionAuthenticationToken(Object principal, Object credentials, String sessionId, Date lastRequest
, Collection</*? extends */GrantedAuthority> authorities) {
super(principal, credentials, authorities);
this.setDetails(new SessionStandaloneInformation(principal, sessionId, lastRequest) );
/*this.*/setAuthorities(authorities);
this.setAuthorities(authorities);
}
/**
*
* @param context
* @return
*/
@Override
public Object buildDetails(Object context) {
final Object principal = ( (/*C*/SecurityContext)context).getAuthentication().getPrincipal(); //To change body of generated methods, choose Tools | Templates.
this.setDetails(new SessionStandaloneInformation(principal, ( (SessionStandaloneInformation)getDetails() ).getSessionId(), new Date() ) );
return this.getDetails();
}
// @Override
// public boolean implies(Subject subject) {
// return super.implies(subject); //
// }
/**
* just for back-compatibility purposes (SpringSecurity 2 / SpringSec 3.X+).
*/
public static class SessionStandaloneInformation extends SessionInformation implements SessionIdentifierAware {
public SessionStandaloneInformation(Object principal, String sessionId, Date lastRequest) {
super(principal, sessionId, lastRequest);
}
}
/**
* @param authorities the authorities to set
*/
public void setAuthorities(Collection</*? extends */GrantedAuthority> authorities) {
this.authorities = authorities;
}
/**
* @return the authorities
*/
@Override
public Collection</*? extends */GrantedAuthority> getAuthorities() {
return this.authorities;
}
/**
*(se estamos numa instância assignable StandardSessionAuthenticationToken, obviamente o tipo de:
* @return SessionStandaloneInformation
*/
@Override
public SessionStandaloneInformation getDetails() {
return (SessionStandaloneInformation)super.getDetails();
}
}
.. the class AuthenticationToken that keeps the (session) expiring info:
import java.util.Collection;
import java.util.Date;
import org.springframework.security.core.GrantedAuthority;
/**
*
* @author Derlon.Aliendres
*/
public class UsernamePasswordWithTimeoutAuthenticationToken extends StandardSessionAuthenticationToken
/* UsernamePasswordAuthenticationToken */{
private String timeout = null;
public UsernamePasswordWithTimeoutAuthenticationToken(Object principal, Object credentials, String timeOut
, String sessionId, Date lastRequest) {
this(principal, credentials, sessionId, lastRequest, timeOut, null);
// this.timeout = null;
}
/**
*
* @param principal the value of principal
* @param credentials the value of credentials
* @param sessionId the value of sessionId
* @param lastRequest the value of lastRequest
* @param timeOut the value of timeOut
* @param authorities the value of authorities
*/
public UsernamePasswordWithTimeoutAuthenticationToken(Object principal, Object credentials, String sessionId, Date lastRequest
, String timeOut, Collection</*? extends */GrantedAuthority> authorities) {
super(principal, credentials, sessionId, lastRequest, authorities);
this.timeout = timeOut;
}
public String getTimeout() {
return timeout;
}
public Boolean jahExpirouSessionDa() {
Boolean result;
final SessionStandaloneInformation sessnInfo = /*(SessionStandaloneInformation)*/(this.getDetails() );
final String sessionId = sessnInfo.getSessionId();
final Date datimeLastRequest = sessnInfo.getLastRequest();
final long timeOutTime = extractNonceValue(this.getTimeout() );
if ( ( (new Date().getTime() ) - datimeLastRequest.getTime() ) > timeOutTime) {
//attr.equals("ROLE_USER")
//ROLE_USER é a ROLE de usuário, logado, então retorna true sempre
result = false;
} else {
//chama uma lógica específica que verifica se o usuário possui permissão no contexto atual
result = true; // gerenciadorPermissao.verificarPermissao(variavelSessao, attr)
}
return result;
}
private long extractNonceValue(String timeout) {
return /*getLong*/ Long.valueOf(timeout);
}
}
... the test:
/**
*
* @author derlon.aliendres
*/
@RunWith(value = SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
@org.junit.FixMethodOrder(MethodSorters.NAME_ASCENDING)//->>Ordem: a1, a2, a3, ...
public class Jsr250AnnotationDomainBizzServiceTest {
// private static InMemoryXmlApplicationContext appContext;
@Inject // @Resource // @Autowired
private /*static*/ BusinessService target;
@Inject //
private ApplicationEventPublisher _eventPublisher;
@Inject //
private DaoAuthenticationProvider daoAuthenticationProvider; // AuthenticationProvider _authProvider
@BeforeClass
public static void setUpClass() {
// SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_GLOBAL);
}
@Before
public void loadAppContext() {
}
@After
public void clearSecurityContext() {
SecurityContextHolder.clearContext();
}
@AfterClass
public static void tearDownClass() {
// if (appContext != null) {
// appContext.close();//<<-Como o contexto é carregado só 1x(no setUpClass) só pode ser encerrado aki!
// }
}
@Test(expected=AuthenticationCredentialsNotFoundException.class)
public void a1targetShouldPreventProtectedMethodInvocationWithNoContext() {
target.someUserMethod1(); //<<<--incide aqui o teste!!!
System.out.println("Falha na Autenticação: fluxo exec BLOQUEADO!!!)");
}
@Test
public void a2permitAllShouldBeDefaultAttribute() {
UsernamePasswordAuthenticationToken token
= new UsernamePasswordAuthenticationToken("Test", "Password", AuthorityUtils.createAuthorityList("ROLE_USER") );
SecurityContextHolder.getContext().setAuthentication(token);// SecurityContextHolder.getContextHolderStrategy().getContext().getAuthentication().
// LoggerListener listener = new LoggerListener();listener.onApplicationEvent(event);
_eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(token, this.getClass() ) );
target.someOther(0); //<<<--incide aqui o teste!!!
}
@Test
public void a3targetShouldAllowProtectedMethodInvocationWithCorrectRole() {
UsernamePasswordAuthenticationToken token
= new UsernamePasswordAuthenticationToken("Test", "Password", AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContextHolder.getContext().setAuthentication(token);
_eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(token, this.getClass() ) );
target.someUserMethod1(); //<<<--incide aqui o teste!!!
}
@Test() // expected=AccountExpiredException/*AccessDeniedException*/.class
public void a4targetShouldAllowInvocationWithCorrectRoleAndNotTimedoutAuthentication() {
final String timeOut = "0080";
final Date datimeLastRequest = new Date()/*, AuthorityUtils.stringArrayToAuthorityArray(new String[]{"ROLE_USER"}) */;
UsernamePasswordWithTimeoutAuthenticationToken token
= new UsernamePasswordWithTimeoutAuthenticationToken("Test", "Password", timeOut, "001", datimeLastRequest);
// = new UsernamePasswordWithTimeoutAuthenticationToken("Test", "Password", "001", datimeLastRequest
// , timeOut, AuthorityUtils.createAuthorityList("ROLE_USER") );
Authentication auth = /*(StandardSessionAuthenticationToken)*/daoAuthenticationProvider.authenticate(token); //
token.setAuthorities(new ArrayList<GrantedAuthority>(auth.getAuthorities() ) );
SecurityContextHolder.getContext().setAuthentication( /*(UsernamePasswordWithTimeoutAuthenticationToken*/token); //auth.getDetails()
_eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(token, this.getClass() ) ); // token
target.someUserMethod1(); //<<<--incide aqui o teste!!!
}
// @Inject
// private SessionRegistry sessionRegistry;
@Test(expected=AccountExpiredException/*AccessDeniedException*/.class) // ProviderNotFoundException
public void a4targetShouldPreventInvocationWithCorrectRoleButNoLongerAuthenticated() {
final String timeOut = "0010";
final Date datimeLastRequest = new Date()/*, AuthorityUtils.stringArrayToAuthorityArray(new String[]{"ROLE_USER"}) */;
UsernamePasswordWithTimeoutAuthenticationToken token
= new UsernamePasswordWithTimeoutAuthenticationToken("Test", "Password", timeOut, "001", datimeLastRequest);
// = new UsernamePasswordWithTimeoutAuthenticationToken("Test", "Password", "001", datimeLastRequest
// , timeOut, AuthorityUtils.createAuthorityList("ROLE_USER") );
Authentication auth = /*(StandardSessionAuthenticationToken)*/daoAuthenticationProvider.authenticate(token); //
token.setAuthorities(new ArrayList<GrantedAuthority>(auth.getAuthorities() ) );
SecurityContextHolder.getContext().setAuthentication( /*(UsernamePasswordWithTimeoutAuthenticationToken*/token); //auth.getDetails()
_eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(token, this.getClass() ) ); //
// /* SecurityContextHolder.getContext().getAuthentication()*/token.setAuthenticated(false)/*.eraseCredentials()*/;
try {
/*new */Thread.sleep(30/*00*/);//<<<--Idle time of user Session Simulation
} catch (InterruptedException e) {
e.printStackTrace();
}
// SecurityContext sc = SecurityContextHolder.getContext();
//
// String idSessao = SessionRegistryUtils.obtainSessionIdFromAuthentication(sc.getAuthentication());
// sessionRegistry.registerNewSession(idSessao, auth.getPrincipal())/*getName()*//*.get(0).expireNow()*/;
// // System.out.println("SessionInformation na Autenticação: " + idSessao);
// SessionInformation[] sessoes = sessionRegistry.getAllSessions(auth.getPrincipal(), true); // sc.getAuthentication()
// for (SessionInformation session: sessoes) {
// int i = 1;
// System.out.println("SessionInformation na Autenticação: " + i++ + "º: " + session.getSessionId() );
// }
// auth = null;
target.someUserMethod1(); //<<<--incide aqui o teste!!!
}
@Test(expected=AccessDeniedException.class)
public void a5targetShouldPreventProtectedMethodInvocationWithIncorrectRole() {
TestingAuthenticationToken token
= new TestingAuthenticationToken("Test", "Password", /*AuthorityUtils.createAuthorityList(*/"ROLE_SOMEINVALIDROLE"/*)*/);
SecurityContextHolder.getContext().setAuthentication(token);
_eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(token, this.getClass() ) );
target.someAdminMethod(); //<<<--incide aqui o teste!!!
System.out.println("Falha na Autenticação: fluxo exec BLOQUEADO!!!)");
}
}
.. and finally (in my opnion) the key implementation (the 'CustomVoter'):
/**
*
* @author Derlon.Aliendres
*/
public class CustomVoter implements AccessDecisionVoter/*<Object>*/ {
// @Inject
// private SessionRegistry sessionRegistry;
@Inject //
private ApplicationEventPublisher _eventPublisher;
final protected Logger logger = Logger/*Factory*/.getLogger(getClass().getName());
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class/*<?>*/ clazzAuthentication) {
return true/*org.aopalliance.intercept.MethodInvocation.class.isAssignableFrom(clazzAuthentication)*/;
}
@Override
public int vote(Authentication authentication, Object object,/* ConfigAttributeDefinition*/Collection<ConfigAttribute> attributes) {
logger.info("### Controle de Acesso ###");
//
//verifica se as credenciais são do tipo esperado
if (SessionStandaloneInformation.class.isInstance(authentication.getDetails())) {/*authentication instanceof UsernamePasswordWithTimeoutAuthenticationToken*/
Boolean result/* = null*/;
UsernamePasswordWithTimeoutAuthenticationToken user
= (UsernamePasswordWithTimeoutAuthenticationToken)SecurityContextHolder.getContext().getAuthentication(); // authentication/*.getPrincipal()*/
// for (ConfigAttribute configAttribute: attributes) {
// String attr = configAttribute.getAttribute();
result = user.jahExpirouSessionDa();
// }
if (result == null || result == Boolean.FALSE) {
logger.info(" -> Acesso Negado!");
// SecurityContextHolder.clearContext();// <<<--threatment already in SessionRegistry App listener!!!
final AccountExpiredException userSessionExpiredException =
new AccountExpiredException("Sessão do Usuário expirou! Efetue o LogOn novamente.");
_eventPublisher.publishEvent(new AuthenticationFailureExpiredEvent
(authentication, userSessionExpiredException) );
throw userSessionExpiredException;
// return ACCESS_ABSTAIN; // ACCESS_DENIED
} else {
logger.info(" -> Acesso Permitido!");
// sessionRegistry.refreshLastRequest(sessionId);
return ACCESS_GRANTED;
}
} else {
System.out.println(" -> Não é do tipo UsernamePasswordWithTimeoutAuthenticationToken!");
return ACCESS_ABSTAIN;
}
}
}