Skip to content

20. Proteção do serviço web de acesso à base de dados [dbproduitscategories]

20.1. Configuração do ambiente de trabalho

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

  
  • os projetos [spring-security-*] encontram-se na pasta [<exemples>\spring-database-generic\spring-security];
  • a segurança será implementada para os projetos SGBD e MySQL com uma camada [DAO / JDBC] e, em seguida, uma camada [DAO / JPA / Hibernate];
  • prima Alt-F5 e, em seguida, regenera 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] a partir da tabela [dbproduitscategories]:

 

Os identificadores criados [login/passwd] são os seguintes: [admin/admin], [user/user], [guest/guest]. Por predefinição, as palavras-passe estão encriptadas.

Image

Feito isto, execute a configuração de execução 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 execução denominada [spring-security-client-generic-JUnitTestDao], que testa o serviço web seguro:

O teste deve ser bem-sucedido.

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 [PRODUITS] e [CATEGORIES] da base de dados [dbproduitscategories]. Já foi criada;
  • a camada [DAO2] é a camada [DAO] que gere as tabelas [USERS], [ROLES] e [USERS_ROLES] da base de dados [dbproduitscategories]. Ainda está por criar;

  

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>
        <!-- servidor web / 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: retoma-se o que já existe com o arquivo do serviço web / json / jdbc analisado;
  • linhas 24-27: a dependência que inclui as classes do Spring Security;

No final, o projeto apresenta 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 [PRODUITS] e [CATEGORIES] da base de dados [dbproduitscategories]. Já foi criada;
  • a camada [DAO2] é a camada [DAO] que gere as tabelas [USERS], [ROLES] e [USERS_ROLES] da base de dados [dbproduitscategories]. É esta que vamos criar agora;
  

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

 

Esta interface é aqui implementada 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;
    // propriedades
    private User user;
    private String simpleClassName = getClass().getSimpleName();

    // construtores
    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étodos privados----------------
    private List<Role> getRoles(Long id) {
        try {
            // procura-se o utilizador através do seu 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 permite obter os detalhes desse utilizador (linha 20);
  • linha 27: o acesso à base de dados será feito através de JDBC, por meio do objeto [NamedParameterJdbcTemplate namedParameterJdbcTemplate] definido no projeto [spring-jdbc-generic-04]. Note-se que este objeto não é injetado pelo Spring, como costumava acontecer. É fornecido ao construtor nas linhas 36-39. Porquê? Porque a classe [AppUserDetails] não é um componente do Spring (ausência da anotação @Component) e, por isso, não é possível efetuar injeções nela;
  • 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]. Este método deve construir uma coleção de elementos do tipo [GrantedAuthority] ou derivado. Aqui, utilizamos o tipo derivado [SimpleGrantedAuthority] (linha 46), que encapsula o nome de uma das funções do utilizador da linha 29;
  • linhas 45-47: percorre-se 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, utiliza-se o método privado [getRoles] da linha 53;
  • linha 56: executa a ordem SQL [ConfigJdbc.SELECT_ROLES_BYUSERID] (definida em [Configjdbc]) da seguinte forma:

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 obter as funções de um utilizador identificado pela sua chave primária. É configurada pela chave primária [:id] do utilizador cujas funções se pretendem obter.

  • linha 56: cada linha de resultado da consulta [SELECT] é transformada 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;
    // propriedades
    private User user;
    private String simpleClassName = getClass().getSimpleName();

    // construtores
    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 e setters
    ...
}
  • linhas 35-37: implementam o método [getPassword] da interface [UserDetails]. Retorna-se a palavra-passe do utilizador da linha 12;
  • linhas 39-42: implementam o método [getUserName] da interface [UserDetails]. Retorna-se o nome de 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 exige 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 {

    // injeções
    @Autowired
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    // local
    private String simpleClassName = getClass().getSimpleName();

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        List<User> users;
        try {
            // procura-se o utilizador através do seu nome de utilizador
            users = namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_USER_BYLOGIN,
                    Collections.singletonMap("login", login), new ShortUserMapper());
        } catch (Exception e) {
            throw new DaoException(145, e, simpleClassName);
        }
        // Encontrado?
        if (users.size() == 0) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // apresentam-se os detalhes do utilizador
        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 através de JDBC, utilizando o objeto [NamedParameterJdbcTemplate 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;
  • linhas 36-37: o utilizador é pesquisado através do seu nome de utilizador. A ordem 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 pelo SELECT é transformada na entidade [User] pela classe [ShortUserMapper] das linhas 52-58.

  • linhas 42-44: se não for encontrada, é lançada uma exceção;
  • linha 46: é criado e apresentado um objeto [AppUserDetails]. Este é, de facto, do tipo [UserDetails] (linha 32). São passadas duas informações 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 que implementa a camada [web]. Esta não precisa de ser alterada.

20.2.4. A 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 parágrafo 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");
    }
}

