Skip to content

20. Proteger o serviço web para aceder à base de dados [dbproduitscategories]

20.1. Configurar o ambiente de desenvolvimento

Iremos implementar a segurança do serviço web utilizando os seguintes projetos:

  
  • os projetos [spring-security-*] podem ser encontrados na pasta [<examples>\spring-database-generic\spring-security];
  • A segurança será implementada para o SGBD MySQL utilizando uma camada [DAO / JDBC], seguida de uma camada [DAO / JPA / Hibernate];
  • Prima Alt-F5 e, em seguida, regenere todos os projetos Maven;

Precisamos de criar utilizadores na base de dados [dbproduitscategories]. Para tal, utilize a configuração de execução [spring-security-create-users-hibernate-eclipselink]:

A execução desta configuração preenche as tabelas [USERS, ROLES, USERS_ROLES] na base de dados [dbproduitscategories]:

 

As credenciais criadas [nome de utilizador/palavra-passe] são as seguintes: [admin/admin], [user/user], [guest/guest]. Na base de dados, as palavras-passe estão encriptadas.

Image

Depois de concluir este passo, execute a configuração de teste denominada [spring-security-server-jpa-generic-hibernate-eclipselink], que inicia o serviço web seguro (o MySQL deve estar em execução):

Em seguida, execute a configuração de teste denominada [spring-security-client-generic-JUnitTestDao], que testa o serviço web seguro:

O teste deve ser aprovado.

20.2. O projeto Eclipse [spring-security-server-jdbc-generic]

O serviço web seguro é implementado pelo projeto [spring-security-server-jdbc-generic]:

Acima:

  • A camada [DAO1] é a camada [DAO] que gere as tabelas [PRODUCTS] e [CATEGORIES] na base de dados [dbproduitscategories]. Já foi escrita;
  • A camada [DAO2] é a camada [DAO] que gere as tabelas [USERS], [ROLES] e [USERS_ROLES] na base de dados [dbproduitscategories]. Ainda não foi escrita;

  

20.2.1. A configuração do Maven

O projeto [spring-security-server-jdbc-generic] é um projeto Maven configurado pelo seguinte ficheiro [pom.xml]:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-security-server-jdbc-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>
 
    <name>spring-security-server-jdbc-generic</name>
    <description>démo spring security</description>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>
 
    <dependencies>
        <!-- Spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- web server / jSON -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-webjson-server-jdbc-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  • linhas 29–33: reutilizamos o código existente com o arquivo de serviço web / json / jdbc que analisámos;
  • linhas 24–27: a dependência que traz as classes do Spring Security;

Em última análise, o projeto tem as seguintes dependências em relação aos outros projetos carregados no Eclipse:

  

20.2.2. A camada [DAO2]

Acima:

  • A camada [DAO1] é a camada [DAO] que gere as tabelas [PRODUCTS] e [CATEGORIES] na base de dados [dbproduitscategories]. Já foi escrita;
  • A camada [DAO2] é a camada [DAO] que gere as tabelas [USERS], [ROLES] e [USERS_ROLES] na base de dados [dbproduitscategories]. É esta que vamos escrever agora;
  

O Spring Security requer a criação de uma classe que implemente a seguinte interface [UsersDetail]:

 

Esta interface é implementada aqui pela classe [AppUserDetails]:


package spring.security.dao;
 
import generic.jdbc.config.ConfigJdbc;
 
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
 
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
 
import spring.jdbc.entities.Role;
import spring.jdbc.entities.User;
import spring.jdbc.infrastructure.DaoException;
 
public class AppUserDetails implements UserDetails {
 
    private static final long serialVersionUID = 1L;
 
    // JdbcTemplate
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    // properties
    private User user;
    private String simpleClassName = getClass().getSimpleName();
 
    // manufacturers
    public AppUserDetails() {
    }
 
    public AppUserDetails(User user, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
        this.user = user;
        this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
    }
 
    // -------------------------interface
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : getRoles(user.getId())) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
...
 
    // méthodes privées----------------
    private List<Role> getRoles(Long id) {
        try {
            // search for the user by id
            return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ROLES_BYUSERID, Collections.singletonMap("id", id),    new ShortRoleMapper());
        } catch (Exception e) {
            //e.printStackTrace();
            throw new DaoException(167, e, simpleClassName);
        }
    }
 
}
 
// --------------------- mappers
class ShortRoleMapper implements RowMapper<Role> {
 
    @Override
    public Role mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Role(rs.getLong("r_ID"), rs.getLong("r_VERSIONING"), rs.getString("r_NAME"));
    }
}
  • linha 22: a classe [AppUserDetails] implementa a interface [UserDetails];
  • linhas 29-30: a classe encapsula um utilizador (linha 19) e o repositório que fornece detalhes sobre esse utilizador (linha 20);
  • linha 27: o acesso à base de dados será feito via JDBC utilizando o objeto [NamedParameterJdbcTemplate namedParameterJdbcTemplate] definido no projeto [spring-jdbc-generic-04]. Note-se que este objeto não é injetado pelo Spring, como costuma acontecer. É fornecido ao construtor nas linhas 36–39. Porquê? Porque a classe [AppUserDetails] não é um componente Spring (não possui a anotação @Component) e, portanto, não pode ser injetada;
  • linhas 36–39: o construtor que instancia a classe com um utilizador e o seu repositório;
  • Linhas 42–49: Implementação do método [getAuthorities] da interface [UserDetails]. Deve construir uma coleção de elementos do tipo [GrantedAuthority] ou de um tipo derivado. Aqui, usamos o tipo derivado [SimpleGrantedAuthority] (linha 46), que encapsula o nome de uma das funções do utilizador da linha 29;
  • linhas 45–47: percorremos a lista de funções do utilizador da linha 29 para construir uma lista de elementos do tipo [SimpleGrantedAuthority];
  • linha 45: para obter as funções do utilizador, utilizamos o método privado [getRoles] da linha 53;
  • linha 56: executa a seguinte instrução SQL [ConfigJdbc.SELECT_ROLES_BYUSERID] (definida em [ConfigJdbc]):

public static final String SELECT_ROLES_BYUSERID = "SELECT DISTINCT r.ID as r_ID, r.VERSIONING as r_VERSIONING, r.NAME as r_NAME FROM ROLES r, USERS u, USERS_ROLES ur WHERE u.ID=:id AND ur.USER_ID=u.ID AND ur.ROLE_ID=r.ID";

Esta consulta SQL realiza uma junção entre as três tabelas [USERS, ROLES, USERS_ROLES] para recuperar as funções de um utilizador identificado pela sua chave primária. É parametrizada pela chave primária [:id] do utilizador cujas funções estão a ser consultadas.

  • linha 56: cada linha de resultados do [SELECT] é convertida numa entidade [Role] pela classe [ShortRowMapper] nas linhas 66–72;

Voltemos ao código da classe [AppUserDetails]:


package spring.security.dao;
 
...
 
public class AppUserDetails implements UserDetails {
 
    private static final long serialVersionUID = 1L;
 
    // JdbcTemplate
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    // properties
    private User user;
    private String simpleClassName = getClass().getSimpleName();
 
    // manufacturers
    public AppUserDetails() {
    }
 
    public AppUserDetails(User user, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
        this.user = user;
        this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
    }
 
