Skip to content

16. [Curso]: Proteção do acesso a um serviço web com o Spring Security

Palavras-chave: arquitetura multicamadas, Spring, injeção de dependências, serviço web seguro / JSON, cliente / servidor

16.1. Suporte

 

Os projetos para este capítulo podem ser encontrados na pasta [support / chap-16]. O script SQL é utilizado para gerar a base de dados necessária para os testes.

16.2. O papel do Spring Security numa aplicação Web

Vamos situar o Spring Security no contexto do desenvolvimento de uma aplicação web. Na maioria das vezes, esta será construída com base numa arquitetura multicamadas, como a seguinte:

  • A camada [Spring Security] concede acesso à camada [web] apenas a utilizadores autorizados.

16.3. Um tutorial sobre o Spring Security

Vamos importar novamente um guia do Spring, seguindo os passos 1 a 3 abaixo:

  

O projeto é composto pelos seguintes elementos:

  • na pasta [templates], encontrará as páginas HTML do projeto;
  • [Application]: é a classe executável do projeto;
  • [MvcConfig]: é a classe de configuração do Spring MVC;
  • [WebSecurityConfig]: é a classe de configuração do Spring Security;

16.3.1. Configuração do Maven

O Projeto [3] é um projeto Maven. Vamos examinar o seu ficheiro [pom.xml] para ver as suas dependências:


<?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>org.springframework</groupId>
    <artifactId>gs-securing-web</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- tag::security[] -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- end::security[] -->
    </dependencies>
 
    <properties>
        <start-class>hello.Application</start-class>
    </properties>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
</project>
  • linhas 10–14: o projeto é um projeto Spring Boot;
  • linhas 17–20: dependência da estrutura [Thymeleaf];
  • linhas 22–25: dependência da estrutura Spring Security;

16.3.2. visualizações Thymeleaf

  

A visualização [home.html] é a seguinte:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <h1>Welcome!</h1>
 
    <p>
        Click <a th:href="@{/hello}">here</a> to see a greeting.
    </p>
</body>
</html>
  • linha 12: o atributo [th:href="@{/hello}"] irá gerar o atributo [href] da tag [<a>]. O valor [@{/hello}] irá gerar o caminho [<context>/hello], onde [context] é o contexto da aplicação web;

O código HTML gerado é o seguinte:


<!DOCTYPE html>
 
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example</title>
    </head>
    <body>
        <h1>Welcome!</h1>
 
        <p>
            Click
            <a href="/hello">here</a>
            to see a greeting.
        </p>
    </body>
</html>

A visualização [hello.html] é a seguinte:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
    </form>
