Integração de Login para WeChat Mini Program no Framework ABP Zero

O framework ABP Zero disponibiliza um módulo de autenticação externa robusto que permite integrar diversos provedores de terceiros. O fluxo básico consiste em validar as credenciais junto ao provedor, recuperar os dados do usuário e processar o registro ou login na base de dados do sistema.

No ecossistema do ABP, a persistência dessas informações ocorre principalmente em duas tabelas: AbpUsers, que armazena os dados cadastrais do perfil, e AbpUserLogins, que mapeia as credenciais externas. Os campos fundamentais para essa integração são:

  • LoginProvider: Nome do provedor (ex: WeChatMiniProgram).
  • ProviderKey: Identificador exclusivo do usuário no proveodr (OpenID).
  • TenantId: Identificador do locatário.
  • UserId: Chave estrangeira para o usuário interno.

Implementando o Provider de Autenticação

Para o WeChat Mini Program, é necessário criar uma classe que herde de ExternalAuthProviderApiBase. Esta classe será responsável por trocar o código temporário enviado pelo cliente pelo OpenID do usuário através da API oficial da Tencent.

using System;
using System.Net.Http;
using System.Threading.Tasks;
using Abp.AspNetZeroCore.Web.Authentication.External;
using Newtonsoft.Json.Linq;
using System.Linq;

namespace MyProject.Web.Authentication.External
{
    public class WeChatMiniProgramAuthProvider : ExternalAuthProviderApiBase
    {
        public const string ProviderName = "WeChatMiniProgram";
        private readonly IExternalAuthConfiguration _externalAuthConfig;
        private const string WeChatApiUrl = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&grant_type=authorization_code&js_code={2}";

        public WeChatMiniProgramAuthProvider(IExternalAuthConfiguration externalAuthConfig)
        {
            _externalAuthConfig = externalAuthConfig;
        }

        public override async Task<ExternalAuthUserInfo> GetUserInfo(string accessCode)
        {
            // O accessCode aqui é recebido como um JSON contendo o código e o nome do usuário
            var payload = JObject.Parse(accessCode);
            var jsCode = payload["Code"]?.ToString();
            var nickName = payload["NickName"]?.ToString();

            if (string.IsNullOrEmpty(jsCode))
            {
                throw new ArgumentException("Código do WeChat inválido.");
            }

            var settings = _externalAuthConfig.Providers.First(p => p.Name == ProviderName);
            var sessionData = await FetchWeChatSession(settings.ClientId, settings.ClientSecret, jsCode);

            return new ExternalAuthUserInfo
            {
                Provider = ProviderName,
                ProviderKey = sessionData.OpenId,
                Name = nickName,
                Surname = nickName,
                EmailAddress = $"{sessionData.OpenId}@wechat.com" // Email fictício para compatibilidade
            };
        }

        private async Task<WeChatSessionResult> FetchWeChatSession(string appId, string secret, string code)
        {
            using (var httpClient = new HttpClient())
            {
                var requestUrl = string.Format(WeChatApiUrl, appId, secret, code);
                var response = await httpClient.GetStringAsync(requestUrl);
                var jsonResult = JObject.Parse(response);

                if (jsonResult["errcode"] != null && jsonResult["errcode"].Value<int>() != 0)
                {
                    throw new Exception($"Erro na API WeChat: {jsonResult["errmsg"]}");
                }

                return new WeChatSessionResult
                {
                    OpenId = jsonResult["openid"].ToString(),
                    SessionKey = jsonResult["session_key"].ToString()
                };
            }
        }
    }

    internal class WeChatSessionResult
    {
        public string OpenId { get; set; }
        public string SessionKey { get; set; }
    }
}

Configuração do Ambiente

As credenciais (AppId e Secret) devem ser configuradas no arquivo appsettings.json dentro da seção de autenticação externa. Além disso, no módulo WebHost (ou WebMvc), é necessário registrar o provedor caso ele esteja habilitado nas configurações do sistema.

Um ponto crítico na integração com Mini Programs é o tratamento do ProviderKey. Diferente de fluxos OAuth2 padrão, o OpenID não deve transitar abertamente. No entanto, o ABP exige que o ProviderKey retornado pelo método GetUserInfo corresponda ao que será buscado no banco de dados.

Customizando o Fluxo no Controller

No TokenAuthController, é recomendável criar um método específico para o WeChat para evitar efeitos colatearis em outras integrações. O foco aqui é garantir que o login seja realizado utilizando o identificador único recuperado da API da Tencent.

[HttpPost]
public async Task<ExternalAuthenticateResultModel> WeChatLogin([FromBody] ExternalAuthenticateModel model)
{
    // Recupera informações validadas do provedor
    var externalUser = await GetExternalUserInfo(model);

    // Tenta realizar o login com o OpenID (ProviderKey) retornado
    var loginResult = await _logInManager.LoginAsync(
        new UserLoginInfo(model.AuthProvider, externalUser.ProviderKey, model.AuthProvider), 
        GetTenancyNameOrNull()
    );

    if (loginResult.Result == AbpLoginResultType.Success)
    {
        return await CreateAuthResult(loginResult);
    }
    
    if (loginResult.Result == AbpLoginResultType.UnknownExternalLogin)
    {
        // Se o usuário não existe, registra automaticamente
        var newUser = await RegisterExternalUserAsync(externalUser);
        
        // Novo login após registro
        var retryLogin = await _logInManager.LoginAsync(
            new UserLoginInfo(model.AuthProvider, externalUser.ProviderKey, model.AuthProvider), 
            GetTenancyNameOrNull()
        );

        return await CreateAuthResult(retryLogin);
    }

    throw _abpLoginResultTypeHelper.CreateExceptionForFailedLoginAttempt(loginResult.Result, model.ProviderKey, GetTenancyNameOrNull());
}

private async Task<ExternalAuthenticateResultModel> CreateAuthResult(AbpLoginResult<Tenant, User> loginResult)
{
    var token = CreateAccessToken(CreateJwtClaims(loginResult.Identity));
    return new ExternalAuthenticateResultModel
    {
        AccessToken = token,
        EncryptedAccessToken = GetEncrpyedAccessToken(token),
        ExpireInSeconds = (int)_configuration.Expiration.TotalSeconds
    };
}

Ao realizar a chamada para a API WeChatLogin, o cliente deve enviar tanto o AuthProvider quanto o ProviderAccessCode (que no caso deste exemplo é o JSON contendo o código de sessão). Uma vez autenticado, o servidor retornará o JWT para as chmaadas subsequentes, que deve ser enviado no cabeçalho Authorization: Bearer [token].

Tags: ABP-Framework ASP.NET-Core WeChat OAuth2 web-api

Publicado em 7-29 09:48