    // -------------------------interface
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : getRoles(user.getId())) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
 
    @Override
    public String getPassword() {
        return user.getPassword();
    }
 
    @Override
    public String getUsername() {
        return user.getLogin();
    }
 
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
 
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
 
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
 
    @Override
    public boolean isEnabled() {
        return true;
    }
 
    // getters and setters
    ...
}
  • linhas 35–37: implementa o método [getPassword] da interface [UserDetails]. Devolvemos a palavra-passe do utilizador da linha 12;
  • linhas 39–42: implementar o método [getUserName] da interface [UserDetails]. Devolvemos o nome de utilizador do utilizador da linha 12;
  • linhas 44–47: a conta do utilizador nunca expira;
  • linhas 49–52: a conta do utilizador nunca é bloqueada;
  • linhas 54–57: as credenciais do utilizador nunca expiram;
  • linhas 59–62: a conta do utilizador está sempre ativa;

O Spring Security também requer a existência de uma classe que implemente a interface [AppUserDetailsService]:

 

Esta interface é implementada pela seguinte classe [AppUserDetailsService]:


package spring.security.dao;
 
import generic.jdbc.config.ConfigJdbc;
 
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
 
import spring.jdbc.entities.User;
import spring.jdbc.infrastructure.DaoException;
 
@Service
public class AppUserDetailsService implements UserDetailsService {
 
    // injections
    @Autowired
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
 
    // local
    private String simpleClassName = getClass().getSimpleName();
 
    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        List<User> users;
        try {
            // search for user via login
            users = namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_USER_BYLOGIN,
                    Collections.singletonMap("login", login), new ShortUserMapper());
        } catch (Exception e) {
            throw new DaoException(145, e, simpleClassName);
        }
        // found?
        if (users.size() == 0) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // render user details
        return new AppUserDetails(users.get(0), namedParameterJdbcTemplate);
    }
}
 
// --------------------- mappers
class ShortUserMapper implements RowMapper<User> {
 
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new User(rs.getLong("u_ID"), rs.getLong("u_VERSIONING"), rs.getString("u_NAME"), rs.getString("u_LOGIN"),
                rs.getString("u_PASSWORD"));
    }
}
  • linha 21: a classe será um componente Spring;
  • linhas 25-26: o acesso à base de dados será feito via JDBC utilizando o objeto [NamedParameterJdbcTemplate] definido nos beans do projeto [spring-jdbc-generic-04];
  • linhas 31-49: implementação do método [loadUserByUsername] da interface [UserDetailsService] (linha 22). O parâmetro é o nome de utilizador do utilizador;
  • linhas 36–37: o utilizador é pesquisado pelo seu nome de utilizador. A instrução SQL [ConfigJdbc.SELECT_USER_BYLOGIN] é a seguinte:

public static final String SELECT_USER_BYLOGIN = "SELECT u.ID as u_ID, u.VERSIONING as u_VERSIONING, u.NAME as u_NAME,u.LOGIN as u_LOGIN,u.PASSWORD as u_PASSWORD FROM USERS u WHERE u.LOGIN= :login";

Cada linha devolvida pela instrução SELECT é convertida numa entidade [User] pela classe [ShortUserMapper] nas linhas 52–58.

  • Linhas 42–44: Se não for encontrada, é lançada uma exceção;
  • linha 46: um objeto [AppUserDetails] é construído e devolvido. É, de facto, do tipo [UserDetails] (linha 32). Duas informações são passadas ao seu construtor:
    • o utilizador que foi encontrado;
    • o objeto [namedParameterJdbcTemplate] que permitirá à classe [AppUserDetails] consultar a base de dados;

20.2.3. A camada [web]

O projeto [spring-security-server-jdbc-generic] depende do projeto [spring-webjson-server-jdbc-generic]:

  

Este projeto implementa a camada [web]. Não é necessário modificá-lo.

20.2.4. Configuração de segurança do projeto

O projeto é configurado pela seguinte classe [AppConfig]:

1
  

Já nos deparámos com uma classe de configuração do Spring Security (ver Secção 19.2.4):


package hello;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
 
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}

Seguiremos o mesmo procedimento:

  • linha 11: definir uma classe que estenda a classe [WebSecurityConfigurerAdapter];
  • linha 13: definir um método [configure(HttpSecurity http)] que define os direitos de acesso às várias URLs do serviço web;
  • linha 19: definir um método [configure(AuthenticationManagerBuilder auth)] que define os utilizadores e as suas funções;

A classe [AppConfig] será a seguinte:


package spring.security.config;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 
import spring.security.dao.AppUserDetailsService;
import spring.webjson.server.config.WebConfig;
 