</body>
</html>
  • Linha 9: O atributo [th:inline="text"] irá gerar o texto da tag [<h1>]. Este texto contém uma expressão $ que deve ser avaliada. O elemento [[${#httpServletRequest.remoteUser}]] é o valor do atributo [RemoteUser] do pedido HTTP atual. Este é o nome do utilizador que está a sessão;
  • linha 10: um formulário HTML. O atributo [th:action="@{/logout}"] irá gerar o atributo [action] da tag [form]. O valor [@{/logout}] irá gerar o caminho [<context>/logout], em que [context] é o contexto da aplicação web;

O código HTML gerado é o seguinte:


<!DOCTYPE html>
 
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1>Hello user!</h1>
        <form method="post" action="/logout">
            <input type="submit" value="Sign Out" />
            <input type="hidden" name="_csrf" value="b152e5b9-d1a4-4492-b89d-b733fe521c91" />
        </form>
    </body>
</html>
  • linha 8: a tradução de Olá [[${#httpServletRequest.remoteUser}]]!;
  • linha 9: a tradução de @{/logout};
  • linha 11: um campo oculto denominado (atributo name) _csrf;

A vista [login.html] é a seguinte:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <div th:if="${param.error}">Invalid username and password.</div>
    <div th:if="${param.logout}">You have been logged out.</div>
    <form th:action="@{/login}" method="post">
        <div>
            <label> User Name : <input type="text" name="username" />
            </label>
        </div>
        <div>
            <label> Password: <input type="password" name="password" />
            </label>
        </div>
        <div>
            <input type="submit" value="Sign In" />
        </div>
    </form>
</body>
</html>
  • linha 9: o atributo [th:if="${param.error}"] garante que a tag <div> só será gerada se o URL que exibe a página de login contiver o parâmetro [error] (http://context/login?error);
  • linha 10: o atributo [th:if="${param.logout}"] garante que a tag <div> só será gerada se a URL que exibe a página de login contiver o parâmetro [logout] (http://context/login?logout);
  • linhas 11–23: um formulário HTML;
  • linha 11: o formulário será enviado para a URL [<context>/login], onde <context> é o contexto da aplicação web;
  • linha 13: um campo de entrada denominado [username];
  • linha 17: um campo de entrada denominado [password];

O código HTML gerado é o seguinte:


<!DOCTYPE html>
 
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example </title>
    </head>
    <body>
 
        <div>
            You have been logged out.
        </div>
        <form method="post" action="/login">
            <div>
                <label>
                    User Name :
                    <input type="text" name="username" />
                </label>
            </div>
            <div>
                <label>
                    Password:
                    <input type="password" name="password" />
                </label>
            </div>
            <div>
                <input type="submit" value="Sign In" />
            </div>
            <input type="hidden" name="_csrf" value="ef809b0a-88b4-4db9-bc53-342216b77632" />
        </form>
    </body>
</html>

Repare que, na linha 28, o Thymeleaf adicionou um campo oculto chamado [_csrf].

16.3.3. Configuração do Spring MVC

  

A classe [MvcConfig] configura a estrutura Spring MVC:


package hello;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
 
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }
 
}
  • linha 7: a anotação [@Configuration] torna a classe [MvcConfig] uma classe de configuração;
  • linha 8: a classe [MvcConfig] estende a classe [WebMvcConfigurerAdapter] para substituir determinados métodos;
  • linha 10: redefinição de um método da classe pai;
  • linhas 11–16: o método [addViewControllers] permite que URLs sejam associadas a visualizações HTML. As seguintes associações são feitas aqui:
URL
view
/, /home
/templates/home.html
/olá
/templates/hello.html
/login
/modelos/login.html

O sufixo [html] e a pasta [templates] são os valores predefinidos utilizados pelo Thymeleaf. Podem ser alterados através da configuração. A pasta [templates] deve estar na raiz do classpath do projeto:

No [1] acima, as pastas [java] e [resources] são ambas pastas de origem. Isto significa que o seu conteúdo estará na raiz do classpath do projeto. Portanto, no [2], as pastas [hello] e [templates] estarão na raiz do classpath.

16.3.4. Configuração do Spring Security

  

A classe [WebSecurityConfig] configura a estrutura Spring Security:


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");
    }
}
  • linha 9: a anotação [@Configuration] torna a classe [WebSecurityConfig] uma classe de configuração;
  • linha 10: a anotação [@EnableWebSecurity] torna a classe [WebSecurityConfig] uma classe de configuração do Spring Security;
  • linha 11: a classe [WebSecurity] estende a classe [WebSecurityConfigurerAdapter] para substituir determinados métodos;
  • linha 12: redefinição de um método da classe pai;
  • linhas 13–16: o método [configure(HttpSecurity http)] é substituído para definir direitos de acesso para os vários URLs da aplicação;
  • linha 14: o método [http.authorizeRequests()] permite que as URLs sejam associadas a direitos de acesso. São feitas as seguintes associações:
URL
regra
código
/, /home
acesso sem autenticação

http.authorizeRequests().antMatchers("/", "/home").permitAll()
outras URLs
apenas acesso autenticado
http.anyRequest().authenticated();
  • linha 15: define o método de autenticação. A autenticação é realizada através de um formulário URL [/login] acessível a todos [http.formLogin().loginPage("/login").permitAll()]. O logout também é acessível a todos;
  • linhas 19–21: redefinem o método [configure(AuthenticationManagerBuilder auth)] que gere os utilizadores;
  • linha 20: a autenticação é realizada utilizando utilizadores codificados [auth.inMemoryAuthentication()]. Um utilizador é definido aqui com o nome de utilizador [user], palavra-passe [password] e função [USER]. Aos utilizadores com a mesma função podem ser atribuídas as mesmas permissões;

16.3.5. Classe executável

  

A classe [Application] é a seguinte:


package hello;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
 
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {
 
    public static void main(String[] args) throws Throwable {
        SpringApplication.run(Application.class, args);
    }
 
}
  • Linha 8: A anotação [@EnableAutoConfiguration] instrui o Spring Boot (linha 3) a realizar a configuração que o programador não definiu explicitamente;
  • linha 9: torna a classe [Application] uma classe de configuração do Spring;
  • linha 10: instrui o sistema a analisar o diretório que contém a classe [Application] para procurar componentes Spring. As duas classes [MvcConfig] e [WebSecurityConfig] serão, assim, encontradas porque possuem a anotação [@Configuration];
  • linha 13: o método [main] da classe executável;
  • linha 14: o método estático [SpringApplication.run] é executado com a classe de configuração [Application] como parâmetro. Já nos deparámos com este processo e sabemos que o servidor Tomcat incorporado nas dependências Maven do projeto será iniciado e o projeto implementado nele. Vimos que quatro URLs eram geridas [/, /home, /login, /hello] e que algumas estavam protegidas por direitos de acesso.

16.3.6. Testar a aplicação

Vamos começar por solicitar a URL [/], que é uma das quatro URLs aceites. Está associada à vista [/templates/home.html]:

 

A URL solicitada [/] é acessível a todos. É por isso que conseguimos recuperá-la. O link [aqui] é o seguinte:

Click <a href="/hello">here</a> to see a greeting.

A URL [/hello] será solicitada quando clicarmos no link. Esta está protegida:

URL
regra
código
/, /home
acesso sem autenticação

http.authorizeRequests().antMatchers("/", "/home").permitAll()
outras URLs
apenas acesso autenticado
http.anyRequest().authenticated();

É necessário estar autenticado para aceder. O Spring Security redirecionará então o navegador do cliente para a página de autenticação. Com base na configuração apresentada, esta é a página na URL [/login]. Esta página é acessível a todos:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

Assim, obtemos isto [1]:

O código-fonte da página obtida é o seguinte:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
...
    <form method="post" action="/login">
...
       <input type="hidden" name="_csrf" value="87bea06a-a177-459d-b279-c6068a7ad3eb" />
   </form>
</body>
</html>
  • na linha 7, aparece um campo oculto que não consta na página original [login.html]. Foi adicionado pelo Thymeleaf. Este código, conhecido como CSRF (Cross-Site Request Forgery), foi concebido para eliminar uma vulnerabilidade de segurança. Este token deve ser reenviado ao Spring Security juntamente com a autenticação para que seja aceite;

Recordamos que apenas o par utilizador/palavra-passe é reconhecido pelo Spring Security. Se introduzirmos outra coisa em [2], obtemos a mesma página com uma mensagem de erro em [3]. O Spring Security redirecionou o navegador para o URL [http://localhost:8080/login?error]. A presença do parâmetro [error] desencadeou a exibição da tag:


<div th:if="${param.error}">Invalid username and password.</div>

Agora, vamos introduzir os valores esperados de utilizador/palavra-passe [4]:

  • em [4], fazemos o login;
  • em [5], o Spring Security redireciona-nos para a URL [/hello], porque essa é a URL que solicitámos quando fomos redirecionados para a página de início de sessão. A identidade do utilizador foi apresentada pela seguinte linha de [hello.html]:

    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

A página [5] apresenta o seguinte formulário:


    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
</form>

Quando clica no botão [Sair], é enviada uma solicitação POST para a URL [/logout]. Tal como a URL [/login], esta URL é acessível a todos:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

No nosso mapeamento de URL/visualização, não definimos nada para a URL [/logout]. O que irá acontecer? Vamos experimentar:

  • Em [6], clicamos no botão [Sair];
  • em [7], vemos que fomos redirecionados para o URL [http://localhost:8080/login?logout]. O Spring Security solicitou este redirecionamento. A presença do parâmetro [logout] no URL fez com que a seguinte linha fosse exibida na visualização:

<div th:if="${param.logout}">You have been logged out.</div>

16.3.7. Conclusão

No exemplo anterior, poderíamos ter escrito a aplicação web primeiro e, posteriormente, ter-lhe aplicado medidas de segurança. O Spring Security é não intrusivo. É possível implementar segurança numa aplicação web que já tenha sido escrita. Além disso, descobrimos os seguintes pontos:

  • é possível definir uma página de autenticação;
  • a autenticação deve ser acompanhada pelo token CSRF emitido pelo Spring Security;
  • se a autenticação falhar, é redirecionado para a página de autenticação com um parâmetro de erro adicional no URL;
  • se a autenticação for bem-sucedida, é redirecionado para a página solicitada no momento da autenticação. Se solicitar a página de autenticação diretamente, sem passar por uma página intermédia, o Spring Security redireciona-o para a URL [/] (este caso não foi demonstrado);
  • O utilizador sai da sessão solicitando a URL [/logout] com um pedido POST. O Spring Security redireciona-o então para a página de autenticação com o parâmetro «logout» na URL;

Todas estas conclusões baseiam-se no comportamento padrão do Spring Security. Este comportamento pode ser alterado através da configuração, substituindo determinados métodos da classe [WebSecurityConfigurerAdapter].

O tutorial anterior será de pouca utilidade para nós daqui em diante. Na verdade, iremos utilizar:

  • uma base de dados para armazenar utilizadores, as suas palavras-passe e as suas funções;
  • autenticação baseada em cabeçalhos HTTP;

Existem muito poucos tutoriais disponíveis para o que pretendemos fazer aqui. A solução que iremos propor é uma combinação de trechos de código encontrados aqui e ali.

16.4. Implementação de segurança no serviço web do produto / JSON

16.4.1. A base de dados

A base de dados [dbintrospringdata] está a ser atualizada para incluir utilizadores, as suas palavras-passe e as suas funções. Estão a ser adicionadas três novas tabelas:

Image

Tabela [USERS]: utilizadores

  • 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 sua palavra-passe;

Na tabela USERS, as palavras-passe não são armazenadas em texto simples:

 

O algoritmo utilizado para encriptar as palavras-passe é o algoritmo BCRYPT.

Tabela [ROLES]: funções

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

Tabela [USERS_ROLES]: tabela de junção USERS/ROLES

Um utilizador pode ter várias funções, e uma função pode incluir vários utilizadores. Esta é 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 da função;
 

16.4.2. O projeto Eclipse

Criamos o seguinte projeto Eclipse:

1
  

  • em [1]: o novo projeto com os seguintes pacotes:
    • [spring.security.entities]: contém as entidades JPA correspondentes às três novas tabelas da base de dados;
    • [spring.security.repositories]: contém os repositórios Spring Data associados às três novas tabelas;
    • [spring.security.dao]: contém um serviço baseado nos [repositórios];
    • [spring.security.config]: contém a configuração do projeto, incluindo a configuração para acesso seguro ao serviço web;
    • [spring.security.boot]: contém a classe de inicialização para o serviço web seguro;

16.4.3. A configuração do Maven

O novo 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>istia.st.spring.security</groupId>
    <artifactId>intro-spring-security-server-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
 
    <name>intro-spring-security-server-01</name>
    <description>démo spring security</description>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>istia.st.webjson</groupId>
            <artifactId>intro-server-webjson-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- Spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- Spring logs -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
        </dependency>
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  • linhas 23–27: reutilizamos o código existente com o serviço web/arquivo JSON que analisámos;
  • linhas 29–32: a dependência que traz as classes do Spring Security;
  • linhas 34–37: a biblioteca de registo;
  • linhas 39–42: a biblioteca que permite a utilização das anotações do Spring Boot;
  • linhas 44–48: a biblioteca necessária para testes;

16.4.4. As novas entidades [JPA]

A camada JPA define três novas entidades:

  

A classe [User] representa a tabela [USERS]:


package spring.security.entities;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
 
import spring.data.entities.AbstractEntity;
 
@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {
 
    // properties
    @Column(name = "NAME")
    private String name;
    @Column(name = "LOGIN")
    private String login;
    @Column(name = "PASSWORD")
    private String password;
 
    // manufacturer
    public User() {
    }
 
    public User(String name, String login, String password) {
        this.name = name;
        this.login = login;
        this.password = password;
    }
 
    // getters and setters
...
}
  • linha 11: a classe estende a classe [AbstractEntity] já utilizada para as outras entidades;

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


package spring.security.entities;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
 
import spring.data.entities.AbstractEntity;
 
@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {
 
    // properties
    @Column(name="NAME")
    private String name;

    // manufacturers
    public Role() {
    }
 
    public Role(String name) {
        this.name = name;
    }
 
    // getters and setters
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
}

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


package spring.security.entities;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
 
import spring.data.entities.AbstractEntity;
 
@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {
 
    // foreign keys
    @Column(name = "USER_ID", insertable = false, updatable = false)
    private Long userId;
    @Column(name = "ROLE_ID", insertable = false, updatable = false)
    private Long roleId;
 
    // a UserRole refers to a User
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;
 
    // a UserRole refers to a Role
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;
 
    // manufacturers
    public UserRole() {
 
    }
 
    public UserRole(User user, Role role) {
        this.user = user;
        this.role = role;
    }
 
    // getters and setters
...
}
 
  • linhas 22–24: definem a chave estrangeira da tabela [USERS_ROLES] para a tabela [USERS];
  • linhas 27-29: definem a chave estrangeira da tabela [USERS_ROLES] para a tabela [ROLES];

16.4.5. Os [repositórios]

Cada uma das entidades JPA anteriores é gerida por um [repositório] Spring Data:

  

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


package spring.security.repositories;
 
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
 
import spring.security.entities.Role;
import spring.security.entities.User;
 
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 4);
  • linhas 12-13: o método [getRoles(User user)] 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 org.springframework.data.repository.CrudRepository;
 
import spring.security.entities.Role;
 
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: pode pesquisar uma função pelo seu nome;

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


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

16.4.6. Classes de gestão de utilizadores e funções

  

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 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.security.entities.Role;
import spring.security.entities.User;
import spring.security.repositories.UserRepository;
 
public class AppUserDetails implements UserDetails {
 
    private static final long serialVersionUID = 1L;
 
    // properties
    private User user;
    private UserRepository userRepository;
 
    // 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<>();
        for (Role role : userRepository.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
    ...
}
  • linha 14: a classe [AppUserDetails] implementa a interface [UserDetails];
  • linhas 19–20: a classe encapsula um utilizador (linha 19) e o repositório que fornece detalhes sobre esse utilizador (linha 20);
  • linhas 26–29: o construtor que instancia a classe com um utilizador e o seu repositório;
  • linhas 32–36: 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 36), que encapsula o nome de uma das funções do utilizador da linha 19;
  • linhas 35–37: percorremos a lista de funções do utilizador da linha 19 para construir uma lista de elementos do tipo [SimpleGrantedAuthority];
  • linhas 42–44: implementamos o método [getPassword] da interface [UserDetails]. Devolvemos a palavra-passe do utilizador da linha 19;
  • linhas 42–44: implementamos o método [getUserName] da interface [UserDetails]. Devolvemos o nome de utilizador do utilizador da linha 19;
  • linhas 51–54: a conta do utilizador nunca expira;
  • linhas 56–59: a conta do utilizador nunca é bloqueada;
  • linhas 61–64: as credenciais do utilizador nunca expiram;
  • linhas 66–69: 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 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.security.entities.User;
import spring.security.repositories.UserRepository;
 
@Service
public class AppUserDetailsService implements UserDetailsService {
 
    @Autowired
    private UserRepository userRepository;
 
    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // search for user via login
        User user = userRepository.findUserByLogin(login);
        // found?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // render user details
        return new AppUserDetails(user, userRepository);
    }
 
}
  • linha 12: a classe será um componente Spring, pelo que estará disponível no seu contexto;
  • linhas 15-16: o componente [UserRepository] será injetado aqui;
  • linhas 19–28: implementação do método [loadUserByUsername] da interface [UserDetailsService] (linha 10). O parâmetro é o nome de utilizador do utilizador;
  • linha 21: o utilizador é procurado através do seu nome de utilizador;
  • linhas 23–25: se o utilizador não for encontrado, é lançada uma exceção;
  • linha 27: um objeto [AppUserDetails] é construído e devolvido. É, de facto, do tipo [UserDetails] (linha 19);

16.4.7. Configuração do projeto

O projeto é configurado por duas classes:

A classe [DaoConfig] configura a camada [DAO] introduzida pelo novo projeto:


package spring.security.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
 
@EnableJpaRepositories(basePackages = { "spring.security.repositories" })
@ComponentScan(basePackages = { "spring.security.dao" })
@Import({ spring.data.config.DaoConfig.class })
public class DaoConfig {
 
    // constants
    final static private String[] ENTITIES_PACKAGES = { "spring.data.entities", "spring.security.entities" };
 
    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
    }
 
}
  • Linha 10: Importamos a classe de configuração [spring.data.config.DaoConfig] do projeto [intro-spring-data-01], que implementa a camada [DAO] para produtos e categorias;
  • linha 8: especificamos as pastas no projeto atual que contêm [repositórios] Spring Data;
  • linha 9: especificamos as pastas no projeto atual que contêm componentes Spring relacionados com a camada [DAO];
  • Linha 14: Especifica os diretórios que contêm entidades JPA. Estes incluem os do projeto [intro-spring-data-01] e os do projeto do servidor seguro. Esta informação é definida no bean nas linhas 16–19. Este bean substitui o bean com o mesmo nome no projeto [intro-spring-data-01]:

    final static private String[] ENTITIES_PACKAGES = { "spring.data.entities" };
 
    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(packagesToScan());
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }
 
    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
}

Na camada [DAO], a linha 8 analisa os diretórios especificados na linha 1. Devido à redefinição do bean nas linhas 14–17 no projeto secure (linhas 16–19), a linha 8 acima irá agora analisar os diretórios ["spring.data.entities", "spring.security.entities"]. Note que a classe importada na linha 10 a partir da classe [spring.security.config.DaoConfig] deve incluir a anotação [@Configuration]; caso contrário, o comportamento descrito acima não funcionará.

A classe [SecurityConfig] configura o aspeto de segurança do projeto. Já nos deparámos com uma classe de configuração do Spring Security:


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 [SecurityConfig] 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.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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 
import spring.security.dao.AppUserDetailsService;
 
@EnableWebSecurity
@ComponentScan(basePackages = { "spring.security.service" })
@Import({ spring.webjson.config.AppConfig.class, DaoConfig.class })
public class SecurityConfig 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");
            // no session
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • linha 16: para ativar os componentes do Spring Security;
  • linha 17: adicionamos os componentes Spring do pacote [spring.security.service];
  • linha 18: importamos os beans da camada [DAO] que acabámos de introduzir, bem como os do servidor web/JSON não seguro;
  • linhas 21–22: a classe [AppUserDetails], que fornece acesso aos utilizadores da aplicação, é injetada;
  • linha 25: um booleano que protege (true) ou não protege (false) a aplicação web;
  • linhas 27–32: o método [configure(HttpSecurity http)] define os utilizadores e as suas funções. Aceita um tipo [AuthenticationManagerBuilder] como parâmetro. Este parâmetro é enriquecido com duas informações (linha 38):
    • uma referência ao [appUserDetailsService] da linha 22, 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. Recorde-se que utilizámos o algoritmo BCrypt;
  • linhas 34–52: o método [configure(HttpSecurity http)] define os direitos de acesso às URLs do serviço web;
  • linha 37: vimos no projeto introdutório que, por predefinição, o Spring Security gere um token CSRF (Cross-Site Request Forgery) que o utilizador que tenta autenticar-se deve enviar de volta ao servidor. Aqui, este mecanismo está desativado. Combinado com o booleano (isSecured=false), isto permite que a aplicação web seja utilizada sem segurança;
  • linha 41: 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 46–48: especificam que todos os 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 50: No modo [session], um utilizador que se tenha autenticado uma vez não precisa de o fazer para acessos subsequentes. Aqui, desativamos este modo, pelo que o utilizador terá de se autenticar sempre que aceder ao serviço;

16.4.8. Teste da camada [DAO]

  

Primeiro, criamos uma classe executável [CreateUser] capaz de criar um utilizador com uma função:


package sprin.security.tests;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
 
import spring.security.config.DaoConfig;
import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.entities.UserRole;
import spring.security.repositories.RoleRepository;
import spring.security.repositories.UserRepository;
import spring.security.repositories.UserRoleRepository;
 
public class CreateUser {
 
    public static void main(String[] args) {
        // syntax: login password roleName
 
        // three parameters are required
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // parameters are retrieved
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // spring context
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // 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(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é...");
    }
 
}
  • linha 17: a classe espera três argumentos que definem um utilizador: o seu nome de utilizador, palavra-passe e função;
  • linhas 25–27: os três parâmetros são recuperados;
  • linha 29: o contexto Spring é construído a partir da classe de configuração [AppConfig];
  • linhas 30–32: as referências aos três objetos [Repository] que podem ser úteis para criar o utilizador são recuperadas;
  • linha 34: verificamos se a função já existe;
  • linhas 36–38: caso contrário, criamo-la na base de dados. Terá um nome no formato [ROLE_XX];
  • linha 40: verificamos se o login já existe;
  • linhas 42-49: se o nome de utilizador não existir, criamo-lo na base de dados;
  • linha 44: encriptamos a palavra-passe. Aqui, utilizamos a classe [BCrypt] do Spring Security (linha 4). 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 46: O utilizador é guardado na base de dados;
  • linha 48: assim como a relação que o liga à sua função;
  • linhas 51–57: se o login já existir, verificamos se a função que pretendemos atribuir-lhe já se encontra entre as suas funções;
  • Linhas 59–61: 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 implementámos proteções contra possíveis exceções. Esta é uma classe auxiliar para criar rapidamente um utilizador com uma função.

Quando a classe é executada com os argumentos [x x guest], obtêm-se os seguintes resultados na base de dados:

Tabela [USERS]

Tabela [FUNÇÕES]

 

Tabela [USERS_ROLES]

 

Agora, vamos considerar a segunda classe [UsersTest], que é um teste JUnit:

  

package spring.security.tests;
 
import java.util.List;
 
import org.junit.Assert;
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.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
 
import spring.security.config.DaoConfig;
import spring.security.dao.AppUserDetails;
import spring.security.dao.AppUserDetailsService;
import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.repositories.UserRepository;
 
@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {
 
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private AppUserDetailsService appUserDetailsService;
 
    // mapper jSON
    private ObjectMapper mapper = new ObjectMapper();
 
    @Test
    public void findAllUsersWithTheirRoles() throws JsonProcessingException {
        Iterable<User> users = userRepository.findAll();
        for (User user : users) {
            System.out.println(String.format("\n----------Utilisateur [%s]",mapper.writeValueAsString(user)));
            display("Roles :", userRepository.getRoles(user.getId()));
        }
    }
 
    @Test
    public void findUserByLogin() {
        // user [admin] is retrieved
        User user = userRepository.findUserByLogin("admin");
        // we check that his password is [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // check admin / admin role
        List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
        Assert.assertEquals(1L, roles.size());
        Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
    }
 
    @Test
    public void loadUserByUsername() {
        // user [admin] is retrieved
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // we check that his password is [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // check admin / admin role
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }
 
    // utility method - displays items in a collection
    private void display(String message, Iterable<?> elements) throws JsonProcessingException {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(mapper.writeValueAsString(element));
        }
    }
}
  • linhas 37–44: teste visual. Exibimos todos os utilizadores juntamente com as suas funções;
  • linhas 46–56: verificamos se o utilizador [admin] tem a palavra-passe [admin] e a função [ROLE_ADMIN] utilizando o [UserRepository];
  • linha 51: [admin] é a palavra-passe em texto simples. Na base de dados, esta é encriptada utilizando o algoritmo BCrypt. O método [BCrypt.checkpw] verifica se a palavra-passe em texto simples encriptada corresponde à que se encontra na base de dados;
  • linhas 58–69: verificamos se o utilizador [admin] tem a palavra-passe [admin] e a função [ROLE_ADMIN] utilizando o [appUserDetailsService];

