Integração de Microsserviços e Autenticação em Sistema de Saúde

  1. Listagem de Hospitais

No módulo de gestão hospitalar, a lista de hospitais exibe níveis hospitalares, mas os dados brutos contêm apenas o campo hostype. É necessário consultar o dicionário de dados para obter as informações de nível. Isso requer uma chamada remota do service-hosp para o service-cmn, introduzindo um centro de registro e chamadas de serviço.

Utiliza-se Nacos como centro de registro. Cria-se um projeto service-cmn-client com uma interface DictConsultaClient para chamadas externas.

@FeignClient(value = "servico-cmn", path = "/admin/cmn/dict/")
public interface DictConsultaClient {

    // Consulta por código e valor do dicionário
    @GetMapping("obterNome/{codigoDado}/{valor}")
    public String obterNome(@PathVariable("codigoDado") String codigoDado,
                            @PathVariable("valor") String valor);

    // Consulta apenas por valor
    @GetMapping("obterNome/{valor}")
    public String obterNome(@PathVariable("valor") String valor);
}

O service-hosp utiliza esta interface cliente para acessar os endpoints do controller do service-cmn.

1.1 Visualização da Lista de Hospitais

Implementação da interface para listar hospitais com paginação e filtros básicos.

1.2 Exibição de Detalhes do Hospital

Exibição de informações detalhadas de um hospital específico, incluindo dados cadastrais e serviços oferecidos.

1.3 Exibição de Informações de Agendamento

O agendamento é dividido em três partes: informações de departamento em formato de árvore, datas de agendamento com paginação e detalhes dos médicos disponíveis para cada data.

A seção esquerda exibe a lista de departamentos do hospital, recuperada por código hospitalar.

1.3.1 Consulta de Árvore de Departamentos

Para obter a estrutura hierárquica de departamentos, agrupam-se os departamentos por código pai.

public List<DepartamentoVo> buscarArvoreDepartamentos(String codigoHospital) {
    List<DepartamentoVo> listaResultado = new ArrayList<>();

    Departamento consultaDepartamento = new Departamento();
    consultaDepartamento.setCodigoHospital(codigoHospital);
    Example<Departamento> exemplo = Example.of(consultaDepartamento);
    List<Departamento> listaDepartamentos = repositorioDepartamentos.findAll(exemplo);

    // Agrupa departamentos por código do departamento pai
    Map<String, List<Departamento>> mapaDepartamentos = listaDepartamentos.stream()
            .collect(Collectors.groupingBy(Departamento::getCodigoDepartamentoPai));

    for (Map.Entry<String, List<Departamento>> entrada : mapaDepartamentos.entrySet()) {
        String codigoPai = entrada.getKey();
        List<Departamento> subdepartamentos = entrada.getValue();

        DepartamentoVo departamentoPai = new DepartamentoVo();
        departamentoPai.setCodigo(codigoPai);
        departamentoPai.setNome(subdepartamentos.get(0).getNomeDepartamentoPai());

        List<DepartamentoVo> filhos = new ArrayList<>();
        for (Departamento dept : subdepartamentos) {
            DepartamentoVo filho = new DepartamentoVo();
            filho.setNome(dept.getNomeDepartamento());
            filho.setCodigo(dept.getCodigoDepartamento());
            filhos.add(filho);
        }
        departamentoPai.setFilhos(filhos);
        listaResultado.add(departamentoPai);
    }
    return listaResultado;
}

  1. Introdução ao API Gateway

Em uma arquitetura de microsserviços, o API Gateway resolve problemas como complexidade do cliente, requisições cross-origin, autenticação fragmentada, dificuldade de refatoração e protocolos incompatíveis. Ele atua como uma camada intermediária, gerenciando segurança, desempenho e monitoramento.

2.1 Spring Cloud Gateway

O Gateway e os microsserviços são registrados no Nacos para roteamento e execução de filtros.

Cria-se o módulo service-gateway com as seguintes configurações:

# Porta do serviço
server.port=80
# Nome da aplicação
spring.application.name=servico-gateway

# Endereço do Nacos
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

# Ativação de roteamento por descoberta
spring.cloud.gateway.discovery.locator.enabled=true