@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = { "spring.security.dao", "spring.security.service" })
@Import({ spring.webjson.server.config.AppConfig.class, WebConfig.class })
public class AppConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AppUserDetailsService appUserDetailsService;
 
    // security
    private boolean activateSecurity = true;
 
    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // authentication is performed by bean [appUserDetailsService]
        // the password is encrypted using the BCrypt hash algorithm
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // secure application?
        if (activateSecurity) {
            // the password is transmitted by the header Authorization: Basic xxxx
            http.httpBasic();
            // the HTTP OPTIONS method must be authorized for all
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // only the ADMIN role can use the application
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // all URL
                    .hasRole("ADMIN");
            // session or not?
            //http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • linha 17: a classe é uma classe de configuração do Spring;
  • linha 18: ativa os componentes do Spring Security;
  • linha 26: recuperamos os componentes Spring da camada [DAO2] e os do pacote [spring.security.service], que discutiremos mais adiante;
  • linha 23: importa os beans do projeto [spring-webjson-server-jdbc-generic], que implementa a camada [web]. Entre esses beans estão também os da camada [DAO1];
  • linhas 22–23: a classe [AppUserDetails], que fornece acesso aos utilizadores da aplicação, é injetada;
  • linha 26: um booleano que protege (true) ou não protege (false) a aplicação web;
  • linhas 28–33: o método [configure(HttpSecurity http)] define os utilizadores e as suas funções. Recebe um [AuthenticationManagerBuilder] como parâmetro. Este parâmetro é enriquecido com duas informações (linha 32):
    • uma referência ao [appUserDetailsService] da linha 23, que fornece acesso aos utilizadores registados. Note-se aqui que o facto de estarem armazenados numa base de dados não é explicitamente mencionado. Podem, portanto, estar num cache, fornecidos por um serviço web, etc.
    • o tipo de encriptação utilizado para a palavra-passe. Utilizámos o algoritmo BCrypt;
  • linhas 35–53: o método [configure(HttpSecurity http)] define os direitos de acesso às URLs do serviço web;
  • linha 38: vimos no projeto introdutório que, por predefinição, o Spring Security gere um token CSRF (Cross-Site Request Forgery) que o utilizador que deseja autenticar-se deve enviar de volta ao servidor. Aqui, este mecanismo está desativado. Isto, combinado com o booleano (isSecured=false), permite que a aplicação web seja utilizada sem segurança;
  • linha 42: ativamos a autenticação através de cabeçalhos HTTP. O cliente deve enviar o seguinte cabeçalho HTTP:
Authorization:Basic code

onde code é a codificação Base64 da cadeia de caracteres login:password. Por exemplo, a codificação Base64 da cadeia de caracteres admin:admin é YWRtaW46YWRtaW4=. Portanto, um utilizador com o nome de utilizador [admin] e a palavra-passe [admin] enviará o seguinte cabeçalho HTTP para se autenticar:

Authorization:Basic YWRtaW46YWRtaW4=
  • Linhas 47–49: indicam que todas as URLs do serviço web são acessíveis a utilizadores com a função [ROLE_ADMIN]. Isto significa que um utilizador sem esta função não pode aceder ao serviço web;
  • Linha 51: No modo [session], um utilizador que se tenha autenticado uma vez não precisa de o fazer para acessos subsequentes. Esta é a configuração padrão do Spring Security. A linha 51 desativa este modo. Se ativado, o utilizador deve autenticar-se em cada acesso. Sem uma sessão, o serviço web seguro é menos responsivo do que com uma sessão, pelo que a linha 51 foi comentada;

20.2.5. Testar o serviço web seguro

Iremos testar o serviço web utilizando o cliente Chrome [Advanced Rest Client]. Teremos de especificar o cabeçalho de autenticação HTTP:

Authorization:Basic code

onde [código] é a cadeia codificada em Base64 [login:password]. Para gerar este código, pode utilizar o seguinte programa do projeto [spring-security-create-users]:

  

package spring.security.helpers;
 
import org.springframework.security.crypto.codec.Base64;
 
public class Base64Encoder {
 
    public static void main(String[] args) {
        // we expect two arguments: login password
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // we retrieve the two arguments
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // encode the string
        byte[] data = Base64.encode(chaîne.getBytes());
        // displays its Base64 encoding
        System.out.println(new String(data));
    }
 
}

Se executarmos este programa com os dois argumentos [admin admin]:

  

obtemos o seguinte resultado:

YWRtaW46YWRtaW4=

Estamos agora prontos para o teste:

  • o SGBD MySQL deve estar em execução;
  • preenchemos as tabelas [PRODUCTS] e [CATEGORIES] utilizando a configuração de execução denominada [spring-jdbc-generic-04-fillDataBase]:
 
  • Se isso ainda não tiver sido feito, preenchemos as tabelas [USERS, ROLES, USERS_ROLES] utilizando a configuração de execução denominada [spring-security-create-users-hibernate-eclipselink]:
 
  • Iniciamos o serviço web seguro utilizando a configuração de tempo de execução denominada [spring-security-server-jdbc-generic]:
 

Em seguida, utilizando o cliente Chrome [Advanced Rest Client], solicitamos a versão longa de todas as categorias:

  • Em [1], solicitamos a URL das descrições completas das categorias;
  • em [2], utilizando o método GET;
  • em [3], fornecemos o cabeçalho HTTP de autenticação. O código [YWRtaW46YWRtaW4=] é a codificação Base64 da cadeia [admin:admin];
  • em [4], enviamos o pedido HTTP;

A resposta do servidor é a seguinte:

  • em [1], o cabeçalho de autenticação HTTP;
  • em [2], o servidor devolve uma resposta JSON;

Conseguimos obter a lista de categorias:

 

Agora vamos tentar uma solicitação HTTP com um cabeçalho de autenticação incorreto. A resposta é então a seguinte:

  • em [1]: o cabeçalho de autenticação HTTP;

Recebemos a seguinte resposta:

  • em [2]: a resposta do serviço web;

Agora, vamos experimentar o utilizador / utilizador. Ele existe, mas não tem acesso ao serviço web. Se executarmos o programa de codificação Base64 com os dois argumentos [utilizador utilizador]:

  

obtemos o seguinte resultado:

dXNlcjp1c2Vy
  • em [1]: o cabeçalho de autenticação HTTP incorreto;
  • em [2]: a resposta do serviço web. Diferencia-se da anterior, que era [401 Não autorizado]. Desta vez, o utilizador autenticou-se corretamente, mas não possui permissões suficientes para aceder ao URL;

Um serviço web seguro está agora operacional.

20.2.6. Um URL de autenticação

  

Vamos criar um URL que nos permita determinar se um utilizador está autorizado a aceder ao serviço web. Para tal, criamos o seguinte novo controlador MVC [AuthenticateController]:


package spring.security.service;
 
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
 
import spring.webjson.server.service.Response;
 
@RestController
public class AuthenticateController {
    @RequestMapping(value = "/authenticate", method = RequestMethod.GET)
    public Response<Void> authenticate() {
        return new Response<Void>(0, null, null);
    }
 
}
  • linha 9: a classe [AuthenticateController] é um controlador Spring. Como tal, expõe URLs. A anotação [@RestController] indica que os métodos que tratam destas URLs devolvem as suas próprias respostas ao cliente;
  • linha 11: expõe a URL [/authenticate];
  • linhas 12–14: o método simplesmente devolve um objeto [Response] vazio, mas com um [status] igual a 0, indicando que não ocorreu nenhum erro;

Para que serve esta URL? Quando quisermos simplesmente autenticar um utilizador, iremos solicitá-la. Vimos que, se a camada de segurança não aceitar este utilizador, lança uma exceção. Aqui está um exemplo;

Com o utilizador [admin:admin]:

Recebemos uma resposta vazia, mas sem exceção.

Com o utilizador [user:user]:

Ocorreu uma exceção.

20.2.7. Conclusão

As classes necessárias para o Spring Security foram adicionadas sem alterar o projeto web/JSON original. Este cenário ideal deve-se ao facto de as três tabelas adicionadas à base de dados serem independentes das tabelas existentes. Poderíamos até tê-las colocado numa base de dados separada. Noutros casos, as tabelas adicionadas podem ter relações com tabelas existentes. O código na camada [DAO] existente deve, então, ser revisto.

20.3. Um cliente programado para o serviço web/JSON seguro

Já escrevemos um cliente para o serviço web/JSON não seguro:

Vamos agora criar um cliente programado para o serviço web seguro:

  

20.3.1. A camada [Cliente HTTP]

 

A classe [Client] gere a comunicação HTTP com o servidor web seguro / JSON. Como acabámos de ver, nesta comunicação HTTP, o cliente deve agora enviar um cabeçalho de autenticação, por exemplo:

Authorization:Basic YWRtaW46YWRtaW4=

A interface [IClient] passa a ser a seguinte:


package spring.webjson.client.dao;
 
import org.springframework.http.HttpMethod;
 
import spring.webjson.client.entities.Credentials;
 
public interface IClient {
    public <T1, T2> T1 getResponse(Credentials credentials,String url, HttpMethod method, int errStatus, T2 body);
}
  • Linha 8: O primeiro parâmetro do método [getResponse] é agora um objeto [Credentials] que encapsula as credenciais do utilizador:

package spring.security.client.entities;
 
public class Credentials {
 
    // properties
    private String login;
    private String password;
 
    // manufacturer
    public Credentials() {
    }
 
    public Credentials(String login, String password) {
        this.login = login;
        this.password = password;
    }
 
    // getters and setters
...
}

A classe [Client], que implementa a interface [IClient], evolui da seguinte forma:


package spring.security.client.dao;
 
...
 
@Component
public class Client implements IClient {
 
    // injections
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;
 
    // local
    private String simpleClassName = getClass().getSimpleName();
 
    private String getBase64(Credentials credentials) {
        // encodes user and password in base 64 - requires java 8
        String chaîne = String.format("%s:%s", credentials.getLogin(), credentials.getPassword());
        return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
    }
 
    // generic request
    @Override
    public <T1, T2> T1 getResponse(Credentials credentials, String url, HttpMethod method, int errStatus, T2 body) {
        // the server response
        ResponseEntity<Response<T1>> response;
        try {
            // prepare the query
            RequestEntity<?> request = null;
            if (method == HttpMethod.GET) {
                HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))    .accept(MediaType.APPLICATION_JSON);
                if (credentials != null) {
                    headersBuilder = headersBuilder.header("Authorization", getBase64(credentials));
                }
                request = headersBuilder.build();
            }
            if (method == HttpMethod.POST) {
                BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url))).header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
                if (credentials != null) {
                    bodyBuilder = bodyBuilder.header("Authorization", getBase64(credentials));
                }
                request = bodyBuilder.body(body);
            }
            // execute the query
            response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
            });
        } catch (Exception e) {
            // encapsulate the exception
            throw new DaoException(errStatus, e, simpleClassName);
        }