Os testes foram executados com sucesso com os seguintes registos:

----------Utilisateur [{"id":14,"version":0,"identity":"admin","login":"admin","password":"$2a$10$FN1LMKjPU46aPffh9Zaw4exJOLo51JJPWrxqzak/eJrbt3CO9WzVG"}]
Roles :
{"id":6,"version":0,"name":"ROLE_ADMIN"}

----------Utilisateur [{"id":15,"version":0,"identity":"user","login":"user","password":"$2a$10$SJehR9Mv2VdyRZo9F0rXa.hKAoGLhJg6kSdyfExi40mEJrNOj0BTq"}]
Roles :
{"id":7,"version":0,"name":"ROLE_USER"}

----------Utilisateur [{"id":16,"version":0,"identity":"guest","login":"guest","password":"$2a$10$ubyWJb/vg2XZnUOAUjspZuz9jpHP3fIbPTbwQU115EtLdeSZ2PB7q"}]
Roles :
{"id":5,"version":0,"name":"ROLE_GUEST"}

----------Utilisateur [{"id":17,"version":0,"identity":"x","login":"x","password":"$2a$10$kEXA56wpKHFReVqwQTyWguKguK8I4uhA2zb6t3wGxag8Dyv7AhLom"}]
Roles :
{"id":5,"version":0,"name":"ROLE_GUEST"}

