Integração Completa do Spring Boot 2.0.4 com MyBatis para Autenticação JWT

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.

Tags: spring-boot MyBatis jwt autenticacao Segurança

Publicado em 6-11 19:49 por Thomas