...
    }
...
}
  • linhas 33–35, 40–42: se as [credenciais] do utilizador não forem nulas, então o cabeçalho de autenticação é adicionado. A codificação Base64 do nome de utilizador e da palavra-passe é tratada pelo método [getBase64] nas linhas 17–21. Note-se que este método utiliza uma classe [Base64] do JDK 1.8. O nosso cliente HTTP pode funcionar com um serviço web não seguro. Basta passar-lhe um [credentials] igual a nulo;
  • À exceção das linhas anteriores, o código permanece inalterado;

20.3.2. A camada [DAO]

20.3.2.1. A interface [IDao]

  

Todos os métodos da interface [IDao] no projeto [spring-webjson-client-generic] recebem um parâmetro [Credentials] adicional:


package spring.security.client.dao;
 
import java.util.List;
 
import spring.security.client.entities.AbstractCoreEntity;
import spring.security.client.entities.Credentials;
 
public interface IDao<T extends AbstractCoreEntity> extends IAuthenticate {
 
    // list of all T entities
    public List<T> getAllShortEntities(Credentials credentials);
 
    public List<T> getAllLongEntities(Credentials credentials);
 
    // special entities - short version
    public List<T> getShortEntitiesById(Credentials credentials, Iterable<Long> ids);
 
    public List<T> getShortEntitiesById(Credentials credentials, Long... ids);
 
    public List<T> getShortEntitiesByName(Credentials credentials, Iterable<String> names);
 
    public List<T> getShortEntitiesByName(Credentials credentials, String... names);
 
    // special entities - long version
    public List<T> getLongEntitiesById(Credentials credentials, Iterable<Long> ids);
 
    public List<T> getLongEntitiesById(Credentials credentials, Long... ids);
 
    public List<T> getLongEntitiesByName(Credentials credentials, Iterable<String> names);
 
    public List<T> getLongEntitiesByName(Credentials credentials, String... names);
 
    // update of several entities
    public List<T> saveEntities(Credentials credentials, Iterable<T> entities);
 
    public List<T> saveEntities(Credentials credentials, @SuppressWarnings("unchecked") T... entities);
 
    // delete all entities
    public void deleteAllEntities(Credentials credentials);
 
    // deletion of multiple entities
    public void deleteEntitiesById(Credentials credentials, Iterable<Long> ids);
 
    public void deleteEntitiesById(Credentials credentials, Long... ids);
 
    public void deleteEntitiesByName(Credentials credentials, Iterable<String> names);
 
    public void deleteEntitiesByName(Credentials credentials, String... names);
 
    public void deleteEntitiesByEntity(Credentials credentials, Iterable<T> entities);
 
    public void deleteEntitiesByEntity(Credentials credentials, @SuppressWarnings("unchecked") T... entities);
}
  • Linha 8: A interface [IDao] estende a seguinte interface [IAuthenticate]:

package spring.security.client.dao;
 
import spring.security.client.entities.Credentials;
 
public interface IAuthenticate {
    // authentication
    public void authenticate(Credentials credentials);
}

A interface [IAuthenticate] possui apenas um único método, [authenticate]. Este método não retorna nada (void) se o utilizador [Credentials credentials] for aceite pelo serviço web seguro; caso contrário, lança uma exceção.

20.3.2.2. A classe [AbstractDao]

  

Recorde-se que a classe [AbstractDao] é a classe pai das classes [DaoCategorie], que gerem URLs de categorias, e da classe [DaoProduit], que gere URLs de produtos. Todos os métodos da classe [AbstractDao] no projeto [spring-webjson-client-generic] recebem um parâmetro adicional [Credentials credentials] que passam para a classe filha. Aqui está um exemplo:


    @Override
    public List<T1> getShortEntitiesById(Credentials credentials, Iterable<Long> ids) {
        // validité de l'argument
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // résultat
        return getShortEntitiesById(credentials, Lists.newArrayList(ids));
}
  • O método [getShortEntitiesById] recebe o parâmetro [Credentials] (linha 2), que passa (linha 9) para o método [getShortEntitiesById] da classe filha;

A classe [AbstractDao] tem a seguinte estrutura:


package spring.security.client.dao;
 
import java.util.ArrayList;
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
 
import spring.security.client.entities.AbstractCoreEntity;
import spring.security.client.entities.Credentials;
import spring.security.client.infrastructure.MyIllegalArgumentException;
 
import com.google.common.collect.Lists;
 
public abstract class AbstractDao<T1 extends AbstractCoreEntity> implements IDao<T1> {
 
    @Autowired
    private IAuthenticate authenticate;
 
...
}
  • linha 14: a classe implementa a interface [IDao] que descrevemos;
  • linhas 16–17: é injetada uma instância da interface [IAuthenticate]. Esta é implementada pela seguinte classe [Authenticate]:

package spring.security.client.dao;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
 
import spring.security.client.entities.Credentials;
 
@Component
public class Authenticate implements IAuthenticate{
    @Autowired
    protected IClient client;
 
    // verification [credentials,mdp]
    public void authenticate(Credentials credentials) {
        client.<Void, Void> getResponse(credentials, "/authenticate", HttpMethod.GET, 111, (Void) null);
    }
 
}
  • linha 9: a classe [Authenticate] é um componente Spring;
  • linha 10: que implementa a interface [IAuthenticate];
  • linhas 11–12: injeção do cliente HTTP que permite a comunicação com o serviço web seguro;
  • linhas 15–17: implementação do método [authenticate] da interface;
  • linha 16: é enviada uma solicitação HTTP GET para a URL [/authenticate]. A utilização desta URL foi demonstrada na secção 20.2.6. O princípio é que a chamada resulta numa exceção se as [credenciais] do utilizador forem desconhecidas ou não tiverem permissões suficientes;

A classe [AbstractDao] implementa o método [authenticate] da interface [IDao] da seguinte forma:


    @Autowired
    private IAuthenticate authenticate;
 
    @Override
    public void authenticate(Credentials credentials) {
        authenticate.authenticate(credentials);
}
  • Linha 7: A tarefa é delegada ao método [authenticate] da classe [Authenticate]. Será, portanto, lançada uma exceção se o utilizador [Credentials credentials] não for aceite pelo serviço web seguro;

20.3.2.3. As classes [DaoCategorie, DaoProduit]

  

As classes [DaoCategorie, DaoProduit] são as do projeto [spring-webjson-server-generic] com o parâmetro adicional [Credentials credentials]. Aqui está um exemplo:


@Component
public class DaoCategorie extends AbstractDao<Categorie> {
 
    // injections
    @Autowired
    protected ApplicationContext context;
    @Autowired
    protected IClient client;
 
