Introdução à Spring Method Security
1. Introdução
Simplificando, o Spring Security suporta semântica de autorização no nível do método.
Normalmente, poderíamos proteger nossa camada de serviço, por exemplo, restringindo quais funções são capazes de executar um método específico - e testá-lo usando o suporte dedicado ao teste de segurança no nível do método.
Neste artigo, vamos revisar o uso de algumas anotações de segurança primeiro. Então, vamos nos concentrar em testar a segurança do nosso método com diferentes estratégias.
2. Habilitando a Segurança do Método
Primeiro de tudo, para usar Spring Method Security, precisamos adicionar a dependênciaspring-security-config:
org.springframework.security
spring-security-config
Podemos encontrar sua versão mais recente emMaven Central.
Se quisermos usar o Spring Boot, podemos usar a dependênciaspring-boot-starter-security que incluispring-security-config:
org.springframework.boot
spring-boot-starter-security
Novamente, a versão mais recente pode ser encontrada emMaven Central.
Em seguida, precisamos habilitar a segurança de método global:
@Configuration
@EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true)
public class MethodSecurityConfig
extends GlobalMethodSecurityConfiguration {
}
-
A propriedadeprePostEnabled ativa as anotações pré / pós Spring Security
-
A propriedadesecuredEnabled determina se a anotação@Secured deve ser habilitada
-
A propriedadejsr250Enabled nos permite usar a anotação@RoleAllowed
Exploraremos mais sobre essas anotações na próxima seção.
3. Aplicando Método de Segurança
3.1. Usando a anotação@Secured
The @Secured annotation is used to specify a list of roles on a method. Portanto, um usuário só pode acessar esse método se tiver pelo menos uma das funções especificadas.
Vamos definir um métodogetUsername:
@Secured("ROLE_VIEWER")
public String getUsername() {
SecurityContext securityContext = SecurityContextHolder.getContext();
return securityContext.getAuthentication().getName();
}
Aqui, a anotação@Secured(“ROLE_VIEWER”) define que apenas os usuários que têm a funçãoROLE_VIEWER são capazes de executar o métodogetUsername.
Além disso, podemos definir uma lista de funções em uma anotação@Secured:
@Secured({ "ROLE_VIEWER", "ROLE_EDITOR" })
public boolean isValidUsername(String username) {
return userRoleRepository.isValidUsername(username);
}
Nesse caso, a configuração indica que se um usuário tiverROLE_VIEWER ouROLE_EDITOR, esse usuário pode invocar o métodoisValidUsername.
A anotação@Secured não suporta Spring Expression Language (SpEL).
3.2. Usando a anotação@RoleAllowed
A anotação@RoleAllowed é a anotação equivalente do JSR-250 da anotação@Secured .
Basicamente, podemos usar a anotação@RoleAllowed de maneira semelhante a@Secured. Assim, poderíamos redefinir os métodosgetUsernameeisValidUsername:
@RolesAllowed("ROLE_VIEWER")
public String getUsername2() {
//...
}
@RolesAllowed({ "ROLE_VIEWER", "ROLE_EDITOR" })
public boolean isValidUsername2(String username) {
//...
}
Da mesma forma, apenas o usuário com funçãoROLE_VIEWER pode executargetUsername2.
Novamente, um usuário é capaz de invocarisValidUsername2 apenas se tiver pelo menos uma das funçõesROLE_VIEWER ouROLER_EDITOR.
3.3. Usando anotações@PreAuthorizee@PostAuthorize
Both @PreAuthorize and @PostAuthorize annotations provide expression-based access control. Portanto, os predicados podem ser escritos usandoSpEL (Spring Expression Language).
The @PreAuthorize annotation checks the given expression before entering the method, enquantothe @PostAuthorize annotation verifies it after the execution of the method and could alter the result.
Agora, vamos declarar um métodogetUsernameInUpperCase como abaixo:
@PreAuthorize("hasRole('ROLE_VIEWER')")
public String getUsernameInUpperCase() {
return getUsername().toUpperCase();
}
O@PreAuthorize(“hasRole(‘ROLE_VIEWER')”) tem o mesmo significado que@Secured(“ROLE_VIEWER”) que usamos na seção anterior. Sinta-se à vontade para descobrir maissecurity expressions details in previous articles.
Consequentemente, a anotação@Secured(\{“ROLE_VIEWER”,”ROLE_EDITOR”}) pode ser substituída por@PreAuthorize(“hasRole(‘ROLE_VIEWER') or hasRole(‘ROLE_EDITOR')”):
@PreAuthorize("hasRole('ROLE_VIEWER') or hasRole('ROLE_EDITOR')")
public boolean isValidUsername3(String username) {
//...
}
Além disso,we can actually use the method argument as part of the expression:
@PreAuthorize("#username == authentication.principal.username")
public String getMyRoles(String username) {
//...
}
Aqui, um usuário pode invocar o métodogetMyRoles apenas se o valor do argumentousername for igual ao nome de usuário do principal atual.
It’s worth to note that @PreAuthorize expressions can be replaced by @PostAuthorize ones.
Vamos reescrevergetMyRoles:
@PostAuthorize("#username == authentication.principal.username")
public String getMyRoles2(String username) {
//...
}
No exemplo anterior, no entanto, a autorização seria adiada após a execução do método de destino.
Além disso,the @PostAuthorize annotation provides the ability to access the method result:
@PostAuthorize
("returnObject.username == authentication.principal.nickName")
public CustomUser loadUserDetail(String username) {
return userRoleRepository.loadUserByUserName(username);
}
Neste exemplo, o métodoloadUserDetail só seria executado com sucesso seusername doCustomUser retornado fosse igual aonickname. do principal de autenticação atual
Nesta seção, usamos principalmente expressões simples do Spring. Para cenários mais complexos, podemos criarcustom security expressions.
3.4. Usando anotações@PreFiltere@PostFilter
Spring Security provides the @PreFilter annotation to filter a collection argument before executing the method:
@PreFilter("filterObject != authentication.principal.username")
public String joinUsernames(List usernames) {
return usernames.stream().collect(Collectors.joining(";"));
}
Neste exemplo, estamos juntando todos os nomes de usuário, exceto aquele que é autenticado.
Aqui,our expression uses the name filterObject to represent the current object in the collection.
No entanto, se o método tiver mais de um argumento que é um tipo de coleção, precisamos usar a propriedadefilterTarget para especificar qual argumento queremos filtrar:
@PreFilter
(value = "filterObject != authentication.principal.username",
filterTarget = "usernames")
public String joinUsernamesAndRoles(
List usernames, List roles) {
return usernames.stream().collect(Collectors.joining(";"))
+ ":" + roles.stream().collect(Collectors.joining(";"));
}
Além disso,we can also filter the returned collection of a method by using @PostFilter annotation:
@PostFilter("filterObject != authentication.principal.username")
public List getAllUsernamesExceptCurrent() {
return userRoleRepository.getAllUsernames();
}
Nesse caso, o nomefilterObject se refere ao objeto atual na coleção retornada.
Com essa configuração, Spring Security irá iterar através da lista retornada e remover qualquer valor que corresponda ao nome de usuário do principal.
Mais detalhes de@PreFiltere@PostFilter podem ser encontrados no artigoSpring Security – @PreFilter and @PostFilter.
3.5. Metanotação de segurança do método
Normalmente, nos encontramos em uma situação em que protegemos métodos diferentes usando a mesma configuração de segurança.
Nesse caso, podemos definir uma meta-anotação de segurança:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('VIEWER')")
public @interface IsViewer {
}
Em seguida, podemos usar diretamente a anotação @IsViewer para proteger nosso método:
@IsViewer
public String getUsername4() {
//...
}
As meta-anotações de segurança são uma ótima idéia, pois adicionam mais semânticas e separam nossa lógica comercial da estrutura de segurança.
3.6. Anotação de segurança no nível da classe
Se nos encontrarmos usando a mesma anotação de segurança para todos os métodos dentro de uma classe, podemos considerar colocar essa anotação no nível da classe:
@Service
@PreAuthorize("hasRole('ROLE_ADMIN')")
public class SystemService {
public String getSystemYear(){
//...
}
public String getSystemDate(){
//...
}
}
No exemplo acima, a regra de segurançahasRole(‘ROLE_ADMIN') será aplicada aos métodosgetSystemYearegetSystemDate.
3.7. Várias anotações de segurança em um método
Também podemos usar várias anotações de segurança em um método:
@PreAuthorize("#username == authentication.principal.username")
@PostAuthorize("returnObject.username == authentication.principal.nickName")
public CustomUser securedLoadUserDetail(String username) {
return userRoleRepository.loadUserByUserName(username);
}
Portanto, o Spring irá verificar a autorização antes e depois da execução do métodosecuredLoadUserDetail.
4. Considerações Importantes
Há dois pontos que gostaríamos de lembrar em relação à segurança do método:
-
By default, Spring AOP proxying is used to apply method security – se um método seguro A é chamado por outro método dentro da mesma classe, a segurança em A é totalmente ignorada. Isso significa que o método A será executado sem nenhuma verificação de segurança. O mesmo se aplica aos métodos privados
-
Spring SecurityContext is thread-bound – por padrão, o contexto de segurança não é propagado para threads filho. Para obter mais informações, podemos consultar o artigoSpring Security Context Propagation
5. Segurança do método de teste
5.1. Configuração
Para testar o Spring Security com JUnit, precisamos da dependênciaspring-security-test:
org.springframework.security
spring-security-test
Não precisamos especificar a versão de dependência porque estamos usando o plugin Spring Boot. As versões mais recentes dessa dependência podem ser encontradas emMaven Central.
A seguir, vamos configurar um teste simples de integração Spring, especificando o runner e a configuraçãoApplicationContext:
@RunWith(SpringRunner.class)
@ContextConfiguration
public class TestMethodSecurity {
// ...
}
5.2. Testando nome de usuário e funções
Agora que nossa configuração está pronta, vamos tentar testar nosso métodogetUsername que é protegido pela anotação@Secured(“ROLE_VIEWER”):
@Secured("ROLE_VIEWER")
public String getUsername() {
SecurityContext securityContext = SecurityContextHolder.getContext();
return securityContext.getAuthentication().getName();
}
Como usamos a anotação@Secured aqui, é necessário que um usuário seja autenticado para invocar o método. Caso contrário, obteremos umAuthenticationCredentialsNotFoundException.
Portanto,we need to provide a user to test our secured method. To achieve this, we decorate the test method with @WithMockUser and provide a user and roles:
@Test
@WithMockUser(username = "john", roles = { "VIEWER" })
public void givenRoleViewer_whenCallGetUsername_thenReturnUsername() {
String userName = userRoleService.getUsername();
assertEquals("john", userName);
}
Fornecemos um usuário autenticado cujo nome de usuário éjohn e cuja função éROLE_VIEWER. Se não especificarmosusername ourole, ousername padrão éusere orole padrão éROLE_USER.
Observe que não é necessário adicionar o prefixoROLE_ aqui, o Spring Security adicionará esse prefixo automaticamente.
Se não quisermos ter esse prefixo, podemos considerar o uso deauthority em vez derole.
Por exemplo, vamos declarar um métodogetUsernameInLowerCase:
@PreAuthorize("hasAuthority('SYS_ADMIN')")
public String getUsernameLC(){
return getUsername().toLowerCase();
}
Poderíamos testar isso usando autoridades:
@Test
@WithMockUser(username = "JOHN", authorities = { "SYS_ADMIN" })
public void givenAuthoritySysAdmin_whenCallGetUsernameLC_thenReturnUsername() {
String username = userRoleService.getUsernameInLowerCase();
assertEquals("john", username);
}
Convenientemente,if we want to use the same user for many test cases, we can declare the @WithMockUser annotation at test class:
@RunWith(SpringRunner.class)
@ContextConfiguration
@WithMockUser(username = "john", roles = { "VIEWER" })
public class TestWithMockUserAtClassLevel {
//...
}
Se quisermos executar nosso teste como um usuário anônimo, poderíamos usar a anotação@WithAnonymousUser:
@Test(expected = AccessDeniedException.class)
@WithAnonymousUser
public void givenAnomynousUser_whenCallGetUsername_thenAccessDenied() {
userRoleService.getUsername();
}
No exemplo acima, esperamos umAccessDeniedException porque o usuário anônimo não recebeu a funçãoROLE_VIEWER ou a autoridadeSYS_ADMIN.
5.3. Testando com umUserDetailsService personalizado
For most applications, it’s common to use a custom class as authentication principal. Nesse caso, a classe personalizada precisa implementar a interfaceorg.springframework.security.core.userdetails.UserDetails.
Neste artigo, declaramos uma classeCustomUser que estende a implementação existente deUserDetails, que éorg.springframework.security.core.userdetails.User:
public class CustomUser extends User {
private String nickName;
// getter and setter
}
Vamos retomar o exemplo com a anotação@PostAuthorize na seção 3:
@PostAuthorize("returnObject.username == authentication.principal.nickName")
public CustomUser loadUserDetail(String username) {
return userRoleRepository.loadUserByUserName(username);
}
Nesse caso, o método só seria executado com sucesso se ousername doCustomUser retornado fosse igual aonickname do principal de autenticação atual.
Se quiséssemos testar esse método, we could provide an implementation of UserDetailsService which could load our CustomUser based on the username:
@Test
@WithUserDetails(
value = "john",
userDetailsServiceBeanName = "userDetailService")
public void whenJohn_callLoadUserDetail_thenOK() {
CustomUser user = userService.loadUserDetail("jane");
assertEquals("jane", user.getNickName());
}
Aqui, a anotação@WithUserDetails afirma que usaremos umUserDetailsService para inicializar nosso usuário autenticado. O serviço é referido pela propriedadeuserDetailsServiceBeanName. EsteUserDetailsService pode ser uma implementação real ou falsa para fins de teste.
Além disso, o serviço usará o valor da propriedadevalue como o nome de usuário para carregarUserDetails.
Convenientemente, também podemos decorar com uma anotação@WithUserDetails no nível da classe, da mesma forma que fizemos com a anotação@WithMockUser.
5.4. Teste com Meta Anotações
Muitas vezes nos encontramos reutilizando os mesmos usuários / funções repetidamente em vários testes.
Para essas situações, é conveniente criar ummeta-annotation.
Retornando ao exemplo anterior@WithMockUser(username=”john”, roles=\{“VIEWER”}), podemos declarar uma meta-anotação como:
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value = "john", roles = "VIEWER")
public @interface WithMockJohnViewer { }
Então, podemos simplesmente usar@WithMockJohnViewer em nosso teste:
@Test
@WithMockJohnViewer
public void givenMockedJohnViewer_whenCallGetUsername_thenReturnUsername() {
String userName = userRoleService.getUsername();
assertEquals("john", userName);
}
Da mesma forma, podemos usar meta-anotações para criar usuários específicos de domínio usando@WithUserDetails.
6. Conclusão
Neste tutorial, exploramos várias opções para usar o Method Security no Spring Security.
Também passamos por algumas técnicas para testar facilmente a segurança dos métodos e aprendemos como reutilizar usuários simulados em diferentes testes.
Todos os exemplos deste tutorial podem ser encontradosover on Github.