16. [Cours]: Proteger o acesso a um serviço web com o Spring Security
Palavras-chave: arquitetura multicamadas, Spring, injeção de dependências, serviço web / jSON seguro, cliente / servidor
16.1. Support
![]() | ![]() |
Os projetos deste capítulo encontram-se na pasta [support / chap-16]. O script SQL serve para gerar a base de dados necessária para os testes.
16.2. O papel do Spring Security numa aplicação Web
Vamos contextualizar o Spring Security no 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 aos 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], encontram-se 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 analisar o seu ficheiro [pom.xml] para conhecer 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 do framework [Thymeleaf];
- linhas 22-25: dependência do framework Spring Security;
16.3.2. As vistas Thymeleaf
![]() |
A vista [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 baliza [<a>]. O valor [@{/hello}] irá gerar o caminho [<context>/hello], 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>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click
<a href="/hello">here</a>
to see a greeting.
</p>
</body>
</html>
A vista [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 baliza [<h1>]. Este texto contém uma expressão $ que deve ser avaliada. O elemento [[${#httpServletRequest.remoteUser}]] é o valor do atributo [RemoteUser] da consulta HTTP atual. Trata-se do nome do utilizador que está ligado;
- linha 10: um formulário HTML. O atributo [th:action="@{/logout}"] irá gerar o atributo [action] da baliza [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 «Hello [[${#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}"] faz com que a baliza <div> só seja gerada se o URL, que apresenta a página de início de sessão, contiver o parâmetro [error] (http://context/login?error);
- linha 10: o atributo [th:if="${param.logout}"] faz com que a tag <div> só seja gerada se o URL, que apresenta a página de início de sessão, contiver o parâmetro [logout] (http://context/login?logout);
- linhas 11-23: um formulário HTML;
- linha 11: o formulário será enviado para o URL [<context>/login], em que <context> é o contexto da aplicação web;
- linha 13: um campo de introdução de dados denominado [username];
- linha 17: um campo de introdução de dados 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>
Note-se, na linha 28, que o Thymeleaf adicionou um campo oculto denominado [_csrf].
16.3.3. Configuração Spring MVC
![]() |
A classe [MvcConfig] configura o framework 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] transforma a classe [MvcConfig] numa classe de configuração;
- linha 8: a classe [MvcConfig] estende a classe [WebMvcConfigurerAdapter] para redefinir alguns dos seus métodos;
- linha 10: redefinição de um método da classe pai;
- linhas 11-16: o método [addViewControllers] permite associar URL a vistas HTML. São feitas as seguintes associações:
vista | |
/templates/home.html | |
/templates/hello.html | |
/templates/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:
![]() |
Acima de [1], as pastas [java] e [resources] são ambas pastas de origem (source folders). Isto significa que o seu conteúdo estará na raiz do Classpath do projeto. Assim, 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 o framework 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] transforma a classe [WebSecurityConfig] numa 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 redefinir alguns dos seus métodos;
- linha 12: redefinição de um método da classe pai;
- linhas 13-16: o método [configure(HttpSecurity http)] é redefinido para definir os direitos de acesso aos diferentes URL da aplicação;
- linha 14: o método [http.authorizeRequests()] permite associar URL a direitos de acesso. São feitas as seguintes associações:
regra | código | |
acesso sem autenticação | | |
Acesso apenas com autenticação |
- linha 15: define o método de autenticação. A autenticação é feita através de um formulário URL [/login] acessível a todos [http.formLogin().loginPage("/login").permitAll()]. O logout também está acessível a todos;
- linhas 19-21: redefinem o método [configure(AuthenticationManagerBuilder auth)] que gere os utilizadores;
- linha 20: a autenticação é feita com utilizadores definidos de forma «estática» [auth.inMemoryAuthentication()]. Um utilizador é aqui definido com o nome de utilizador [user], a palavra-passe [password] e a função [USER]. É possível conceder os mesmos direitos a utilizadores com a mesma função;
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] solicita ao Spring Boot (linha 3) que efetue a configuração que o programador não terá feito explicitamente;
- linha 9: transforma a classe [Application] numa classe de configuração do Spring;
- linha 10: solicita a análise da pasta da classe [Application] para procurar componentes Spring. As duas classes [MvcConfig] e [WebSecurityConfig] serão assim detetadas, uma vez que 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 incluído nas dependências Maven do projeto será iniciado e que o projeto será implementado nesse servidor. Vimos que quatro instâncias de URL eram geridas por [/, /home, /login, /hello] e que algumas estavam protegidas por direitos de acesso.
16.3.6. Testes da aplicação
Comecemos por solicitar o URL [/], que é um dos quatro URL aceites. Está associado à vista [/templates/home.html]:
![]() |
A URL solicitada, [/], está acessível a todos. Foi por isso que a obtivemos. O link [here] é o seguinte:
O URL [/hello] será solicitado quando se clicar no link. Este está protegido:
regra | código | |
acesso sem autenticação | | |
Acesso apenas para utilizadores autenticados |
É necessário estar autenticado para o obter. O Spring Security irá então redirecionar o navegador do cliente para a página de autenticação. De acordo com a configuração apresentada, trata-se da página URL [/login]. Esta página está acessível a todos:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Assim, obtemos [1]:
![]() |
O código-fonte da página obtida é o seguinte:
- na linha 7, surge um campo oculto que não consta na página original [login.html]. Foi o Thymeleaf que o adicionou. Este código, denominado CSRF (Cross Site Request Forgery), tem como objetivo eliminar uma falha de segurança. Este token deve ser reenviado ao Spring Security juntamente com a autenticação para que esta seja aceite;
Recordamos que apenas o utilizador user/password é reconhecido pelo Spring Security. Se introduzirmos outro valor em [2], obtemos a mesma página com uma mensagem de erro em [3]. O Spring Security redirecionou o navegador para a página URL [http://localhost:8080/login?error]. A presença do parâmetro [error] fez com que fosse exibida a tag:
<div th:if="${param.error}">Invalid username and password.</div>
Agora, introduzamos os valores esperados para «user/password» [4]:
![]() |
- em [4], identificamo-nos;
- em [5], o Spring Security redireciona-nos para o URL [/hello], pois era o URL que solicitámos quando fomos redirecionados para a página de início de sessão. A identidade do utilizador foi apresentada na seguinte linha de [hello.html]:
A página [5] apresenta o seguinte formulário:
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
Ao clicar no botão [Sign Out], será efetuada uma ação POST na página URL [/logout]. Este, tal como o URL e o [/login], está acessível a todos:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Na nossa associação URL / vistas, não definimos nada para o URL e o [/logout]. O que irá acontecer? Vamos experimentar:
![]() |
- em [6], clicamos no botão [Sign Out];
- em [7], vemos que fomos redirecionados para o URL [http://localhost:8080/login?logout]. Foi o Spring Security que solicitou este redirecionamento. A presença do parâmetro [logout] no URL fez com que fosse apresentada a seguinte linha na vista:
<div th:if="${param.logout}">You have been logged out.</div>
16.3.7. Conclusão
No exemplo anterior, poderíamos ter escrito primeiro a aplicação web e, só depois, implementado a segurança. O Spring Security não é intrusivo. É possível implementar a segurança numa aplicação web já 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, o utilizador é redirecionado para a página de autenticação, com um parâmetro «error» adicional no token URL;
- se a autenticação for bem-sucedida, o utilizador é redirecionado para a página solicitada no momento em que a autenticação ocorreu. Se se aceder diretamente à página de autenticação sem passar por uma página intermédia, o Spring Security redireciona-nos para o URL [/] (este caso não foi apresentado);
- desautentificamo-nos ao aceder à página URL [/logout] com um POST. O Spring Security redireciona-nos então para a página de autenticação com o parâmetro «logout» no URL;
Todas estas conclusões baseiam-se nos comportamentos por predefinição do Spring Security. Estes comportamentos podem ser alterados através da configuração, redefinindo determinados métodos da classe [WebSecurityConfigurerAdapter].
O tutorial anterior será de pouca utilidade daqui em diante. Iremos, de facto, utilizar:
- uma base de dados para armazenar os utilizadores, as suas palavras-passe e as suas funções;
- uma autenticação por cabeçalho HTTP;
Existem poucos tutoriais sobre o que pretendemos fazer aqui. A solução que iremos propor é uma compilação de códigos encontrados aqui e ali.
16.4. Implementação da segurança no serviço web / JSON dos produtos
16.4.1. A base de dados
A base de dados [dbintrospringdata] está a ser atualizada para incluir os utilizadores, as suas palavras-passe e as suas funções. Surgem três novas tabelas:

Tabela [USERS]: os utilizadores
- ID: chave primária;
- VERSION: coluna de controlo de versões da linha;
- IDENTITY: uma identidade descritiva do 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 que encripta as palavras-passe é o algoritmo BCRYPT.
Tabela [ROLES]: as funções
- 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;
![]() |
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. Trata-se de 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;
![]() |
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 [repositories] do Spring Data associados às três novas tabelas;
- [spring.security.dao]: contém um serviço baseado nos [repositories];
- [spring.security.config]: contém a configuração do projeto e, nomeadamente, a configuração dos acessos seguros ao serviço web;
- [spring.security.boot]: contém a classe de inicialização do 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>
<!-- Registos do Spring -->
<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>
<!-- Teste do Spring Boot -->
<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: retoma-se o que já existe com o arquivo do serviço web / json analisado;
- linhas 29-32: a dependência que fornece as classes do Spring Security;
- linhas 34-37: a biblioteca de registos;
- linhas 39-42: a biblioteca que permite utilizar as anotações do Spring Boot;
- linhas 44-48: a biblioteca necessária para os testes;
16.4.4. As novas entidades [JPA]
![]() |
A camada JPA define três novas entidades:
![]() |
A classe [User] é a imagem da 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 {
// propriedades
@Column(name = "NAME")
private String name;
@Column(name = "LOGIN")
private String login;
@Column(name = "PASSWORD")
private String password;
// Construtor
public User() {
}
public User(String name, String login, String password) {
this.name = name;
this.login = login;
this.password = password;
}
// getters e setters
...
}
- linha 11: a classe estende a classe [AbstractEntity] já utilizada para as outras entidades;
A classe [Role] é a imagem da 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 {
// propriedades
@Column(name="NAME")
private String name;
// construtores
public Role() {
}
public Role(String name) {
this.name = name;
}
// getters e setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
A classe [UserRole] é a imagem da 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 {
// chaves externas
@Column(name = "USER_ID", insertable = false, updatable = false)
private Long userId;
@Column(name = "ROLE_ID", insertable = false, updatable = false)
private Long roleId;
// um UserRole faz referência a um utilizador
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
// um UserRole faz referência a uma função
@ManyToOne
@JoinColumn(name = "ROLE_ID")
private Role role;
// construtores
public UserRole() {
}
public UserRole(User user, Role role) {
this.user = user;
this.role = role;
}
// getters e 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. As [repositories]
![]() |
Cada uma das entidades JPA anteriores é gerida por um [repository] 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> {
// 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 4);
- linhas 12-13: o método [getRoles(User user)] 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 org.springframework.data.repository.CrudRepository;
import spring.security.entities.Role;
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;
A interface [UserRoleRepository] gere os acessos à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] limita-se a estender a interface [CrudRepository] sem lhe adicionar novos métodos;
16.4.6. As classes de gestão de utilizadores e funções
![]() |
![]() |
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 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;
// propriedades
private User user;
private UserRepository userRepository;
// 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<>();
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 e 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 permite obter os detalhes desse 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]. Este método deve construir uma coleção de elementos do tipo [GrantedAuthority] ou derivado. Aqui, utilizamos o tipo derivado [SimpleGrantedAuthority] (linha 36), que encapsula o nome de uma das funções do utilizador da linha 19;
- linhas 35-37: percorre-se a lista de funções do utilizador da linha 19 para criar uma lista de elementos do tipo [SimpleGrantedAuthority];
- linhas 42-44: implementam o método [getPassword] da interface [UserDetails]. Retorna-se a palavra-passe do utilizador da linha 19;
- linhas 42-44: implementam o método [getUserName] da interface [UserDetails]. Retorna-se o nome de 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 exige 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 {
// procura-se o utilizador pelo seu nome de utilizador
User user = userRepository.findUserByLogin(login);
// Encontrado?
if (user == null) {
throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
}
// retornamos os detalhes do utilizador
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;
- linha 21: o utilizador é procurado através do seu nome de utilizador;
- linhas 23-25: se não for encontrado, é lançada uma exceção;
- linha 27: é criado e apresentado um objeto [AppUserDetails]. Este é, de facto, do tipo [UserDetails] (linha 19);
16.4.7. A 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 {
// constantes
final static private String[] ENTITIES_PACKAGES = { "spring.data.entities", "spring.security.entities" };
@Bean
public String[] packagesToScan() {
return ENTITIES_PACKAGES;
}
}
- linha 10: importa-se a classe de configuração [spring.data.config.DaoConfig] do projeto [intro-spring-data-01], que implementa a camada [DAO] de produtos e categorias;
- linha 8: indicam-se as pastas do projeto atual que contêm os componentes Spring Data [repositories];
- linha 9: indicam-se as pastas do projeto atual que contêm componentes Spring relativos à camada [DAO];
- linha 14: indicam-se as pastas que contêm entidades JPA. Existem as do projeto [intro-spring-data-01] e as do projeto do servidor seguro. Esta informação é o objeto do bean das linhas 16-19. Este bean redefine o bean com o mesmo nome do 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 as pastas indicadas na linha 1. Devido à redefinição do bean das linhas 14-17 no projeto seguro (linhas 16-19), a linha 8 acima passará agora a analisar as pastas ["spring.data.entities", "spring.security.entities"]. É importante referir que a classe importada na linha 10 da classe [spring.security.config.DaoConfig] deve incluir a anotação [@Configuration]; caso contrário, o fenómeno que acabou de ser explicado não funciona.
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");
}
}
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 [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;
// proteção
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");
// sem sessão
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
}
- linha 16: para ativar os elementos do Spring Security;
- linha 17: adicionam-se os componentes Spring do pacote [spring.security.service];
- linha 18: importam-se os beans da camada [DAO] que acabámos de apresentar, bem como os do servidor web / jSON não seguro;
- linhas 21-22: é injetada a classe [AppUserDetails], que concede acesso aos utilizadores da aplicação;
- linha 25: um valor booleano que protege (true) ou não (false) a aplicação web;
- linhas 27-32: 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 38):
- uma referência ao serviço [appUserDetailsService] da linha 22, que dá acesso aos utilizadores registados. Note-se aqui que o facto de estarem registados numa base de dados não é mencionado. Podem, portanto, estar num cache, fornecidos por um serviço web, ...
- o tipo de encriptação utilizado para a palavra-passe. Recorde-se aqui que utilizámos o algoritmo BCrypt;
- linhas 34-52: o método [configure(HttpSecurity http)] define os direitos de acesso aos URL do serviço web;
- linha 37: vimos no projeto de introdução que, por predefinição, o Spring Security geria 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, aliado ao valor booleano (isSecured=false), permite utilizar a aplicação web sem segurança;
- linha 41: ativa-se o modo de autenticação por cabeçalho HTTP. O cliente deverá enviar o seguinte cabeçalho HTTP:
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:
- linhas 46-48: 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 50: no modo [session], um utilizador que se tenha autenticado uma vez não precisa de o fazer nos acessos seguintes. Aqui, desativamos este modo, pelo que o utilizador terá de se autenticar em cada acesso;
16.4.8. Testes da camada [DAO]
![]() |
![]() |
Em primeiro lugar, 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) {
// sintaxe: login, palavra-passe roleName
// são necessários três parâmetros
if (args.length != 3) {
System.out.println("Syntaxe : [pg] user password role");
System.exit(0);
}
// recuperam-se os parâmetros
String login = args[0];
String password = args[1];
String roleName = String.format("ROLE_%s", args[2].toUpperCase());
// contexto Spring
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
UserRepository userRepository = context.getBean(UserRepository.class);
RoleRepository roleRepository = context.getBean(RoleRepository.class);
UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
// a função já existe?
Role role = roleRepository.findRoleByName(roleName);
// se não existir, criamo-lo
if (role == null) {
role = roleRepository.save(new Role(roleName));
}
// o utilizador já existe?
User user = userRepository.findUserByLogin(login);
// Se não existir, criamo-lo
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(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é...");
}
}
- linha 17: a classe espera três argumentos que definem um utilizador: o seu nome de utilizador, a sua palavra-passe e a sua 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: recuperam-se as referências das três instâncias de [Repository] que podem ser úteis para criar o utilizador;
- linha 34: verifica-se se a função já existe;
- linhas 36-38: se não for o caso, cria-se na base de dados. Terá um nome do tipo [ROLE_XX];
- linha 40: verifica-se se o nome de utilizador já existe;
- linhas 42-49: se o nome de utilizador não existir, criamo-lo na base de dados;
- linha 44: encripta-se a palavra-passe. Aqui, utiliza-se a classe [BCrypt] do Spring Security (linha 4). 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 46: o utilizador é guardado na base de dados;
- linha 48: assim como a relação que o liga à sua função;
- linhas 51-57: 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 59-61: 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 se previu a ocorrência de eventuais exceções. Trata-se de uma classe de suporte para criar rapidamente um utilizador com uma função.
Ao executar a classe com os argumentos [x x guest], obtêm-se, na base de dados, os seguintes resultados:
Tabela [USERS]
![]() Tabela |
Tabela [ROLES]
![]() |
Tabela [USERS_ROLES]
![]() |
Consideremos agora 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;
// mapeador 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() {
// recupera-se o utilizador [admin]
User user = userRepository.findUserByLogin("admin");
// verifica-se se a sua palavra-passe é [admin]
Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
// verifica-se a função de administrador / administrador
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() {
// recuperamos o utilizador [admin]
AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
// verifica-se se a sua palavra-passe é [admin]
Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
// verifica-se a função de admin / admin
@SuppressWarnings("unchecked")
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
Assert.assertEquals(1L, authorities.size());
Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
}
// método utilitário — apresenta os elementos de uma coleção
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. São apresentados todos os utilizadores com as respetivas funções;
- linhas 46-56: verifica-se se o utilizador [admin] tem a palavra-passe [admin] e a função [ROLE_ADMIN], utilizando o repositório [UserRepository];
- linha 51: [admin] é a palavra-passe em texto simples. Na base, está encriptada de acordo com o algoritmo BCrypt. O método [BCrypt.checkpw] permite verificar se a palavra-passe em texto simples, uma vez encriptada, é efetivamente igual à que se encontra na base;
- linhas 58-69: verifica-se se o utilizador [admin] tem a palavra-passe [admin] e a função [ROLE_ADMIN], utilizando o serviço [appUserDetailsService];
A execução dos testes é bem-sucedida com os seguintes registos:
16.4.9. Testes do serviço web
Vamos testar o serviço web com o cliente Chrome [Advanced Rest Client]. Teremos de especificar o cabeçalho de autenticação HTTP:
onde [code] é o código Base64 da cadeia [login:password]. Para gerar este código, pode utilizar-se o seguinte programa:
![]() |
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:
Agora que sabemos como gerar o cabeçalho de autenticação HTTP, iniciamos o serviço web seguro e, em seguida, com o cliente Chrome [Advanced Rest Client], solicitamos a lista de todos os produtos:
![]() |
- em [1], solicitamos o URL das categorias;
- em [2], com o 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:
![]() |
- 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;
O nosso serviço web seguro está agora operacional.
16.4.10. 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 novo controlador MVC [AuthenticateController] seguinte:
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 {
// dependências do Spring
@Autowired
private ApplicationContext context;
@RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String authenticate() throws JsonProcessingException {
// resposta 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 URL;
- linha 22: expõe o 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 houve 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, ela 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.
16.4.11. Conclusão
A adição das classes necessárias ao Spring Security foi possível sem alterações no projeto web/json original. Este caso 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. Nesse caso, é necessário modificar as entidades JPA, o que, geralmente, afeta todas as camadas do projeto.
16.5. 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:
![]() |
Duplicamos o projeto já criado, [intro-webjson-client], num novo projeto, [intro-spring-security-client-01]:
![]() |
16.5.1. A classe [AbstractDao]
A classe [AbstractDao] 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:
Isto é feito da seguinte forma:
package spring.security.client.dao;
import java.net.URI;
...
public abstract class AbstractDao {
// dados
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// pedido genérico
protected String getResponse(User user, String url, String jsonPost) {
// URL: URL para contacto
- linha 15: o método genérico [getResponse], responsável pela comunicação HTTP com o serviço web seguro, aceita agora como primeiro parâmetro o utilizador que solicita um URL. A classe [User] é a seguinte:
Esta classe é a seguinte:
![]() |
package spring.security.client.entities;
public class User {
// características
private String login;
private String password;
// construtor
public User() {
}
public User(String login, String password) {
this.login = login;
this.password = password;
}
// getters e 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 {
// dados
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
private String getBase64(User user) {
// o utilizador e a sua palavra-passe são codificados em base 64 — requer 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())));
}
// pedido genérico
protected String getResponse(User user, String url, String jsonPost) {
// URL: URL - contactar
// jsonPost: o valor jSON a enviar
try {
// execução da solicitação
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);
}
// a consulta está a ser executada
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 [user] não for nulo, então adiciona-se o cabeçalho de autenticação. A encriptação Base64 do utilizador e da sua palavra-passe é assegurada pelo método [getBase64] das linhas 25-29. É importante ter em conta que este método utiliza uma classe [Base64] pertencente ao JDK 1.8.
- Para além 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 {
// autenticação
public void authenticate(User user);
// inserção de uma lista de produtos
public List<Produit> addProduits(User user, List<Produit> produits);
// eliminação de todos os produtos
public void deleteAllProduits(User user);
// atualização de uma lista de produtos
public List<Produit> updateProduits(User user, List<Produit> produits);
// obter todos os produtos
public List<Produit> getAllProduits(User user);
// inserção de uma lista de categorias
public List<Categorie> addCategories(User user, List<Categorie> categories);
// eliminação de todas as categorias
public void deleteAllCategories(User user);
// atualização de uma lista de categorias
public List<Categorie> updateCategories(User user, List<Categorie> categories);
// obter todas as categorias
public List<Categorie> getAllCategories(User user);
// um produto específico
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);
// uma categoria específica
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 ao 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:
// autenticação
@Override
public void authenticate(User user) {
getResponse(user, "/authenticate", null);
}
@Override
public List<Produit> addProduits(User user, List<Produit> produits) {
// ----------- adicionar produtos (sem a respetiva categoria)
try {
// mapeadores jSON
ObjectMapper mapperPost = context.getBean(ObjectMapper.class);
mapperPost.setFilters(jsonFilterProduitWithoutCategorie);
ObjectMapper mapperResponse = mapperPost;
// pedido
Response<List<Produit>> response = mapperResponse.readValue(
getResponse(user, "/addProduits", mapperPost.writeValueAsString(produits)),
new TypeReference<Response<List<Produit>>>() {
});
// erro?
if (response.getStatus() != 0) {
// é lançada 1 exceção
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// retorna o corpo da resposta do servidor
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (IOException | RuntimeException e2) {
throw new DaoException(100, e2);
}
}
16.5.4. Testes unitários da classe [Dao]
A classe [Test01] de testes unitários da classe [Dao] é alterada da seguinte forma:
![]() |
package client.tests.junit;
...
@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {
// contexto Spring
@Autowired
private ApplicationContext context;
// camada [DAO]
@Autowired
private IDaoClient dao;
// utilizadores
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() {
// limpa-se a base de dados antes de cada teste
log("Vidage de la base de données", 1);
// esvazia-se a tabela [CATEGORIES] — em cadeia, a tabela [PRODUITS] será esvaziada
dao.deleteAllCategories(admin);
// --------------------------------------------------------------------------------------
log("Remplissage de la base", 1);
// preenchem-se as tabelas
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);
}
// adição da categoria — por efeito em cadeia, os produtos também serão inseridos
dao.addCategories(admin, categories);
}
@Test
public void showDataBase() throws BeansException, JsonProcessingException {
// lista de categorias
log("Liste des catégories", 2);
List<Categorie> categories = dao.getAllCategories(admin);
affiche(categories, context.getBean("jsonMapperCategorieWithoutProduits", ObjectMapper.class));
// lista de produtos
log("Liste des produits", 2);
List<Produit> produits = dao.getAllProduits(admin);
affiche(produits, context.getBean("jsonMapperProduitWithoutCategorie", ObjectMapper.class));
// algumas verificações
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, nas linhas 21-26, são criados três utilizadores:
- o utilizador [admin] tem acesso ao URL do serviço web, teste linhas 96-104;
- o utilizador [user] existe, mas não está autorizado a utilizar os URL do serviço web, teste nas linhas 71-81;
- o utilizador [unknown] não existe, teste nas linhas 83-93;
- os métodos de teste são os já vistos para o serviço web não seguro, com a diferença de que os métodos da interface [IDaoClient] são chamados com, como primeiro parâmetro, o utilizador [admin], que tem permissão para utilizar os URL;
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. Obriga o utilizador a autenticar-se em cada acesso. Se a colocarmos em comentário, a duração do teste JUnit anterior passa de 10,57 segundos para 4,21 segundos, 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). Com um serviço web não seguro, a duração do teste JUnit desce para 2,33 segundos.
























