    @Override
    public List<Categorie> getAllShortEntities(Credentials credentials) {
        try {
            // filters jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // get all categories
            Object map = client.<List<Categorie>, Void> getResponse(credentials, "/getAllShortCategories", HttpMethod.GET,
                    202, null);
            // the List<Categorie> category list
            return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
            });
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(221, e2, simpleClassName);
        }
    }
....

20.3.3. A configuração do Spring

  

A classe [AppConfig] configura o ambiente Spring do projeto. É idêntica à do projeto [spring-webjson-client-generic], com uma exceção:


@Configuration
@ComponentScan({ "spring.security.client.dao" })
public class AppConfig {
  • linha 2: deve especificar o pacote da nova camada [DAO];

20.3.4. Testes para a camada [DAO]

  

20.3.4.1. O teste [JUnitTestCredentials]

O teste [JUnitTestCredentials] utiliza o método [IDao.authenticate] para verificar a validade de determinados utilizadores:


package client.tests.junit;
 
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import spring.security.client.config.AppConfig;
import spring.security.client.dao.IAuthenticate;
import spring.security.client.entities.Credentials;
import spring.security.client.infrastructure.DaoException;
 
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestCredentials {

    // layer [DAO]
    @Autowired
    private IAuthenticate authenticate;
 
    // users
    static private Credentials admin;
    static private Credentials user;
    static private Credentials unknown;
 
    @BeforeClass
    public static void init() {
        admin = new Credentials("admin", "admin");
        user = new Credentials("user", "user");
        unknown = new Credentials("x", "y");
    }
 
    @Test()
    public void checkUserUser() {
        DaoException se = null;
        try {
            authenticate.authenticate(user);
        } catch (DaoException e) {
            se = e;
            System.out.println("checkUserUser: " + e);
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("403 Forbidden", se.getExceptions().get(0).getErrorMessage());
    }
 
    @Test()
    public void checkUserUnknown() {
        DaoException se = null;
        try {
            authenticate.authenticate(unknown);
        } catch (DaoException e) {
            se = e;
            System.out.println("checkUserUnknown : " + e);
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("401 Unauthorized", se.getExceptions().get(0).getErrorMessage());
    }
 
    @Test()
    public void checkUserAdmin() {
        DaoException se = null;
        try {
            authenticate.authenticate(admin);
        } catch (DaoException e) {
            se = e;
            System.out.println("checkUserAdmin : " + e);
        }
        Assert.assertNull(se);
    }
}
  • Durante a inicialização da classe de teste, nas linhas 29–34, são criados três utilizadores:
    • o utilizador [admin] tem acesso aos URLs do serviço web. Isto é testado nas linhas 63–72;
    • o utilizador [user] existe, mas não está autorizado a utilizar os URLs do serviço web. São testados nas linhas 37–47;
    • o utilizador [unknown] não existe. É testado nas linhas 50–60;

Lançamos o serviço web seguro com a configuração de tempo de execução denominada [spring-security-server-jdbc-generic] [1]:

Em seguida, executamos o teste JUnit [JUnitTestCredentials] com a configuração de tempo de execução [spring-security-client-generic-JUnitTestCredentials] [2]. Os resultados obtidos na consola são os seguintes:

checkUserUser: [code=111, trace=[Client,getResponse,65], exceptions=[{"className":"org.springframework.web.client.HttpClientErrorException","errorMessage":"403 Forbidden"}]
checkUserUnknown : [code=111, trace=[Client,getResponse,65], exceptions=[{"className":"org.springframework.web.client.HttpClientErrorException","errorMessage":"401 Unauthorized"}]

e o teste é aprovado:

  

20.3.4.2. O teste [JUnitTestDao]

O teste [JUnitTestDao] é idêntico ao que era no projeto [spring-webjson-client-generic] não seguro, exceto que agora os métodos da camada [DAO] que estão a ser testados têm todos o utilizador [admin / admin] como primeiro parâmetro:


@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao {
 
    // spring context
    @Autowired
    private ApplicationContext context;
    // layer [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;
 
....
 
    // users
    static private Credentials admin;
 
    @BeforeClass
    public static void init() {
        admin = new Credentials("admin", "admin");
    }
 
    @Before
    public void clean() {
        // the base is cleaned before each test
        log("Vidage de la base de données", 1);
        // we empty table [CATEGORIES] and cascade table [PRODUITS]
        daoCategorie.deleteAllEntities(admin);
        // emptying dictionaries
        for (Long id : mapCategories.keySet()) {
            mapCategories.remove(id);
        }
        for (Long id : mapProduits.keySet()) {
            mapProduits.remove(id);
        }
    }
 
    private List<Categorie> fill(int nbCategories, int nbProduits) {
        // fill the tables
        ...
        // adding the category - by cascading the products will also be
        // inserted - the result is returned at the same time
        return daoCategorie.saveEntities(admin, categories);
    }
 
    private Object[] showDataBase() throws BeansException, JsonProcessingException {
        // list of categories
        log("Liste des catégories", 2);
        List<Categorie> categories = daoCategorie.getAllShortEntities(admin);
        affiche(categories, context.getBean("jsonMapperShortCategorie", ObjectMapper.class));
        // product list
        log("Liste des produits", 2);
        List<Produit> produits = daoProduit.getAllShortEntities(admin);
        affiche(produits, context.getBean("jsonMapperShortProduit", ObjectMapper.class));
        // result
        return new Object[] { categories, produits };
    }
...

Todas as operações são realizadas utilizando o utilizador [admin / admin], que é o único com direitos de acesso ao serviço web seguro.

Iremos executar o teste utilizando a configuração de execução denominada [spring-security-client-generic-JUnitTestDao]:

 

O teste é bem-sucedido, mas podemos ver que é mais lento do que com o serviço web não seguro. Proteger uma aplicação aumenta significativamente os seus tempos de resposta. Há um fator importante que afeta o desempenho do serviço web protegido: na classe [AppConfig] que o configura, escrevemos:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // secure application?
        if (activateSecurity) {
            // the password is transmitted by the header Authorization: Basic xxxx
            http.httpBasic();
            // the HTTP OPTIONS method must be authorized for all
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // only the ADMIN role can use the application
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // all URL
                    .hasRole("ADMIN");
            // no session
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
}

A linha 17 tem impacto. Determina se o utilizador é ou não obrigado a autenticar-se em cada acesso. Se a comentarmos, a duração do teste JUnit é significativamente mais curta, porque o utilizador [admin] apenas se autentica no primeiro teste e não nos subsequentes (mesmo que o cabeçalho de autenticação HTTP seja enviado pelo cliente, o servidor não volta a verificar a palavra-passe do utilizador).

20.4. O projeto Eclipse [spring-security-server-jpa-generic]

O serviço web seguro será agora implementado pelo projeto [spring-security-server-jpa-generic], que se baseia no projeto [spring-jpa-generic] que gere o acesso à base de dados utilizando o Spring Data JPA:

Acima:

  • a camada [DAO1] é a camada [DAO] que gere as tabelas [PRODUCTS] e [CATEGORIES] na base de dados [dbproduitscategories]. Já foi escrita;
  • a camada [DAO2] é a camada [DAO] que gere as tabelas [USERS], [ROLES] e [USERS_ROLES] na base de dados [dbproduitscategories]. Ainda não foi escrita;

O projeto [spring-security-server-jpa-generic] é inicialmente criado através da clonagem do projeto anteriormente estudado [spring-security-server-jdbc-generic]. Na verdade, as camadas [web] e [security] permanecem inalteradas porque:

  • A camada [DAO1 / Repositories / JPA] (já escrita) tem a mesma interface que a camada [DAO1 / JDBC];
  • a camada [DAO2 / Repositories / JPA] (a ser escrita) terá a mesma interface que a camada [DAO2 / JDBC];

O projeto [spring-security-server-jpa-generic] é o seguinte:

  
  • o pacote [spring.security.repositories] implementa a camada [repositories];
  • o pacote [spring.security.dao] implementa a camada [dao2];

20.4.1. O projeto Maven

O projeto é um projeto Maven configurado pelo seguinte ficheiro [pom.xml]:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-security-server-jpa-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>
 
    <name>spring-security-server-jpa-generic</name>
    <description>démo spring security</description>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>
 
    <dependencies>
        <!-- Spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- web server / jSON -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-webjson-server-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  • linhas 24–27: a dependência para a camada [security] do projeto;
  • linhas 29–33: a dependência para a camada [web] do projeto. O projeto [spring-webjson-server-jpa-generic] implementa totalmente a camada [web]. Esta camada não precisa de ser escrita nem modificada;

No final, as dependências são as seguintes:

  

20.4.2. A configuração do Spring

  

O ficheiro de configuração [AppConfig] do projeto anterior [spring-security-server-jdbc-generic] é adequado. Basta adicionar uma configuração adicional:


@Configuration
@EnableWebSecurity
@EnableJpaRepositories(basePackages = { "spring.security.repositories" })
@ComponentScan(basePackages = { "spring.security.dao", "spring.security.service" })
@Import({ spring.webjson.server.config.AppConfig.class })
public class AppConfig extends WebSecurityConfigurerAdapter {
  • linha 3: declaramos o pacote que implementa a camada [repositories];
  • linha 4: os pacotes que contêm os beans Spring têm o mesmo nome no novo projeto;
  • linha 5: no projeto anterior, a classe [spring.webjson.server.config.AppConfig] encontrava-se na dependência [spring-webjson-server-jdbc-generic]. Aqui, ela encontra-se na dependência [spring-webjson-server-jpa-generic];

20.4.3. A camada JPA

As entidades JPA geridas pela camada [JPA] encontram-se no projeto [mysql-config-jpa-hibernate] [2], que é uma dependência do projeto [1]:

A classe [User] corresponde à tabela [USERS]:

Image

  • ID: chave primária;
  • VERSION: coluna de versionamento de linhas;
  • IDENTITY: um identificador descritivo para o utilizador;
  • LOGIN: o nome de utilizador do utilizador;
  • PASSWORD: a palavra-passe do utilizador;

package generic.jpa.entities.dbproduitscategories;
 
import generic.jdbc.config.ConfigJdbc;
 
import java.util.List;
 
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
 
import com.fasterxml.jackson.annotation.JsonIgnore;
 
@Entity
@Table(name = ConfigJdbc.TAB_USERS)
public class User implements AbstractCoreEntity {
    // properties
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = ConfigJdbc.TAB_JPA_ID)
    protected Long id;
 
    @Version
    @Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
    protected Long version;
 
    @Transient
    protected EntityType entityType=EntityType.POJO;
 
    // properties
    @Column(name = ConfigJdbc.TAB_USERS_NAME, length = 30, nullable = false)
    private String name;
    @Column(name = ConfigJdbc.TAB_USERS_LOGIN, length = 30, unique = true, nullable = false)
    private String login;
    @Column(name = ConfigJdbc.TAB_USERS_PASSWORD, length = 60, nullable = false)
    private String password;
 
    // the associated UserRole
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = { CascadeType.ALL })
    @JsonIgnore
    private List<UserRole> userRoles;
 
    // manufacturers
    public User() {
    }
 
    public User(Long id, Long version, String identity, String login, String password) {
        this.id = id;
        this.version = version;
        this.name = identity;
        this.login = login;
        this.password = password;
    }
 
    // ------------------------------------------------------------
    // redefine [equals] and [hashcode]
...
 
    // getters and setters
...
}
  • linha 23: a classe implementa a interface [AbstractCoreEntity] já utilizada para as outras entidades;
  • linhas 34–35: o tipo de entidade. Esta propriedade não é persistida na base de dados [@Transient];
  • linhas 38–43: as três propriedades básicas de um utilizador (nome, login, palavra-passe);
  • linhas 46–48: a lista de funções do utilizador. Um utilizador pode ter várias funções. Da mesma forma, veremos que uma função pode estar associada a vários utilizadores. Assim, no sentido do termo JPA, existe uma relação [ManyToMany] entre as entidades [User] e [Role]:
    • Um utilizador pode ser atribuído a várias funções;
    • Uma função pode estar associada a vários utilizadores;

Esta relação [ManyToMany] é implementada na base de dados através da tabela de ligação [USERS_ROLES]. Se um utilizador U tiver uma relação com uma função R, essa relação é armazenada na tabela [USERS_ROLES] através do registo do par de chaves primárias das entidades (U,R). No lado do JPA, a relação [ManyToMany] que liga as entidades [User] e [Role] pode ser dividida em duas relações [ManyToOne, OneToMany]:

  • (continuação)
    • uma relação [ManyToOne] da entidade [User] para a entidade [UserRole];
    • uma relação [OneToMany] da entidade [UserRole] para a entidade [UserRole];

Da mesma forma, a relação [ManyToMany] que liga as entidades [Role] e [User] pode ser dividida em duas relações [ManyToOne, OneToMany]:

  • (continuação)
    • uma relação [ManyToOne] da entidade [Role] para a entidade [UserRole];
    • uma relação [OneToMany] da entidade [UserRole] para a entidade [User];
  • linha 48: o facto de um utilizador ter várias funções é representado por uma relação [OneToMany] com a entidade [UserRole];

A classe [Role] representa a tabela [ROLES]:

Image

  • ID: chave primária;
  • VERSION: coluna de versionamento de linhas;
  • NAME: nome da função. Por predefinição, o Spring Security espera nomes no formato ROLE_XX, por exemplo, ROLE_ADMIN ou ROLE_GUEST;

package generic.jpa.entities.dbproduitscategories;
 
import generic.jdbc.config.ConfigJdbc;
 
import java.util.List;
 
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
 
import com.fasterxml.jackson.annotation.JsonIgnore;
 
@Entity
@Table(name = ConfigJdbc.TAB_ROLES)
public class Role implements AbstractCoreEntity {
    // properties
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = ConfigJdbc.TAB_JPA_ID)
    protected Long id;
    
    @Version
    @Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
    protected Long version;
 
    @Transient
    protected EntityType entityType=EntityType.POJO;
 
    // properties
    @Column(name = ConfigJdbc.TAB_ROLES_NAME, length = 30, unique = true, nullable = false)
    private String name;
 
    // the associated UserRole
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "role", cascade = { CascadeType.ALL })
    @JsonIgnore
    private List<UserRole> userRoles;
 
    // manufacturers
    public Role() {
    }
 
    public Role(Long id, Long version, String name) {
        this.id = id;
        this.version = version;
        this.name = name;
    }
 
    // getters and setters
    public Role(String name) {
        this.name = name;
    }
 
    // ------------------------------------------------------------
    // redefine [equals] and [hashcode]
...
    // getters and setters
...
}
  • linhas 42–44: o facto de vários utilizadores poderem estar associados a uma função é representado por uma relação [@OneToMany] com a entidade [UserRole];

A classe [UserRole] representa a tabela [USERS_ROLES]:

Image

Um utilizador pode ter várias funções e uma função pode ter vários utilizadores. Temos uma relação muitos-para-muitos representada pela tabela [USERS_ROLES].

  • ID: chave primária;
  • VERSION: coluna de versionamento de linhas;
  • USER_ID: identificador do utilizador;
  • ROLE_ID: identificador de uma função;

package generic.jpa.entities.dbproduitscategories;
 
import generic.jdbc.config.ConfigJdbc;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
 
@Entity
@Table(name = ConfigJdbc.TAB_USERS_ROLES)
public class UserRole implements AbstractCoreEntity {
    // properties
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = ConfigJdbc.TAB_JPA_ID)
    protected Long id;
 
    @Version
    @Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
    protected Long version;
 
    @Transient
    protected EntityType entityType=EntityType.POJO;
 
    // a UserRole refers to a User
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_USERS_ROLES_USER_ID, nullable = false)
    private User user;
 
    // a UserRole refers to a Role
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_USERS_ROLES_ROLE_ID, nullable = false)
    private Role role;
 
