Gestão de Usuários no Spring Security
Objective
Entenda como o Spring Security representa um usuário: o contrato UserDetails descreve credenciais e status da conta, GrantedAuthority descreve o que um usuário tem permissão para fazer, e UserDetailsService/UserDetailsManager descrevem como o framework encontra — e, na variante Manager, cria e modifica — usuários. Juntas, essas quatro interfaces são a metade de "gestão de usuários" do fluxo de autenticação apresentado em spring-security-authentication-architecture.
Use Cases
- Envolver uma entidade JPA
Userexistente em uma implementaçãoUserDetailsseparada em vez de fazer a própria entidade implementar o contrato, mantendo as preocupações de persistência e as preocupações do Spring Security em classes diferentes. - Escolher entre implementar
UserDetailsService(busca somente leitura, suficiente apenas para autenticação) eUserDetailsManager(adiciona criar/atualizar/deletar/trocar senha) com base em se a aplicação realmente precisa gerenciar usuários, não apenas autenticá-los. - Usar a classe builder
Userpara produzir rapidamente uma instânciaUserDetailsimutável em uma demo ou teste, sem escrever uma classe dedicada. - Reconhecer por que uma entidade JPA que também implementa
UserDetailsdiretamente tende a ficar difícil de ler — e aplicar a divisão em decorator (classe de entidade + wrapperSecurityUser) para corrigir isso.
Deep Dive
O contrato UserDetails: credenciais mais quatro flags de status da conta
UserDetails é o formato que o Spring Security espera que um usuário tenha. Apenas dois métodos carregam dados de autenticação; o resto rege autorização e status da conta:
javapublic interface UserDetails extends Serializable {
String getUsername();
String getPassword();
Collection<? extends GrantedAuthority> getAuthorities();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}Os quatro métodos isXxx() são todos formulados de modo que true significa "deixe essa conta passar" — isAccountNonExpired() ler como uma dupla negativa é proposital: a mente humana associa true ao caso positivo e false ao negativo, então cada um dos quatro métodos reprova a conta retornando false. Aplicações que não modelam expiração ou bloqueio simplesmente retornam true incondicionalmente nos quatro.
GrantedAuthority: um método, nomeando um privilégio
GrantedAuthority representa um único privilégio que um usuário possui — leitura, escrita, ou um nome de ação específico da aplicação:
javapublic interface GrantedAuthority extends Serializable {
String getAuthority();
}Como tem exatamente um método abstrato, uma lambda já basta para implementá-la, embora SimpleGrantedAuthority seja a alternativa embutida e mais explícita:
javaGrantedAuthority g1 = () -> "READ";
GrantedAuthority g2 = new SimpleGrantedAuthority("READ");Uma implementação mínima de UserDetails, depois uma real
A implementação mais simples possível retorna valores fixos para cada método — útil só para ver o formato do contrato:
javapublic class DummyUser implements UserDetails {
@Override public String getUsername() { return "bill"; }
@Override public String getPassword() { return "12345"; }
@Override public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> "READ");
}
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; }
}Uma implementação prática recebe username/password como argumentos de construtor, de forma que cada instância possa representar um usuário diferente:
javapublic class SimpleUser implements UserDetails {
private final String username;
private final String password;
public SimpleUser(String username, String password) {
this.username = username;
this.password = password;
}
@Override public String getUsername() { return username; }
@Override public String getPassword() { return password; }
// ...
}O builder User: uma instância UserDetails sem uma classe dedicada
Para casos simples, o builder User.withUsername(...) do Spring Security produz uma instância UserDetails imutável sem declarar nenhuma classe:
javaUserDetails u = User.withUsername("bill")
.password("12345")
.authorities("read", "write")
.accountExpired(false)
.disabled(true)
.build();build() aplica uma função opcional de codificação de senha, e então constrói a instância. O builder também pode partir de um UserDetails já existente (User.withUserDetails(existing)), útil para produzir uma cópia modificada.
Separando a entidade JPA da implementação de UserDetails
Uma entidade JPA que também implementa UserDetails diretamente mistura duas responsabilidades em uma classe só — campos de persistência e getters/setters lado a lado com métodos do contrato de segurança com nomenclatura diferente (getAuthority() retornando uma String, getAuthorities() retornando uma Collection que implementa a interface):
java@Entity
public class User implements UserDetails {
@Id private int id;
private String username;
private String password;
private String authority;
@Override public String getUsername() { return this.username; }
@Override public String getPassword() { return this.password; }
@Override public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> this.authority);
}
// JPA getters/setters interleaved with security-contract overrides
}Dividir isso em uma entidade JPA simples e um decorator separado restaura uma única responsabilidade por classe:
java@Entity
public class User {
@Id private int id;
private String username;
private String password;
private String authority;
// plain JPA entity — no Spring Security dependency
}
public class SecurityUser implements UserDetails {
private final User user;
public SecurityUser(User user) { this.user = user; }
@Override public String getUsername() { return user.getUsername(); }
@Override public String getPassword() { return user.getPassword(); }
@Override public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> user.getAuthority());
}
// ...
}SecurityUser só faz sentido envolvendo um User, então o campo é final e obrigatório no construtor — a classe documenta sua própria dependência.
UserDetailsService: um método, a única coisa que a autenticação estritamente precisa
javapublic interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}O AuthenticationProvider chama esse método para encontrar um usuário pelo username; se não existir nenhum, ele lança UsernameNotFoundException (uma RuntimeException por baixo dos panos — a cláusula throws é só documentação, já que ela por fim estende AuthenticationException). Uma implementação mínima sobre uma lista em memória não precisa de mais nada:
javapublic class InMemoryUserDetailsService implements UserDetailsService {
private final List<UserDetails> users;
public InMemoryUserDetailsService(List<UserDetails> users) {
this.users = users;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return users.stream()
.filter(u -> u.getUsername().equals(username))
.findFirst()
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
}UserDetailsManager: UserDetailsService mais operações de gestão de usuário
A autenticação sozinha só precisa de UserDetailsService. Aplicações que também precisam criar, atualizar ou deletar usuários implementam o contrato mais amplo UserDetailsManager:
javapublic interface UserDetailsManager extends UserDetailsService {
void createUser(UserDetails user);
void updateUser(UserDetails user);
void deleteUser(String username);
void changePassword(String oldPassword, String newPassword);
boolean userExists(String username);
}Essa divisão é uma aplicação direta do princípio de segregação de interfaces: uma aplicação que só autentica usuários nunca é forçada a implementar createUser/deleteUser/etc. InMemoryUserDetailsManager — usada para registrar um único usuário de demonstração em spring-security-authentication-architecture — é um UserDetailsManager; só a sua metade UserDetailsService foi necessária ali.
Trade-offs
- Um
UserDetailsmínimo (estiloDummyUser) é um recurso didático, não um design — toda instância representa o mesmo usuário, o que anula o propósito de um sistema de autenticação; código real precisa de pelo menos username/password como estado da instância. - Implementar
UserDetailsManagerquandoUserDetailsServicejá resolveria adiciona superfície não usada — cinco métodos extras (createUser,updateUser,deleteUser,changePassword,userExists) que precisam ser implementados (mesmo que só para lançarUnsupportedOperationException) para uma aplicação que só autentica, nunca provisiona, usuários. - Misturar uma entidade JPA com
UserDetailsparece limpo no começo e piora a cada relacionamento adicionado — a classe de dupla responsabilidade neste Deep Dive é pequena; na prática, adicionar relacionamentos de entidade (papéis, campos de auditoria) a uma classe que também satisfaz um contrato de segurança amplia a confusão que a divisão em decorator evita. - O builder
Useré conveniente, mas implícito — construir umUserDetailsinline esconde o tipo do resto da base de código atrás deUser.withUsername(...), o que é aceitável para uma demo, mas dificulta adicionar depois campos que uma classe dedicada poderia manter como estado de primeira classe.