Continuando do artigo anterior sobre autenticação JWT com Spring Boot, realizei uma integração completa entre o Spring Boot 2.0.4 e o MyBatis.
Artigo de referência: Implementação de Controle de Acesso RESTful com Spring Boot + Spring Security + JWT
Código-fonte disponível em:
Gitee: https://gitee.com/region/spring-security-oauth-example/tree/master/spring-security-jwt
Dependências necessárias no pom.xml para Spring Boot 2.0.4 + MyBatis:
Como o Spring Boot 2.0.4 não possui um PasswordEncoder padrão, não podemos fazer login com texto puro. Para facilitar o desenvolvimento, utilizei um banco de dados.
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- Spring-MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<!-- Driver de conexão MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
Modificações na configuração de segurança:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import com.jwt.server.filter.JwtAuthenticationFilter;
import com.jwt.server.filter.JwtLoginFilter;
import com.jwt.server.provider.CustomAuthenticationProvider;
/**
* Configuração do Spring Security que combina os filtros JwtLoginFilter e JwtAuthenticationFilter
*
* @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) Esta anotação era utilizada na versão 1.5.8 do Spring Boot
* conforme verificado no código fonte
* @author zyl
*
*/
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Qualifier("userDetailServiceImpl")
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// Configurações personalizadas
http.cors().and().csrf().disable().authorizeRequests().antMatchers("/users/signup").permitAll().anyRequest()
.authenticated().and().addFilter(new JwtLoginFilter(authenticationManager()))// Filtro padrão de login
.addFilter(new JwtAuthenticationFilter(authenticationManager()));// Filtro personalizado
}
// Método acionado durante o processo de login
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// Utilização de componente de autenticação personalizado com injeção manual da classe de criptografia
auth.authenticationProvider(new CustomAuthenticationProvider(userDetailsService, bCryptPasswordEncoder));
}
}
Componente personalizado de autenticação:
package com.jwt.server.provider;
import java.util.ArrayList;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* Componente personalizado para validação de credenciais
* @author zyl
*
*/
public class CustomAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
private BCryptPasswordEncoder bCryptPasswordEncoder;
public CustomAuthenticationProvider(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder){
this.userDetailsService = userDetailsService;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// Obtém nome de usuário e senha da autenticação
String name = authentication.getName();
String password = authentication.getCredentials().toString();
// Lógica de autenticação
UserDetails userDetails = userDetailsService.loadUserByUsername(name);
if (null != userDetails) {
if (bCryptPasswordEncoder.matches(password, userDetails.getPassword())) {
// Define permissões e funções aqui
ArrayList<GrantedAuthority> authorities = new ArrayList<>();
authorities.add( new GrantedAuthorityImpl("ROLE_ADMIN"));
authorities.add( new GrantedAuthorityImpl("ROLE_API"));
authorities.add( new GrantedAuthorityImpl("AUTH_WRITE"));
// Gera token com nome de usuário, senha e autoridades
Authentication auth = new UsernamePasswordAuthenticationToken(name, password, authorities);
return auth;
} else {
throw new BadCredentialsException("Senha incorreta");
}
} else {
throw new UsernameNotFoundException("Usuário não encontrado");
}
}
/**
* Verifica se pode fornecer serviços de autenticação para o tipo de entrada
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
Implementação de autorização para armazenar permissões e funções:
package com.jwt.server.provider;
import org.springframework.security.core.GrantedAuthority;
/**
* Implementação de autorização para armazenar permissões e funções
*
* @author zyl
*/
public class GrantedAuthorityImpl implements GrantedAuthority {
/**
*
*/
private static final long serialVersionUID = 1L;
private String authority;
public GrantedAuthorityImpl(String authority) {
this.authority = authority;
}
public void setAuthority(String authority) {
this.authority = authority;
}
@Override
public String getAuthority() {
return this.authority;
}
}
Definição dos arquivos de serviço e DAO para o banco de dados:
package com.jwt.server.service;
import com.jwt.server.domain.UserInfo;
/**
* Interface de serviço para usuários
* @author zyl
*
*/
public interface UserService {
/**
* Verifica se um usuário existe pelo nome de usuário
* @param username
* @return
*/
public UserInfo findByUsername(String username);
/**
* Adiciona um novo usuário
* @param user
* @return
*/
public UserInfo save(UserInfo user);
}
package com.jwt.server.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.jwt.server.domain.UserInfo;
import com.jwt.server.mapper.UserMapper;
import com.jwt.server.service.UserService;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper usermapper;
@Override
public UserInfo findByUsername(String username) {
return usermapper.findByUsername(username);
}
@Override
public UserInfo save(UserInfo user) {
return usermapper.save(user);
}
}
Modificação da classe UserDetailServiceImpl existente:
package com.jwt.server.service.impl;
import static java.util.Collections.emptyList;
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 com.jwt.server.domain.UserInfo;
import com.jwt.server.service.UserService;
/**
* Implementação do UserDetailsService
* @author zyl
*
*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
protected UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserInfo user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),
emptyList());
}
}
Adição da classe gerdaora de IDs:
package com.jwt.server.util;
import java.net.InetAddress;
import java.net.UnknownHostException;
import org.apache.commons.lang3.time.DateFormatUtils;
import lombok.extern.slf4j.Slf4j;
/**
* Diferente do algoritmo snowflake, esta implementação retorna um ID como string,
* ocupando mais bytes mas permitindo visualização da data de geração diretamente no ID
*
*/
@Slf4j
public enum IdGenerator {
/**
* Cada tipo de sequência requerida corresponde a uma instância
*/
USER_TRANSID("1");
private long workerId; // Identificado pelos últimos bytes do IP
private long datacenterId = 0L; // Pode ser configurado no properties, carregado na inicialização
private long sequence = 0L;
private final long twepoch = 1516175710371L;
private final long workerIdBits = 1L;
private final long datacenterIdBits = 2L;
private final long sequenceBits = 3L;
private final long workerIdShift = sequenceBits;
private final long datacenterIdShift = sequenceBits + workerIdBits;
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private long sequenceMask = -1L ^ (-1L << sequenceBits); //4095
private long lastTimestamp = -1L;
private String index;
IdGenerator(String ind) {
this.index = ind;
workerId = 0x000000FF & getLastIP();
}
public synchronized String nextId() {
long timestamp = timeGen(); // Obtém timestamp atual em milissegundos
// Verifica problemas de tempo do servidor (clock retrocedido)
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format(
"Clock retrocedido. Recusando gerar ID por %d milissegundos", lastTimestamp - timestamp));
}
// Se o timestamp for igual ao anterior (mesmo milissegundo)
if (lastTimestamp == timestamp) {
// Incrementa sequence, usando apenas os bits inferiores devido ao AND com sequenceMask
sequence = (sequence + 1) & sequenceMask;
// Verifica se houve estouro, ou seja, mais de 4095 por milissegundo
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp); // Aguarda até o próximo milissegundo
}
} else {
sequence = 0L; // Reseta sequence para o novo milissegundo
}
lastTimestamp = timestamp;
long suffix = ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
String datePrefix = DateFormatUtils.format(timeGen(), "yyyyMMddHHmmss");
return datePrefix +index + suffix;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
private byte getLastIP(){
byte lastip = 0;
try{
InetAddress ip = InetAddress.getLocalHost();
byte[] ipByte = ip.getAddress();
lastip = ipByte[ipByte.length - 1];
} catch (UnknownHostException e) {
log.error("Erro de UnknownHostException: {}", e.getMessage());
}
return lastip;
}
public static void main(String[] args) {
IdGenerator id = IdGenerator.USER_TRANSID;
for (int i = 0; i < 1000; i++) {
String serialNo = id.nextId();
System.out.println(serialNo + "===" + serialNo.length());
}
}
}
Mapper:
package com.jwt.server.mapper;
import com.jwt.server.domain.UserInfo;
public interface UserMapper {
/**
* Consulta um usuário pelo nome de usuário
*
* @param username
* @return
*/
public UserInfo findByUsername(String username);
/**
* Insere um novo usuário no banco
*
* @param user
* @return
*/
public UserInfo save(UserInfo user);
}
Mapper XML:
<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.jwt.server.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.jwt.server.domain.UserInfo">
<id column="id" jdbcType="VARCHAR" property="id" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="password" jdbcType="VARCHAR" property="password" />
</resultMap>
<!-- Consulta de login do usuário -->
<select id="findByUsername" parameterType="java.lang.String" resultMap="BaseResultMap">
select id,username,password from tb_user where username=#{username,jdbcType=VARCHAR}
</select>
<insert id="save" parameterType="com.jwt.server.domain.UserInfo">
INSERT INTO tb_user
(id,username,password) VALUES
(#{id,jdbcType=VARCHAR},#{username,jdbcType=VARCHAR},#{password,jdbcType=VARCHAR})
</insert>
</mapper>
Configuração do mybatis-config.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<typeAliases>
<typeAlias alias="Integer" type="java.lang.Integer" />
<typeAlias alias="Long" type="java.lang.Long" />
<typeAlias alias="HashMap" type="java.util.HashMap" />
<typeAlias alias="LinkedHashMap" type="java.util.LinkedHashMap" />
<typeAlias alias="ArrayList" type="java.util.ArrayList" />
<typeAlias alias="LinkedList" type="java.util.LinkedList" />
</typeAliases>
</configuration>
Configuração do application.yml:
# Configurações comuns, independentes do profile selecionado mapperLocations aponta para o caminho src/main/resources
mybatis:
typeAliasesPackage: com.jwt.server.domain
mapperLocations: classpath:mapper/*.xml
---
# Configurações para ambiente de desenvolvimento
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?prepStmtCacheSize=517&cachePrepStmts=true&autoReconnect=true&characterEncoding=utf-8&allowMultiQueries=true
username: root
password: tiger
Modificação da classe de inicialização para incluir scan de mappers:
package com.jwt.server;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@SpringBootApplication
@MapperScan("com.jwt.server.mapper")//
public class SpringJwtApplication {
public static void main(String[] args) {
SpringApplication.run(SpringJwtApplication.class, args);
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
Testes:
Teste de login personalizado:
Pronto, funcionando!
É importante notar que:
Na versão Spring Boot 2.0.4, como não há um PasswordEncoder padrão, é necessário injetá-lo manualmente. Sem a injeção, ocorrerá o seguinte erro durante a autenticação:
Se você testar e encontrar o seguinte erro, significa que você injetou o PasswordEncoder mas não criptografou a senha:
Além disso, se não armazenarmos as informações de login no banco, pode ocorrer outro problema: ao comparar a senha criptografada com a senha original, será gerado o seguinte erro:
Normalmente, quando usamos criptorgafia, durante a autorização o sistema comparará as senhas.
Esse erro ocorre neste ponto. A solução é:
Implementar uma classe de autenticação personalizada
Chamar menualmente e garantir que as senhas correspondam. Para mais detalhes, consulte a análise do código-fonte.