    // manufacturers
    public UserRole() {
 
    }
 
    public UserRole(User user, Role role) {
        this.user = user;
        this.role = role;
    }
 
    // ------------------------------------------------------------
    // redefine [equals] and [hashcode]
    ...
 
    // getters and setters
...
}
  • linhas 34–36: implementar a chave estrangeira da tabela [USERS_ROLES] para a tabela [USERS];
  • linhas 38–41: implementar a chave estrangeira da tabela [USERS_ROLES] para a tabela [ROLES];

20.4.4. A camada [repositories]

  

A interface [UserRepository] gere o acesso às entidades [User]:


package spring.security.repositories;
 
import generic.jpa.entities.dbproduitscategories.Role;
import generic.jpa.entities.dbproduitscategories.User;
 
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
 
public interface UserRepository extends CrudRepository<User, Long> {
 
    // liste des rôles d'un utilisateur identifié par son id
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);
 
    // liste des rôles d'un utilisateur identifié par son login unique
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);
 
    // recherche d'un utilisateur via son login
    User findUserByLogin(String login);
}
  • linha 9: a interface [UserRepository] estende a interface [CrudRepository] do Spring Data (linha 7);
  • linhas 12-13: o método [getRoles(long id)] recupera todas as funções de um utilizador identificado pelo seu [id]
  • linhas 16-17: igual ao anterior, mas para um utilizador identificado pelo seu nome de utilizador e palavra-passe;
  • Linha 20: para encontrar um utilizador pelo seu nome de utilizador;