Vamos seguir 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 aos diferentes URL 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;

    // segurança
    private boolean activateSecurity = true;

    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // a autenticação é feita pelo bean [appUserDetailsService]
        // a palavra-passe é encriptada pelo algoritmo de hash BCrypt
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // aplicação segura?
        if (activateSecurity) {
            // a palavra-passe é transmitida através do cabeçalho Authorization: Basic xxxx
            http.httpBasic();
            // o método HTTP OPTIONS deve ser autorizado para todos
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // apenas a função ADMIN pode utilizar a aplicação
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // todas as URL
                    .hasRole("ADMIN");
            // com sessão ou sem sessão?
            //http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • linha 17: a classe é uma classe de configuração do Spring;
  • linha 18: ativa os elementos do Spring Security;
  • linha 26: recuperam-se os componentes Spring da camada [DAO2] e os do pacote [spring.security.service], sobre os quais falaremos mais adiante;
  • linha 23: importam-se os beans do projeto [spring-webjson-server-jdbc-generic], que implementa a camada [web]. Entre esses beans, encontram-se também os da camada [DAO1];
  • linhas 22-23: é injetada a classe [AppUserDetails], que dá acesso aos utilizadores da aplicação;
  • linha 26: um valor booleano que protege (true) ou não (false) a aplicação web;
  • linhas 28-33: o método [configure(HttpSecurity http)] define os utilizadores e as suas funções. Recebe como parâmetro um tipo [AuthenticationManagerBuilder]. Este parâmetro é complementado com duas informações (linha 32):
    • uma referência ao serviço [appUserDetailsService] da linha 23, que dá acesso aos utilizadores registados. Note-se aqui que o facto de estarem registados numa base de dados não é indicado. 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 aos URL do serviço web;
  • linha 38: vimos no projeto de introdução que, por predefinição, o Spring Security gerava um token CSRF (Cross Site Request Forgery) que o utilizador que pretendesse autenticar-se tinha de reenviar ao servidor. Aqui, este mecanismo está desativado. Isto, juntamente com o valor booleano (isSecured=false), permite utilizar a aplicação web sem segurança;
  • linha 42: ativa-se o modo de autenticação por cabeçalho HTTP. O cliente deverá enviar o seguinte cabeçalho HTTP:
Authorization:Basic code

onde «code» é a codificação da cadeia «login:password» através do algoritmo Base64. Por exemplo, a codificação Base64 da cadeia admin:admin é YWRtaW46YWRtaW4=. Assim, o 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 todos os URL do serviço web estão acessíveis aos utilizadores com a função [ROLE_ADMIN]. Isto significa que um utilizador que não tenha essa 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 nos acessos seguintes. Este é o valor predefinido do Spring Security. A linha 51 desativa este modo. Se estiver ativa, o utilizador terá de se autenticar em cada acesso. Sem sessão, a capacidade de resposta do serviço web seguro é menor do que com sessão, pelo que a linha 51 foi colocada em comentário;

20.2.5. Testes do serviço web seguro

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

Authorization:Basic code

onde [code] é o código Base64 da cadeia [login:password]. Para gerar este código, pode utilizar-se 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) {
        // são esperados dois argumentos: nome de utilizador e palavra-passe
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // recuperam-se os dois argumentos
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // codifica-se a cadeia
        byte[] data = Base64.encode(chaîne.getBytes());
        // exibe a sua codificação Base64
        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 os testes:

  • o SGBD MySQL deve ser executado;
  • preenchemos as tabelas [PRODUITS] e [CATEGORIES] com a configuração de execução denominada [spring-jdbc-generic-04-fillDataBase]:
 
  • se ainda não tiver sido feito, preenchemos as tabelas [USERS, ROLES, USERS_ROLES] com a configuração de execução denominada [spring-security-create-users-hibernate-eclipselink]:
 
  • Iniciamos o serviço web seguro com a configuração de execução denominada [spring-security-server-jdbc-generic]:
 

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

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

A resposta do servidor é a seguinte:

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

Consegue-se, de facto, a lista de categorias:

 

Vamos agora 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;

Obtemos a seguinte resposta:

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

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

  

obtemos o seguinte resultado:

dXNlcjp1c2Vy
  • em [1]: o cabeçalho de autenticação HTTP está incorreto;
  • em [2]: a resposta do serviço web. É diferente da anterior, que era [401 Unauthorized]. Desta vez, o utilizador autenticou-se corretamente, mas não possui direitos 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 permitirá saber se um utilizador está autorizado ou não 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 os URL. A anotação [@RestController] indica que os métodos que processam estes URL enviam eles próprios a sua resposta ao cliente;
  • linha 11: apresenta o URL [/authenticate];
  • linhas 12-14: o método limita-se a devolver um objeto [Response] vazio, mas com um [status] igual a 0, indicando que não ocorreu qualquer erro;

Para que serve este URL? Quando quisermos simplesmente autenticar um utilizador, iremos solicitá-lo. Vimos que, se a camada de segurança não aceitar esse utilizador, devolve uma exceção. Eis um exemplo;

Com o utilizador [admin:admin]:

Recebemos uma resposta vazia, mas não há exceção.

Com o utilizador [user:user]:

Ocorreu uma exceção.

20.2.7. Conclusão

A adição das classes necessárias ao Spring Security foi possível sem alterações no projeto web/json original. Este cenário muito favorável deve-se ao facto de as três tabelas adicionadas à base de dados serem independentes das tabelas existentes. Teríamos até podido colocá-las numa base de dados separada. Noutros casos, as tabelas adicionadas podem ter relações com as tabelas existentes. O código da camada [DAO] existente deve, então, ser revisto.

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

Já criámos 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 [Client HTTP]

 

A classe [Client] assegura a comunicação HTTP com o servidor web / jSON seguro. 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 os identificadores de um utilizador:

package spring.security.client.entities;

public class Credentials {

    // propriedades
    private String login;
    private String password;

    // construtor
    public Credentials() {
    }

    public Credentials(String login, String password) {
        this.login = login;
        this.password = password;
    }

    // getters e setters
...
}

A classe [Client], que implementa a interface [IClient], sofre as seguintes alterações:


package spring.security.client.dao;

...

@Component
public class Client implements IClient {

    // injeções
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // local
    private String simpleClassName = getClass().getSimpleName();

    private String getBase64(Credentials credentials) {
        // o utilizador e a sua palavra-passe são codificados em base 64 - requer 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())));
    }

    // pedido genérico
    @Override
    public <T1, T2> T1 getResponse(Credentials credentials, String url, HttpMethod method, int errStatus, T2 body) {
        // a resposta do servidor
        ResponseEntity<Response<T1>> response;
        try {
            // prepara-se a solicitação
            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);
            }
            // executa-se a solicitação
            response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
            });
        } catch (Exception e) {
            // encapsulando a exceção
            throw new DaoException(errStatus, e, simpleClassName);
        }
