UserDetailsService
O método do UserDetailsService é tentar encontrar um usuário por Username. Como nosso Username na verdade é o email, precisamos:
Criar um UserRepository com o método "findByEmail". Como já sabemos, o Repository consegue realizar a busca em virtude do "by". Para que ele consiga também buscar as roles desse usuário, faremos uma consulta SQL raíz.
Nesse ponto, nós já sabemos!
Criar uma UserDetailsProjection no pacote projections, com os atributos em get:
public interface UserDetailsProjection {
String getUsername();
String getPassword();
Long getRoleId();
String getAuthority();
}Fazer a consulta no UserRepository, usando o projection com o SQL
Query(nativeQuery = true, value = """
SELECT tb_user.email AS username, tb_user.password, tb_role.id AS roleId, tb_role.authority
FROM tb_user
INNER JOIN tb_user_role ON tb_user.id = tb_user_role.user_id
INNER JOIN tb_role ON tb_role.id = tb_user_role.role_id
WHERE tb_user.email = :email
""")
List<UserDetailsProjection> searchUserAndRolesByEmail(String email);Injetar esse Repository no UserService, e utilizá-lo dentro do método advindo da interface (UserDetailsService):

Criaremos uma lista do tipo projection e usamos o método do repository passando o username;
Se ela estiver vazia, lançamos a exceção;
Caso contrário, instanciaremos um User, setaremos o seu email e password;
Para settar as roles, faremos um for:
Para cada Projection dentro da lista result, entraremos no objeto User e utilizaremos o método addRole;
Dentro dele, criaremos um new Role, passando o roleId e tipo de authority :)
Retorna o user depois.
Atualizado