A interface [RoleRepository] gere o acesso às entidades [Role]:


package spring.security.repositories;
 
import generic.jpa.entities.dbproduitscategories.Role;
 
import org.springframework.data.repository.CrudRepository;
 
public interface RoleRepository extends CrudRepository<Role, Long> {
 
    // search for a role by name
    Role findRoleByName(String name);
 
}
  • linha 7: a interface [RoleRepository] estende a interface [CrudRepository];
  • linha 10: É possível pesquisar uma função pelo seu nome. Note que a entidade [Role] possui um campo [name]. O método [findEntityByField] é implementado automaticamente pelo Spring Data. Portanto, não há necessidade de implementar o método [findRoleByName] aqui. Basta declará-lo na interface.

A interface [UserRoleRepository] gere o acesso às entidades [UserRole]:


package spring.security.repositories;
 
import generic.jpa.entities.dbproduitscategories.UserRole;
 
import org.springframework.data.repository.CrudRepository;
 
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
 
}
  • Linha 7: A interface [UserRoleRepository] simplesmente estende a interface [CrudRepository] sem adicionar nenhum método novo;

20.4.5. A camada [DAO2]

  

A camada [DAO2] contém as mesmas classes que a camada [DAO2] no projeto [spring-security-server-jdbc-generic] discutido anteriormente na Secção 20.2.2. Agora, basta implementá-las utilizando as classes da camada [repositories].

A classe [AppUserDetails] evolui da seguinte forma:


package spring.security.dao;
 
import generic.jpa.entities.dbproduitscategories.Role;
import generic.jpa.entities.dbproduitscategories.User;
 
import java.util.ArrayList;
import java.util.Collection;
 
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
 
import spring.data.infrastructure.DaoException;
import spring.security.repositories.UserRepository;
 
public class AppUserDetails implements UserDetails {
 
    private static final long serialVersionUID = 1L;
 
    // properties
    private User user;
    private UserRepository userRepository;
    
    // local
    private String simpleClassName = getClass().getSimpleName();
 
    // manufacturers
    public AppUserDetails() {
    }
 
    public AppUserDetails(User user, UserRepository userRepository) {
        this.user = user;
        this.userRepository = userRepository;
    }
 
    // -------------------------interface
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        Iterable<Role> roles;
        try {
            roles = userRepository.getRoles(user.getId());
        } catch (Exception e) {
            e.printStackTrace();
            throw new DaoException(167, e, simpleClassName);
        }
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
...
}
  • Na linha 31, o construtor da classe recebe o objeto [UserRepository] como segundo parâmetro, o que permite à classe recuperar as funções de um determinado utilizador (linha 42);

O componente Spring [AppUserDetailsService] evolui da seguinte forma:


package spring.security.dao;
 
import generic.jpa.entities.dbproduitscategories.User;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
 
import spring.data.infrastructure.DaoException;
import spring.security.repositories.UserRepository;
 
@Service
public class AppUserDetailsService implements UserDetailsService {
 
    @Autowired
    private UserRepository userRepository;
    // local
    private String simpleClassName = getClass().getName();
 
    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // search for user via login
        User user;
        try {
            user = userRepository.findUserByLogin(login);
        } catch (Exception e) {
            throw new DaoException(168, e, simpleClassName);
        }
        // found?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // render user details
        return new AppUserDetails(user, userRepository);
    }
 
}
  • linha 18: injeção do componente Spring [userRepository], que permitirá ao serviço devolver o utilizador identificado pelo seu login, linha 27;

Por fim, percebemos que precisamos apenas do [userRepository] e não dos outros dois repositórios [roleRepository, userRoleRepository]. Estes serão utilizados no próximo projeto, que visa preencher as tabelas [USERS, ROLES, USERS_ROLES].

20.4.6. Testes

O serviço web seguro é iniciado com a configuração denominada [spring-security-server-jpa-generic-hibernate-eclipselink] [1]. O teste [JUnitTestDao] para o cliente genérico é iniciado com a configuração denominada [spring-security-client-generic-JUnitTestDao] [2]:

Os testes são bem-sucedidos.

20.5. O projeto Eclipse [spring-security-create-users]

  

20.5.1. A base de dados

A execução do projeto preenche as tabelas [USERS, ROLES, USERS_ROLES] na base de dados [dbproduitscategories]:

Image

 

As credenciais criadas [nome de utilizador/palavra-passe] são as seguintes: [admin/admin], [user/user], [guest/guest]. Por predefinição, as palavras-passe são encriptadas.

Image

20.5.2. Configuração do Maven