...
    }
...
}
  • linhas 33-35, 40-42: se o utilizador [credentials] não for nulo, então é adicionado o cabeçalho de autenticação. A encriptação Base64 do utilizador e da sua palavra-passe é assegurada pelo método [getBase64] das linhas 17-21. É importante ter em conta que este método utiliza uma classe [Base64] pertencente ao JDK 1.8. O nosso cliente HTTP pode funcionar com um serviço web não seguro. Basta passar-lhe um [credentials] igual a null;
  • fora 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] do projeto [spring-webjson-client-generic] recebem um parâmetro adicional [Credentials credentials]:


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 {

    // lista de todas as entidades T
    public List<T> getAllShortEntities(Credentials credentials);

    public List<T> getAllLongEntities(Credentials credentials);

    // de entidades específicas - versão curta
    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);

    // de entidades específicas - versão longa
    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);

    // atualização de várias entidades
    public List<T> saveEntities(Credentials credentials, Iterable<T> entities);

    public List<T> saveEntities(Credentials credentials, @SuppressWarnings("unchecked") T... entities);

    // eliminação de todas as entidades
    public void deleteAllEntities(Credentials credentials);

    // eliminação de várias entidades
    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 {
    // autenticação
    public void authenticate(Credentials credentials);
}

A interface [IAuthenticate] possui apenas o 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 os URL das categorias, e da classe [DaoProduit], que gere os URL dos produtos. Todos os métodos da classe [AbstractDao] do projeto [spring-webjson-client-generic] recebem um parâmetro adicional [Credentials credentials], que passam para a classe filha. Eis um exemplo:


    @Override
    public List<T1> getShortEntitiesById(Credentials credentials, Iterable<Long> ids) {
        // validade do argumento
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // resultado
        return getShortEntitiesById(credentials, Lists.newArrayList(ids));
}
  • o método [getShortEntitiesById] recebe o parâmetro [Credentials credentials] (linha 2), que transmite (linha 9) ao 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;

    // verificação [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: é emitido um comando HTTP GET para o URL [/authenticate]. A utilização deste URL foi demonstrada no parágrafo 20.2.6. O seu princípio consiste em que a chamada termina com uma exceção se o utilizador [credentials] for desconhecido ou não tiver direitos 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]. Por conseguinte, ocorrerá uma exceção se o utilizador [Credentials credentials] não for aceite pelo serviço web seguro;