16.4.9. Teste do serviço web

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:

  

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=

Agora que sabemos como gerar o cabeçalho de autenticação HTTP, iniciamos o serviço web seguro e, em seguida, utilizando o cliente Chrome [Advanced Rest Client], solicitamos a lista de todos os produtos:

  • em [1], solicitamos a URL das categorias;
  • em [2], utilizando o método GET;
  • em [3], fornecemos o cabeçalho de autenticação HTTP. 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;

O nosso serviço web seguro está agora operacional.

16.4.10. 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.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import spring.webjson.models.Response;
 
@Controller
public class AuthenticateController {
 
    // spring dependencies
    @Autowired
    private ApplicationContext context;
 
    @RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String authenticate() throws JsonProcessingException {
        // answer jSON
        ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
        return mapperResponse.writeValueAsString(new Response<Void>(0, null, null));
    }
 
}
  • linha 15: a classe [AuthenticateController] é um controlador Spring. Como tal, expõe URLs;
  • linha 22: expõe a URL [/authenticate];
  • linha 23: o resultado do método será enviado diretamente para o cliente;
  • linhas 26–27: o método limita-se a devolver 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.

16.4.11. Conclusão

As classes necessárias para o Spring Security foram adicionadas sem modificar o projeto web/JSON original. Este cenário muito favorável decorre do 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. Nesse caso, as entidades JPA devem ser modificadas, o que geralmente afeta todas as camadas do projeto.