# Configuração de rotas
spring.cloud.gateway.routes[0].id=servico-hosp
spring.cloud.gateway.routes[0].uri=lb://servico-hosp
spring.cloud.gateway.routes[0].predicates=Path=/*/hosp/**

spring.cloud.gateway.routes[1].id=servico-cmn
spring.cloud.gateway.routes[1].uri=lb://servico-cmn
spring.cloud.gateway.routes[1].predicates=Path=/*/cmn/**

Se a porta 80 estiver ocupada (por exemplo, pelo Nginx), é necessário liberá-la. No frontend, ajusta-se o BASE_API para http://localhost sem porta explícita.

Para configurar cross-origin, remove-se a anotação @CrossOrigin dos controllers e define-se um filtro global:

@Configuration
public class ConfiguracaoCors {
    @Bean
    public CorsWebFilter filtroCorsWeb() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        UrlBasedCorsConfigurationSource fonte = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        fonte.registerCorsConfiguration("/**", config);
        return new CorsWebFilter(fonte);
    }
}

  1. Configuração do Frontend do Usuário

3.1 Introdução ao Nuxt.js

Nuxt.js é um framework leve baseado em Vue.js para criar aplicações com renderização no lado do servidor (SSR) ou sites estáticos, oferecendo estrutura modular e hot reloading.

Para iniciar: npm install e npm run dev, acessando http://localhost:3000.

3.2 Estrutura de Diretórios do Nuxt

  • assets: Recursos estáticos não compilados.
  • components: Componentes Vue.js sem extensões especiais do Nuxt.
  • layouts: Componentes de layout da aplicação.
  • pages: Rotas e visualizações, com geração automática de configurações.
  • plugins: Plugins JavaScript para execução antes da instanciação do Vue.
  • nuxt.config.js: Configurações personalizadas para sobrescrever padrões.

3.3 Funcionalidade de Login

Implementa-se login via email com código de verificação de seis dígitos, armazenado no Redis.

// Envio de código por email
@GetMapping("enviar/{email}")
public Resultado enviarCodigo(@PathVariable String email) {
    String codigo = modeloRedis.opsForValue().obter(email);
    if (!StringUtils.isEmpty(codigo)) {
        return Resultado.ok();
    }

    codigo = UtilAleatorio.getCodigoSeisDigitos();
    boolean enviado = servicoMsm.enviar(email, codigo);
    if (enviado) {
        modeloRedis.opsForValue().definir(email, codigo, 2, TimeUnit.MINUTOS);
        return Resultado.ok();
    } else {
        return Resultado.falha().mensagem("Falha ao enviar email");
    }
}

Após verificação do código, se o email for novo, registra-se o usuário no banco yygh_user e gera-se um token JWT.

@Override
public Map<String, Object> loginUsuario(LoginVo dadosLogin) {
    String email = dadosLogin.getEmail();
    String codigo = dadosLogin.getCodigo();
    if (StringUtils.isEmpty(email) || StringUtils.isEmpty(codigo)) {
        throw new HospitalException(ResultadoEnum.ERRO_PARAMETRO);
    }

    String codigoRedis = modeloRedis.opsForValue().obter(email);
    if (!codigo.equals(codigoRedis)) {
        throw new HospitalException(ResultadoEnum.ERRO_CODIGO);
    }

    InfoUsuario usuario = null;
    ConsultaWrapper<InfoUsuario> wrapper = new ConsultaWrapper<>();
    wrapper.eq("email", email);
    usuario = baseMapper.selecionarUm(wrapper);
    if (usuario == null) {
        usuario = new InfoUsuario();
        usuario.setNome("");
        usuario.setEmail(email);
        usuario.setStatus(1);
        baseMapper.inserir(usuario);
    }

    if (usuario.getStatus() == 0) {
        throw new HospitalException(ResultadoEnum.ERRO_LOGIN_DESATIVADO);
    }

    Map<String, Object> mapa = new HashMap<>();
    String nome = usuario.getNome();
    if (StringUtils.isEmpty(nome)) {
        nome = usuario.getApelido();
    }
    if (StringUtils.isEmpty(nome)) {
        nome = usuario.getEmail();
    }
    mapa.put("nome", nome);

    String token = JwtAssistente.criarToken(usuario.getId(), nome);
    mapa.put("token", token);
    return mapa;
}

3.4 Login via WeChat

Utiliza-se QR code em modal para autenticação com WeChat, integrando-se ao módulo service-user.

Configuração no application.properties:

server.port=8160
wx.open.app_id=wxed9954c01bb89b47
wx.open.app_secret=a7482517235173ddb4083788de60b90e
wx.open.redirect_url=http://localhost:8160/api/ucenter/wx/callback
yygh.baseUrl=http://localhost:3000

Classe de configuração para propriedades estáticas:

@Component
public class ConstantesPropriedadesWx implements InitializingBean {

    @Value("${wx.open.app_id}")
    private String idApp;

    @Value("${wx.open.app_secret}")
    private String segredoApp;

    @Value("${wx.open.redirect_url}")
    private String urlRedirecionamento;

    @Value("${yygh.baseUrl}")
    private String baseUrlYygh;

    public static String ID_APP_WX;
    public static String SEGREDO_APP_WX;
    public static String URL_REDIRECIONAMENTO_WX;
    public static String BASE_URL_YYGH;

    @Override
    public void afterPropertiesSet() {
        ID_APP_WX = idApp;
        SEGREDO_APP_WX = segredoApp;
        URL_REDIRECIONAMENTO_WX = urlRedirecionamento;
        BASE_URL_YYGH = baseUrlYygh;
    }
}

Controller para geração de QR code:

@GetMapping("obterParametrosLogin")
@ResponseBody
public Resultado gerarQrConnect() {
    try {
        Map<String, Object> mapa = new HashMap<>();
        mapa.put("appid", ConstantesPropriedadesWx.ID_APP_WX);
        mapa.put("scope", "snsapi_login");
        String urlRedirecionamento = URLEncoder.encode(ConstantesPropriedadesWx.URL_REDIRECIONAMENTO_WX, "utf-8");
        mapa.put("redirect_uri", urlRedirecionamento);
        mapa.put("state", System.currentTimeMillis() + "");
        return Resultado.ok(mapa);
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
        return null;
    }
}

No callback após escaneamento, obtém-se um código temporário para troca por token de acesso e informações do usuário via APIs do WeChat.

@GetMapping("callback")
public String callback(String codigo, String estado) {
    // Passo 1: Obter código temporário
    String urlTokenAcesso = String.format(
        "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
        ConstantesPropriedadesWx.ID_APP_WX, ConstantesPropriedadesWx.SEGREDO_APP_WX, codigo
    );
    try {
        String infoToken = ClienteHttp.obter(urlTokenAcesso);
        JSONObject jsonToken = JSONObject.parseObject(infoToken);
        String tokenAcesso = jsonToken.getString("access_token");
        String idAberto = jsonToken.getString("openid");

        InfoUsuario usuario = servicoUsuario.selecionarPorIdAberto(idAberto);
        if (usuario == null) {
            String urlInfoUsuario = String.format(
                "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s",
                tokenAcesso, idAberto
            );
            String infoUsuarioStr = ClienteHttp.obter(urlInfoUsuario);
            JSONObject jsonUsuario = JSONObject.parseObject(infoUsuarioStr);
            String apelido = jsonUsuario.getString("nickname");
            String urlImagem = jsonUsuario.getString("headimgurl");

            usuario = new InfoUsuario();
            usuario.setApelido(apelido);
            usuario.setIdAberto(idAberto);
            usuario.setStatus(1);
            servicoUsuario.salvar(usuario);
        }

        Map<String, String> mapa = new HashMap<>();
        String nome = usuario.getNome();
        if (StringUtils.isEmpty(nome)) {
            nome = usuario.getApelido();
        }
        if (StringUtils.isEmpty(nome)) {
            nome = usuario.getTelefone();
        }
        mapa.put("nome", nome);

        if (StringUtils.isEmpty(usuario.getTelefone())) {
            mapa.put("openid", usuario.getIdAberto());
        } else {
            mapa.put("openid", "");
        }

        String token = JwtAssistente.criarToken(usuario.getId(), nome);
        mapa.put("token", token);
        return "redirect:" + ConstantesPropriedadesWx.BASE_URL_YYGH + "/weixin/callback?token=" + mapa.get("token") +
               "&openid=" + mapa.get("openid") + "&nome=" + URLEncoder.encode(mapa.get("nome"), "utf-8");
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

3.5 Autenticação Real

Endpoints para autenticação do usuário com dados de identidade, armazenados na tabela user-info.

// Autenticação do usuário
@PostMapping("autenticar/autenticarUsuario")
public Resultado autenticarUsuario(@RequestBody UserAuthVo dadosAuth, HttpServletRequest requisicao) {
    servicoUsuario.autenticarUsuario(AuthContextHolder.obterIdUsuario(requisicao), dadosAuth);
    return Resultado.ok();
}

// Obter informações do usuário por ID
@GetMapping("autenticar/obterInfoUsuario")
public Resultado obterInfoUsuario(HttpServletRequest requisicao) {
    Long idUsuario = AuthContextHolder.obterIdUsuario(requisicao);
    InfoUsuario usuario = servicoUsuario.obterPorId(idUsuario);
    return Resultado.ok(usuario);
}

3.6 Gerenciamento de Usuários

Interfaces no service-user para administração, incluindo listagem, bloqueio e aprovação de usuários.

// Lista de usuários com paginação e filtro
@GetMapping("{pagina}/{limite}")
public Resultado listar(@PathVariable Long pagina,
                        @PathVariable Long limite,
                        UserInfoQueryVo consulta) {
    Pagina<InfoUsuario> paginaParam = new Pagina<>(pagina, limite);
    IPagina<InfoUsuario> modeloPagina = servicoUsuario.selecionarPagina(paginaParam, consulta);
    return Resultado.ok(modeloPagina);
}

// Alterar estado de bloqueio do usuário
@GetMapping("bloquear/{idUsuario}/{status}")
public Resultado bloquear(@PathVariable Long idUsuario, @PathVariable Integer status) {
    servicoUsuario.bloquear(idUsuario, status);
    return Resultado.ok();
}

// Detalhes do usuário
@GetMapping("exibir/{idUsuario}")
public Resultado exibir(@PathVariable Long idUsuario) {
    Map<String, Object> mapa = servicoUsuario.exibir(idUsuario);
    return Resultado.ok(mapa);
}

// Alterar status de aprovação da autenticação
@GetMapping("aprovar/{idUsuario}/{statusAuth}")
public Resultado aprovar(@PathVariable Long idUsuario, @PathVariable Integer statusAuth) {
    servicoUsuario.aprovar(idUsuario, statusAuth);
    return Resultado.ok();
}

Tags: spring-cloud Nacos Feign spring-cloud-gateway nuxtjs

Publicado em 6-26 02:31