20.3.2.3. As turmas [DaoCategorie, DaoProduit]

  

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


@Component
public class DaoCategorie extends AbstractDao<Categorie> {

    // injeções
    @Autowired
    protected ApplicationContext context;
    @Autowired
    protected IClient client;

    @Override
    public List<Categorie> getAllShortEntities(Credentials credentials) {
        try {
            // filtros jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // obter todas as categorias
            Object map = client.<List<Categorie>, Void> getResponse(credentials, "/getAllShortCategories", HttpMethod.GET,
                    202, null);
            // a lista de categorias List<Categoria>
            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 à que existia no projeto [spring-webjson-client-generic], com uma única diferença:


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

20.3.4. Testes da camada [DAO]

  

20.3.4.1. O teste [JUnitTestCredentials]

O teste [JUnitTestCredentials] utiliza o método [IDao.authenticate] para verificar a validade ou não 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 {

    // camada [DAO]
    @Autowired
    private IAuthenticate authenticate;

    // utilizadores
    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 a 34, são criados três utilizadores:
    • o utilizador [admin] tem acesso aos URL do serviço web. É testado nas linhas 63-72;
    • o utilizador [user] existe, mas não está autorizado a utilizar os URL do serviço web. É testado nas linhas 37-47;
    • o utilizador [unknown] não existe. É testado nas linhas 50-60;

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

Em seguida, é iniciado o teste JUnit [JUnitTestCredentials] com a configuração 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 foi bem-sucedido:

  

20.3.4.2. O teste [JUnitTestDao]

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


@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao {

    // contexto Spring
    @Autowired
    private ApplicationContext context;
    // camada [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;

....

    // utilizadores
    static private Credentials admin;

    @BeforeClass
    public static void init() {
        admin = new Credentials("admin", "admin");
    }

    @Before
    public void clean() {
        // limpa-se a base de dados antes de cada teste
        log("Vidage de la base de données", 1);
        // esvazia-se a tabela [CATEGORIES] e, em cadeia, a tabela [PRODUITS]
        daoCategorie.deleteAllEntities(admin);
        // esvaziam-se os dicionários
        for (Long id : mapCategories.keySet()) {
            mapCategories.remove(id);
        }
        for (Long id : mapProduits.keySet()) {
            mapProduits.remove(id);
        }
    }

    private List<Categorie> fill(int nbCategories, int nbProduits) {
        // preenchem-se as tabelas
        ...
        // adição da categoria — em cadeia, os produtos também serão
        // inseridos — o resultado é apresentado em simultâneo
        return daoCategorie.saveEntities(admin, categories);
    }

    private Object[] showDataBase() throws BeansException, JsonProcessingException {
        // lista de categorias
        log("Liste des catégories", 2);
        List<Categorie> categories = daoCategorie.getAllShortEntities(admin);
        affiche(categories, context.getBean("jsonMapperShortCategorie", ObjectMapper.class));
        // lista de produtos
        log("Liste des produits", 2);
        List<Produit> produits = daoProduit.getAllShortEntities(admin);
        affiche(produits, context.getBean("jsonMapperShortProduit", ObjectMapper.class));
        // resultado
        return new Object[] { categories, produits };
    }
...

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

O teste será iniciado com a configuração de execução denominada [spring-security-client-generic-JUnitTestDao]:

 

O teste é bem-sucedido, mas verifica-se que é mais lento do que com o serviço web não seguro. A securização de uma aplicação aumenta significativamente os seus tempos de resposta. É possível observar um fator importante no desempenho do serviço web seguro: na classe [AppConfig] que o configura, escrevemos:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // aplicação segura?
        if (activateSecurity) {
            // a palavra-passe é transmitida através do cabeçalho Authorization: Basic xxxx
            http.httpBasic();
            // o método HTTP OPTIONS deve ser autorizado para todos
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // apenas a função ADMIN pode utilizar a aplicação
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // todas as URL
                    .hasRole("ADMIN");
            // sem sessão
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
}

A linha 17 tem um custo. Ela obriga ou não o utilizador a autenticar-se em cada acesso. Se a colocarmos em comentários, a duração do teste JUnit é significativamente menor, isto porque o utilizador [admin] só se autentica no primeiro teste e não nos seguintes (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 vai agora ser implementado pelo projeto [spring-security-server-jpa-generic], que se baseia no projeto [spring-jpa-generic], o qual gere os acessos à base de dados com o Spring Data JPA:

Acima:

  • a camada [DAO1] é a camada [DAO], que gere as tabelas [PRODUITS] e [CATEGORIES] da base de dados [dbproduitscategories]. Já foi criada;
  • a camada [DAO2] é a camada [DAO] que gere as tabelas [USERS], [ROLES] e [USERS_ROLES] da base de dados [dbproduitscategories]. Ainda está por criar;

O projeto [spring-security-server-jpa-generic] é obtido, em primeiro lugar, por cópia do projeto analisado anteriormente, [spring-security-server-jdbc-generic]. Com efeito, as camadas [web] e [security] não se alteram porque:

  • a camada [DAO1 / Repositories / JPA] (já escrita) tem a mesma interface que a camada [DAO1 / JDBC];
  • a camada [DAO2 / Repositories / JPA] (a gravar) 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>
        <!-- servidor web / 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 da camada [security] do projeto;
  • linhas 29-33: a dependência da camada [web] do projeto. O projeto [spring-webjson-server-jpa-generic] implementa na íntegra a camada [web]. Esta não precisa de ser escrita nem alterada;

Em suma, 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-lhe 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: declara-se 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, será encontrada 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] é a imagem da tabela [USERS]:

Image

  • ID: chave primária;
  • VERSION: coluna de controlo de versões da linha;
  • IDENTITY: uma identificação descritiva do utilizador;
  • LOGIN: o nome de utilizador do utilizador;
  • PASSWORD: a sua palavra-passe;

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 {
    // propriedades
    @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;

    // propriedades
    @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;

    // os UserRole associados
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = { CascadeType.ALL })
    @JsonIgnore
    private List<UserRole> userRoles;

    // fabricantes
    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;
    }

    // ------------------------------------------------------------
    // redefinição de [equals] e [hashcode]
...

    // getters e setters
...
}
  • linha 23: a classe implementa a interface [AbstractCoreEntity] já utilizada para as outras entidades;
  • linhas 34-35: o tipo da entidade. Esta propriedade não é guardada na base de dados [@Transient];
  • linhas 38-43: as três propriedades básicas de um utilizador (nome, nome de utilizador, palavra-passe);
  • linhas 46-48: a lista de funções do utilizador. Este pode ter várias. Da mesma forma, veremos que a uma função podem estar associados vários utilizadores. Temos, portanto, no sentido JPA do termo, uma relação [ManyToMany] entre as entidades [User] e [Role]:
    • um utilizador pode estar associado 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 junção [USERS_ROLES]. Se um utilizador U tiver uma relação com uma função R, essa relação é registada na tabela [USERS_ROLES], através do registo do par de chaves primárias das entidades (U,R). Na tabela 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];
  • ligação 48: o facto de um utilizador ter várias funções é traduzido por uma relação [OneToMany] para a entidade [UserRole];

A classe [Role] é a representação da tabela [ROLES]:

Image

  • ID: chave primária;
  • VERSION: coluna de controlo de versões da linha;
  • 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 {
    // propriedades
    @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;

    // propriedades
    @Column(name = ConfigJdbc.TAB_ROLES_NAME, length = 30, unique = true, nullable = false)
    private String name;

    // os UserRole associados
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "role", cascade = { CascadeType.ALL })
    @JsonIgnore
    private List<UserRole> userRoles;

    // construtores
    public Role() {
    }

    public Role(Long id, Long version, String name) {
        this.id = id;
        this.version = version;
        this.name = name;
    }

    // getters e setters
    public Role(String name) {
        this.name = name;
    }

    // ------------------------------------------------------------
    // redefinição de [equals] e [hashcode]
...
    // getters e setters
...
}
  • linhas 42-44: o facto de uma função poder estar associada a vários utilizadores traduz-se numa relação [@OneToMany] com a entidade [UserRole];

A classe [UserRole] é a representação da tabela [USERS_ROLES]:

Image

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