16.5. 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:

Duplicamos o projeto existente [intro-webjson-client] para um novo projeto [intro-spring-security-client-01]:

  

16.5.1. A classe [AbstractDao]

A classe [AbstractDao] gere 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=

Isto é feito da seguinte forma:


package spring.security.client.dao;
 
import java.net.URI;
...
 
public abstract class AbstractDao {
 
    // data
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;
 
    // generic request
    protected String getResponse(User user, String url, String jsonPost) {
 
// url : URL to contact

  • linha 15: o método genérico [getResponse], responsável pela comunicação HTTP com o serviço web seguro, aceita agora o utilizador que solicita uma URL como seu primeiro parâmetro. A classe [User] é a seguinte:

Esta classe é a seguinte:

  

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

O método [getResponse] passa então a ser o seguinte:


package spring.security.client.dao;
 
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Base64;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.RequestEntity.HeadersBuilder;
import org.springframework.web.client.RestTemplate;
 
import spring.security.client.entities.User;
 
public abstract class AbstractDao {

    // data
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;
 
    private String getBase64(User user) {
        // encodes user and password in base 64 - requires java 8
        String chaîne = String.format("%s:%s", user.getLogin(), user.getPassword());
        return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
    }
 
    // generic request
    protected String getResponse(User user, String url, String jsonPost) {
 
        // url : URL to contact
        // jsonPost: the jSON value to be posted
        try {
            // request execution
            RequestEntity<?> request;
            if (jsonPost == null) {
                HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    headersBuilder = headersBuilder.header("Authorization", getBase64(user));
                }
                request = headersBuilder.build();
            } else {
                BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
                }
                request = bodyBuilder.body(jsonPost);
            }
 
            // execute the query
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e1) {
            throw new DaoException(20, e1);
        } catch (RuntimeException e2) {
            throw new DaoException(21, e2);
        }
    }
 
}
 
  • linhas 42–44, 49–51: se o utilizador não for nulo, então o cabeçalho de autenticação é adicionado. A codificação Base64 do utilizador e da sua palavra-passe é tratada pelo método [getBase64] nas linhas 25–29. Note-se que este método utiliza uma classe [Base64] pertencente ao JDK 1.8.
  • À exceção das linhas anteriores, o código permanece inalterado;

