URLs e Roles dinâmicas no Spring Security

Boa parte das pessoas que aprendem o Spring Security sempre se questionam o motivo da associação entre url role ser fixa no código.

	http.authorizeRequests()
	.antMatchers("/produtos/form").hasRole("ADMIN")
	.antMatchers("/shopping/**").permitAll()
	.antMatchers(HttpMethod.POST,"/produtos").hasRole("ADMIN")
	.antMatchers("/produtos/**").permitAll()
	.antMatchers(HttpMethod.GET, "/admin/users").permitAll()
	.anyRequest().authenticated()

O principal, até citado pela própria documentação do Spring, é que isso é uma parte importante do seu sistema e, portanto, deve ser testada para garantir que tudo esteja configurado da maneira correta. Quando você decide optar por uma configuração dinâmica, por exemplo carregando essas associações de um banco de dados, o sistema fica mais suscetível a erros, já que algum cadastro pode ter sido esquecido ou até ter sido feito de maneira errada. Para ser bem sincero, concordo com a documentação. Sem contar os fatos já explicados, ainda tem a parte que é mais complicado de fazer :). Busquei um pouco sobre o assunto no todo poderoso do conhecimento e não encontrei nada realmente matador, então resolvi escrever este post.

Podemos até combinar os dois meios de configuração, o estático e o dinâmico. Para facilitar, vamos simular que você deseja que todas associações entre roles e urls venham do banco. A primeira coisa que precisamos fazer é retirar as configurações estáticas.

	http.authorizeRequests()
	.anyRequest().authenticated()
	.and()
	.formLogin().loginPage("/login").permitAll()
	.and()
	.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"));

E agora, como vamos fazer? A classe responsável por checar as roles liberadas para cada url  é a FilterSecurityInterceptor. É justamente ela que precisa ser alterada. O melhor jeito de ter acesso a este objeto é pedindo para o Spring Security nos notificar quando ele tiver sido criado, pois dessa forma basta que façamos algumas pequenas alterações.

	http.authorizeRequests()
	.anyRequest().authenticated()
	.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
		public <O extends FilterSecurityInterceptor> O postProcess(
				O fsi) {
                        //podemos alterar o objeto aqui
			return fsi;
		}
	})
	.and()
	.formLogin().loginPage("/login").permitAll()
	.and()
	.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"));

O método withObjectPostProcessor deve ser usado sempre que você precisar processar algum objeto criado pela configuração, o que é meio raro, mas perfeito para o nosso caso. O objeto do tipo FilterSecurityInterceptor possui um método chamado setSecurityMetadataSource,  que recebe como argumento algum objeto que implementa a interface FilterInvocationSecurityMetadataSource. É justamente aqui que vamos entrar. Precisamos criar uma implementação dessa interface que carregue do banco de dados a associação entre roles e urls.

	.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
		public <O extends FilterSecurityInterceptor> O postProcess(
				O fsi) {
			fsi.setSecurityMetadataSource(dynamicSecurityMetadataSource);
			return fsi;
		}
	})

Agora vamos dar uma olhada na implementação que criamos.

	package br.com.casadocodigo.loja.conf;

	@Component
	public class DynamicSecurityMetadataSource implements
			FilterInvocationSecurityMetadataSource {

		@Autowired
		private SystemURLDAO systemUrls;;

		@Override
		public Collection<ConfigAttribute> getAttributes(Object object)
				throws IllegalArgumentException {
			final HttpServletRequest request = ((FilterInvocation) object)
					.getRequest();

			String urlWithoutContextPath = request.getRequestURI().substring(
					request.getContextPath().length());

			Optional<SystemURL> foundUrl = systemUrls
					.findByURL(urlWithoutContextPath);

			if (foundUrl.isPresent()) {
				return foundUrl.get().getRolesAllowed().stream()
						.map(this::configAttribute).collect(Collectors.toList());
			}

			return null;
		}

		private ConfigAttribute configAttribute(Role role) {
			return new ConfigAttribute() {
				@Override
				public String getAttribute() {
					return role.getAuthority();
				}
			};
		}

		@Override
		public Collection<ConfigAttribute> getAllConfigAttributes() {
			return null;
		}

		@Override
		public boolean supports(Class<?> clazz) {
			return FilterInvocation.class.isAssignableFrom(clazz);
		}

	}

A interface ConfigAttribute é a abstração de uma permissão. Por exemplo, quando usamos o modo default usamos a implementação WebExpressionConfigAttribute. No nosso caso, simplesmente usamos uma classe anônima que implementa a interface, mas também poderíamos ter criado uma classe chamada, por exemplo, de RoleConfigAttribute. 

Um último detalhe, muito importante. Quando uma nova requisição é feita, para o Spring Security decidir se o usuário pode acessar o recurso ou não, é usada uma implementação da interface AccessDecisionManager. O jeito mais fácil de entender o que acontece por debaixo dos panos, é dando uma olhada no código executado para liberar ou não um acesso. Vamos olhar, por exemplo, a implementação da classe AffirmativeBased.

	public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
		int deny = 0;

		for (AccessDecisionVoter voter : getDecisionVoters()) {
			int result = voter.vote(authentication, object, configAttributes);

			if (logger.isDebugEnabled()) {
				logger.debug("Voter: " + voter + ", returned: " + result);
			}

			switch (result) {
			case AccessDecisionVoter.ACCESS_GRANTED:
				return;

			case AccessDecisionVoter.ACCESS_DENIED:
				deny++;

				break;

			default:
				break;
			}
		}

Ele faz um loop por implementações de AccessDecisionVoter e, neste caso, apenas verifica se alguma implementação libera o acesso. Existem outras implementações da interface AccessDecisionManager que decidem a liberação das urls baseadas em outras estratégias. Quando usamos as configurações estáticas, o único voter utilizado é o WebExpressionVoter. Só que, no nosso caso, precisamos de uma implementação baseada simplesmente em roles . Para isso que existe a classe RoleVoter. Apenas vamos ensinar ao Spring Security que queremos usar outro AcessDecisionManager.

	AffirmativeBased affirmativeBased = new AffirmativeBased(Arrays.asList(new RoleVoter());
	http.authorizeRequests().accessDecisionManager(affirmativeBased)
	.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
		public <O extends FilterSecurityInterceptor> O postProcess(
				O fsi) {
			fsi.setSecurityMetadataSource(dynamicSecurityMetadataSource);
			return fsi;
		}
	})
	.and()
	.formLogin().loginPage("/login").permitAll()
	.and()
	.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"));

Pronto! Agora conseguimos verificar os acessos a partir de regras cadastradas no banco de dados. O código completo de exemplo pode ser encontrado nesse commit. Esse post foi um pouco mais avançado e já espera que você possua um certo conhecimento sobre o Spring Security. No meu livro eu dedico um capítulo inteiro para o Spring Security e o código fonte também pode ser encontrado no meu github.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s