  • ID: chave primária;
  • VERSION: coluna de controlo de versões da linha;
  • USER_ID: identificador de um 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 {
    // propriedades
    @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;

    // um UserRole faz referência a um utilizador
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_USERS_ROLES_USER_ID, nullable = false)
    private User user;

    // um UserRole faz referência a um Role
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_USERS_ROLES_ROLE_ID, nullable = false)
    private Role role;

    // construtores
    public UserRole() {

    }

    public UserRole(User user, Role role) {
        this.user = user;
        this.role = role;
    }

    // ------------------------------------------------------------
    // redefinição de [equals] e [hashcode]
    ...

    // getters e setters
...
}
  • linhas 34-36: representam a chave estrangeira da tabela [USERS_ROLES] para a tabela [USERS];
  • linhas 38-41: representam 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> {

    // lista de funções de um utilizador identificado pelo seu ID
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);

    // lista de funções de um utilizador identificado pelo seu nome de utilizador único
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    // pesquisa de um utilizador através do seu nome de utilizador
    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)] permite obter todas as funções de um utilizador identificado pelo seu [id]
  • linhas 16-17: o mesmo, mas para um utilizador identificado pelo seu nome de utilizador e palavra-passe;
  • linha 20: para encontrar um utilizador através do seu nome de utilizador;