16.5.2. A interface [IDao]

Todos os métodos da interface [IDao] recebem um parâmetro adicional [User user]:

  

package spring.security.client.dao;
 
import java.util.List;
 
import spring.security.client.entities.Categorie;
import spring.security.client.entities.Produit;
import spring.security.client.entities.User;
 
public interface IDaoClient {
 
    // authentication
    public void authenticate(User user);
 
    // insert product list
    public List<Produit> addProduits(User user, List<Produit> produits);
 
    // removal of all products
    public void deleteAllProduits(User user);
 
    // product list update
    public List<Produit> updateProduits(User user, List<Produit> produits);
 
    // all products obtained
    public List<Produit> getAllProduits(User user);
 
    // inserting a list of categories
    public List<Categorie> addCategories(User user, List<Categorie> categories);
 
    // delete all categories
    public void deleteAllCategories(User user);
 
    // updating a list of categories
    public List<Categorie> updateCategories(User user, List<Categorie> categories);
 
    // obtaining all categories
    public List<Categorie> getAllCategories(User user);
 
    // a special product
    public Produit getProduitByIdWithCategorie(User user, Long idProduit);
 
    public Produit getProduitByIdWithoutCategorie(User user, Long idProduit);
 
    public Produit getProduitByNameWithCategorie(User user, String nom);
 