O projeto é um projeto Maven configurado pelo seguinte ficheiro [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-security-create-users-jpa</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>spring-security-create-users-jpa</name>
    <description>création de utilisateurs dans la base [dbproduitscategories]</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>
 
    <dependencies>
        <!-- Spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- spring-security-server-jpa-generic -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-security-server-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  • linhas 22–25: dependência da estrutura Spring Security. O algoritmo de encriptação de senhas é fornecido por esta estrutura
  • linhas 27–31: dependência do projeto [spring-security-server-jpa-generic] que acabámos de criar. Este projeto implementa as camadas [repositories] e [JPA] do projeto;

No final, as dependências são as seguintes:

  

20.5.3. A camada [da consola]

Uma vez que as camadas [repositórios] e [JPA] são implementadas pela dependência [spring-security-server-jpa-generic], resta apenas implementar a camada [console].

  
  • [AppConfig] é a classe de configuração Spring do projeto;
  • [CreateUsers] é a classe executável que cria utilizadores e funções;
  • [Base64Encoder] é uma classe auxiliar para gerar o código Base64 para um par [login, palavra-passe]. Já a utilizámos. Não é necessária para este projeto;

A classe de configuração do Spring [AppConfig] é a seguinte:


package spring.security.install;
 
import generic.jpa.config.ConfigJpa;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
 
@Configuration
@EnableJpaRepositories(basePackages = { "spring.security.repositories" })
@Import({ ConfigJpa.class })
public class AppConfig {
}
  • Linha 10: Especifica onde encontrar os [repositórios] da aplicação. Estes estão localizados no pacote [spring.security.repositories] da dependência [spring-security-server-jpa-generic]
  • Linha 11: Importa os beans da classe [ConfigJpa], que configura a camada [JPA] do projeto. Esta classe encontra-se na dependência [mysql-config-jpa-hibernate]:
  

A classe [CreateUsers] é a seguinte:


package spring.security.install;
 
import generic.jpa.entities.dbproduitscategories.Role;
import generic.jpa.entities.dbproduitscategories.User;
import generic.jpa.entities.dbproduitscategories.UserRole;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
 
import spring.security.repositories.RoleRepository;
import spring.security.repositories.UserRepository;
import spring.security.repositories.UserRoleRepository;
 
public class CreateUsers {
 
    public static void main(String[] args) {
 
        // end
        System.out.println("Travail en cours...");
 
        // we create three users
        String[] logins = { "admin", "user", "guest" };
        String[] passwds = { "admin", "user", "guest" };
        String[] roles = { "admin", "user", "guest" };
 
        // spring context
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        for (int i = 0; i < logins.length; i++) {
            // we retrieve information from user n° i
            String login = logins[i];
            String password = passwds[i];
            String roleName = String.format("ROLE_%s", roles[i].toUpperCase());
            // does the role already exist?
            Role role = roleRepository.findRoleByName(roleName);
            // if it doesn't exist, we create it
            if (role == null) {
                role = roleRepository.save(new Role(roleName));
            }
            // does the user already exist?
            User user = userRepository.findUserByLogin(login);
            // if it doesn't exist, we create it
            if (user == null) {
                // hash the password with bcrypt
                String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
                // save user
                user = userRepository.save(new User(null, null, login, login, crypt));
                // we create the relationship with the role
                userRoleRepository.save(new UserRole(user, role));
            } else {
                // the user already exists - does he/she have the required role?
                boolean trouvé = false;
                for (Role r : userRepository.getRoles(user.getId())) {
                    if (r.getName().equals(roleName)) {
                        trouvé = true;
                        break;
                    }
                }
                // if not found, we create the relationship with the role
                if (!trouvé) {
                    userRoleRepository.save(new UserRole(user, role));
                }
            }
        }
        // closing Spring context
        context.close();
        // end
        System.out.println("Travail terminé...");
    }
 
}
  • linhas 22–24: definem o nome de utilizador, a palavra-passe e a função para três utilizadores;
  • linha 27: o contexto Spring é construído a partir da classe de configuração [AppConfig];
  • linhas 28–30: recuperam as referências aos três objetos [Repository] que podem ser usados para criar um utilizador;
  • linha 31: cria os três utilizadores;
  • linhas 33–35: informações para criar o utilizador #i;
  • linha 37: verificamos se a função já existe;
  • linhas 39–41: caso contrário, criamo-la na base de dados. Terá um nome no formato [ROLE_XX];
  • linha 43: verificamos se o login já existe;
  • linhas 45–52: se o nome de utilizador não existir, criá-lo na base de dados;
  • linha 47: encriptamos a palavra-passe. Aqui, usamos a classe [BCrypt] do Spring Security (linha 8). Por isso, precisamos dos arquivos para esta estrutura. O ficheiro [pom.xml] inclui esta dependência:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • linha 49: o utilizador é guardado na base de dados;
  • linha 51: assim como a relação que o liga à sua função;
  • linhas 55–60: se o login já existir, verificamos se a função que pretendemos atribuir-lhe já se encontra entre as suas funções;
  • linhas 62–64: se a função procurada não for encontrada, é criada uma linha na tabela [USERS_ROLES] para ligar o utilizador à sua função;
  • Não nos protegemos contra possíveis exceções. Esta é uma classe auxiliar para criar utilizadores rapidamente;

Para executar o projeto, execute a configuração de execução denominada [spring-security-create-users-hibernate-eclipselink]:

 

Acabámos de criar dois serviços web seguros:

  • um com uma arquitetura [segurança / web / JDBC / MySQL];
  • o outro com uma arquitetura [segurança / web / Hibernate / MySQL];

Vamos agora analisar duas outras arquiteturas:

  • uma arquitetura [segurança / web / EclipseLink / SQL Server 2014 Express];
  • uma arquitetura [segurança / web / OpenJpa / Oracle Express];

  • Em [1], carregamos os projetos configurando uma camada [JDBC / SQL Server] e uma camada [JPA / EclipseLink / SQL Server];

Nota: Prima Alt-F5 e, em seguida, regenere todos os projetos Maven.

Partimos do princípio de que o SGBD SQL Server está em execução e que a base de dados [dbproduitscategories] foi gerada. Primeiro, precisamos de preencher as tabelas [USERS, ROLES, USERS_ROLES] nesta base de dados. Para tal, execute a configuração de execução denominada [spring-security-create-users-hibernate-eclipselink]:

Isto deverá preencher as três tabelas com dados:

 
 
  • Inicie o serviço web seguro utilizando a configuração denominada [spring-security-server-jpa-generic-hibernate-eclipselink][1];
  • execute o teste JUnitTestDao com a configuração denominada [spring-security-client-generic-JUnitTestDao][2]. Deve ser aprovado [3];

20.5.5. Arquitetura [segurança / web / OpenJpa / Oracle Express]

  • Em [1], carregue os projetos que configuram uma camada [JDBC / Oracle Express] e uma camada [JPA / OpenJpa / Oracle Express];

Nota: Prima Alt-F5 e, em seguida, regenere todos os projetos Maven.

Partimos do princípio de que o SGBD Oracle Express está em execução e que a base de dados [dbproduitscategories] foi gerada. Primeiro, precisamos de preencher as tabelas [USERS, ROLES, USERS_ROLES] nesta base de dados. Para tal, execute a configuração de execução denominada [spring-security-create-users-openjpa]:

Isto deverá preencher as três tabelas com dados:

 
 
  • Inicie o serviço web seguro utilizando a configuração denominada [spring-security-server-jpa-generic-openjpa][1-2];
  • execute o teste JUnitTestDao com a configuração denominada [spring-security-client-generic-JUnitTestDao][3]. Deve ser aprovado [4];