A interface [RoleRepository] gere os acessos à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> {

    // pesquisa de uma função através do seu nome
    Role findRoleByName(String name);

}
  • linha 7: a interface [RoleRepository] estende a interface [CrudRepository];
  • linha 10: é possível pesquisar uma função pelo seu nome. Recorde-se aqui que a entidade [Role] possui um campo [name]. O método [findEntityByChamp] é implementado automaticamente pelo Spring Data. Por isso, não é necessário implementar aqui o método [finRoleByName]. 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] limita-se a estender a interface [CrudRepository] sem lhe adicionar novos métodos;

20.4.5. A camada [DAO2]

  

Na camada [DAO2], encontram-se as mesmas classes que na camada [DAO2] do projeto [spring-security-server-jdbc-generic] analisado anteriormente no parágrafo 20.2.2. Basta agora implementá-las com a ajuda das 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;

    // propriedades
    private User user;
    private UserRepository userRepository;
    
    // local
    private String simpleClassName = getClass().getSimpleName();

    // construtores
    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 como segundo parâmetro o objeto [UserRepository], que permite à classe obter as funções de um determinado utilizador (linha 42);

O componente Spring [AppUserDetailsService], por sua vez, 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 {
        // procura-se o utilizador através do seu nome de utilizador
        User user;
        try {
            user = userRepository.findUserByLogin(login);
        } catch (Exception e) {
            throw new DaoException(168, e, simpleClassName);
        }
        // encontrado?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // apresentam-se os detalhes do utilizador
        return new AppUserDetails(user, userRepository);
    }

}
  • linha 18: injeção do componente Spring [userRepository], que permitirá ao serviço identificar o utilizador através do seu nome de utilizador, linha 27;

No final, percebe-se que só é necessário o [userRepository] e não os outros dois repositórios [roleRepository, userRoleRepository]. Estes serão utilizados no projeto seguinte, que visa preencher as tabelas [USERS, ROLES, USERS_ROLES].

20.4.6. Os testes

O serviço web seguro é iniciado com a configuração denominada [spring-security-server-jpa-generic-hibernate-eclipselink] [1]. O teste [JUnitTestDao] do 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] a partir da tabela [dbproduitscategories]:

Image

 

Os identificadores criados [login/passwd] são os seguintes: [admin/admin], [user/user], [guest/guest]. Por predefinição, as palavras-passe estã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 do framework Spring Security. O algoritmo de encriptação de palavras-passe é fornecido por este framework
  • 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 [console]