    public Produit getProduitByNameWithoutCategorie(User user, String nom);
 
    // a special category
    public Categorie getCategorieByIdWithProduits(User user, Long idCategorie);
 
    public Categorie getCategorieByIdWithoutProduits(User user, Long idCategorie);
 
    public Categorie getCategorieByNameWithProduits(User user, String nom);
 
    public Categorie getCategorieByNameWithoutProduits(User user, String nom);
 
}
  • Linha 12: Adicionámos o método [authenticate(User user)] para autenticar um utilizador. Este método lança uma exceção se o utilizador não tiver permissão para aceder à URL [/authenticate] do serviço web;

16.5.3. A classe [Dao]

Todos os métodos da classe [Dao] recebem um parâmetro adicional [User user] que passam para o método genérico [getResponse] da classe [AbstractDao]. Aqui estão dois exemplos:


// authentication
    @Override
    public void authenticate(User user) {
        getResponse(user, "/authenticate", null);
    }
 
    @Override
    public List<Produit> addProduits(User user, List<Produit> produits) {
        // ----------- add products (without category)
        try {
            // mappers jSON
            ObjectMapper mapperPost = context.getBean(ObjectMapper.class);
            mapperPost.setFilters(jsonFilterProduitWithoutCategorie);
            ObjectMapper mapperResponse = mapperPost;
            // request
            Response<List<Produit>> response = mapperResponse.readValue(
                    getResponse(user, "/addProduits", mapperPost.writeValueAsString(produits)),
                    new TypeReference<Response<List<Produit>>>() {
                    });
            // mistake?
            if (response.getStatus() != 0) {
                // 1 exception is thrown
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // render the core of the server response
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (IOException | RuntimeException e2) {
            throw new DaoException(100, e2);
        }
    }
 

16.5.4. Testes unitários para a classe [Dao]

A classe [Test01] para testes unitários da classe [Dao] é modificada da seguinte forma:

  

package client.tests.junit;
 
...
 
@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {
 
    // spring context
    @Autowired
    private ApplicationContext context;
    // layer [DAO]
    @Autowired
    private IDaoClient dao;
 
    // users
    static private User admin;
    static private User user;
    static private User unknown;
 
    @BeforeClass
    public static void init() {
        admin = new User("admin", "admin");
        user = new User("user", "user");
        unknown = new User("x", "y");
    }
 
    @Before
    public void cleanAndFill() {
        // the base is cleaned before each test
        log("Vidage de la base de données", 1);
        // table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
        dao.deleteAllCategories(admin);
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // fill the tables
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < 2; i++) {
            Categorie categorie = new Categorie(String.format("categorie%d", i));
            for (int j = 0; j < 5; j++) {
                categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                        String.format("desc%d%d", i, j)));
            }
            categories.add(categorie);
        }
        // add the category - the products will be cascaded in as well
        dao.addCategories(admin, categories);
    }
 
    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // list of categories
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories(admin);
        affiche(categories, context.getBean("jsonMapperCategorieWithoutProduits", ObjectMapper.class));
        // product list
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits(admin);
        affiche(produits, context.getBean("jsonMapperProduitWithoutCategorie", ObjectMapper.class));
        // a few checks
        Assert.assertEquals(2, categories.size());
        Assert.assertEquals(10, produits.size());
        Categorie categorie = findCategorieByName("categorie0", categories);
        Assert.assertNotNull(categorie);
        Produit produit = findProduitByName("produit03", produits);
        Assert.assertNotNull(produit);
        Long idCategorie = produit.getIdCategorie();
        Assert.assertEquals(categorie.getId(), idCategorie);
    }
...
    @Test()
    public void checkUserUser() {
        ServiceException se = null;
        try {
            dao.authenticate(user);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("403 Forbidden", se.getMessages().get(0));
    }
 
    @Test()
    public void checkUserUnknown() {
        ServiceException se = null;
        try {
            dao.authenticate(unknown);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("401 Unauthorized", se.getMessages().get(0));
    }
 
    @Test()
    public void checkUserAdmin() {
        ServiceException se = null;
        try {
            dao.authenticate(admin);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNull(se);
    }
...
}
  • Durante a inicialização da classe de teste, linhas 21–26, são criados três utilizadores:
    • o utilizador [admin] tem acesso aos URLs do serviço web, linhas de teste 96–104;
    • o utilizador [user] existe, mas não está autorizado a utilizar os URLs do serviço web, linhas de teste 71–81;
    • o utilizador [unknown] não existe, linhas de teste 83–93;
  • os métodos de teste são os mesmos que já foram vistos para o serviço web não seguro, exceto que os métodos da interface [IDaoClient] são chamados com o utilizador [admin] — que tem permissão para utilizar as URLs — como primeiro parâmetro;

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 seguro: 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 um custo. Obriga o utilizador a autenticar-se em cada acesso. Se a comentarmos, a duração do teste JUnit anterior diminui de 10,57 segundos para 4,21 segundos, porque o utilizador [admin] só se autentica no primeiro teste e não nos seguintes (embora o cabeçalho de autenticação HTTP seja enviado pelo cliente, o servidor não volta a verificar a palavra-passe do utilizador). Com um serviço web não seguro, a duração do teste JUnit cai para 2,33 segundos.