Uma vez que as camadas [repositories] 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 de suporte para gerar o código Base64 de um par [login, password]. Já a utilizámos. Não é útil para este projeto;

A classe de configuração 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: indica-se onde encontrar os [repositories] da aplicação. Estes encontram-se no pacote [spring.security.repositories] da dependência [spring-security-server-jpa-generic]
  • linha 11: importam-se 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) {

        // fim
        System.out.println("Travail en cours...");

        // criam-se três utilizadores
        String[] logins = { "admin", "user", "guest" };
        String[] passwds = { "admin", "user", "guest" };
        String[] roles = { "admin", "user", "guest" };

        // Contexto Spring
        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++) {
            // recuperar as informações do utilizador n.º i
            String login = logins[i];
            String password = passwds[i];
            String roleName = String.format("ROLE_%s", roles[i].toUpperCase());
            // a função já existe?
            Role role = roleRepository.findRoleByName(roleName);
            // se não existir, cria-se
            if (role == null) {
                role = roleRepository.save(new Role(roleName));
            }
            // O utilizador já existe?
            User user = userRepository.findUserByLogin(login);
            // se não existir, cria-se
            if (user == null) {
                // a palavra-passe é hashada com o bcrypt
                String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
                // guardamos o utilizador
                user = userRepository.save(new User(null, null, login, login, crypt));
                // criamos a relação com a função
                userRoleRepository.save(new UserRole(user, role));
            } else {
                // o utilizador já existe — tem a função solicitada?
                boolean trouvé = false;
                for (Role r : userRepository.getRoles(user.getId())) {
                    if (r.getName().equals(roleName)) {
                        trouvé = true;
                        break;
                    }
                }
                // se não for encontrado, cria-se a relação com a função
                if (!trouvé) {
                    userRoleRepository.save(new UserRole(user, role));
                }
            }
        }
        // encerramento do contexto Spring
        context.close();
        // fim
        System.out.println("Travail terminé...");
    }

}
  • linhas 22-24: definem o nome de utilizador, a palavra-passe e a função de três utilizadores;
  • linha 27: o contexto Spring é construído a partir da classe de configuração [AppConfig];
  • linhas 28-30: recuperam-se as referências das três instâncias de [Repository] que podem ser úteis para criar um utilizador;
  • linha 31: criam-se os três utilizadores;
  • linhas 33-35: as informações para criar o utilizador n.º i;
  • linha 37: verifica-se se a função já existe;
  • linhas 39-41: se não for o caso, cria-se na base de dados. Terá um nome do tipo [ROLE_XX];
  • linha 43: verifica-se se o nome de utilizador já existe;
  • linhas 45-52: se o nome de utilizador não existir, cria-se na base de dados;
  • linha 47: encripta-se a palavra-passe. Aqui, utiliza-se a classe [BCrypt] do Spring Security (linha 8). Por isso, são necessários os ficheiros deste framework. 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: caso em que o login já exista – verifica-se então se, entre as suas funções, já se encontra a função que se pretende atribuir-lhe;
  • linhas 62-64: se a função procurada não for encontrada, cria-se uma linha na tabela [USERS_ROLES] para associar o utilizador à sua função;
  • não nos protegemos contra eventuais exceções. Trata-se de uma classe de suporte para criar utilizadores rapidamente;

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

 

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

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

Vamos agora abordar duas outras arquiteturas:

  • uma arquitetura [security / web / EclipseLink / SQL Server 2014 Express];
  • uma arquitetura [security / web / OpenJpa / Oracle Express];

  • na [1], carregam-se os projetos que configuram uma camada [JDBC / SQL Server] e uma camada [JPA / EclipseLink / SQL Server];

Nota: prima Alt-F5 e, em seguida, regenera todos os projetos Maven.

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

Esta configuração deve preencher as três tabelas com dados:

 
 
  • inicie o serviço web seguro com 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 bem-sucedido [3];

20.5.5. Arquitetura [security / web / OpenJpa / Oracle Express]

  • no [1], carregam-se os projetos que configuram uma camada [JDBC / Oracle Express] e uma camada [JPA / OpenJpa / Oracle Express];

Nota: prima Alt-F5 e, em seguida, regenera todos os projetos Maven.

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

Esta configuração deve preencher as três tabelas com dados:

 
 
  • inicie o serviço web seguro com 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 bem-sucedido [4];