使用 Abp.Zero 搭建第三方登录模块(二):服务端开发

微信SDK库的集成

微信SDK库是针对微信相关 API 进行封装的模块 ,目前开源社区中微信SDK库数量真是太多了,我选了一个比较好用的EasyAbp WeChat库。

EasyAbp/Abp.WeChat: Abp 微信 SDK 模块,包含对微信小程序、公众号、企业微信、开放平台、第三方平台等相关接口封装。 (github.com)

当然这个库是ABP vNext 框架的,需要稍微改写一下。封装好后我们需要以下几个接口

小程序码生成接口:


public Task<GetUnlimitedACodeResponse> GetUnlimitedACodeAsync(string scene, string page = null, short width = 430, bool autoColor = false, LineColorModel lineColor = null, bool isHyaline = false)
       

 获取用户OpenId与SessionKey的接口

public async Task<Code2SessionResponse> Code2SessionAsync(string jsCode, string grantType = "authorization_code")

 

 第三方登录模块

Abp.Zero 第三方登录机制

我们先来回顾一下第三方登录在Abp.Zero 中的实现方式

AbpUserLogin表中存储第三方账户唯一Id和系统中的User的对应关系,如图

在登陆时,在ExternalAuthenticate方法中,需要传递登录凭证,也就是ProviderAccessCode,这是一个临时凭据,根据它拿到并调用对应Provider的GetUserInfo方法,获取第三方登录信息,包括第三方账户唯一Id

之后调用GetExternalUserInfo,它会去AbpUserLogin表中根据第三方账户唯一Id查找是否有已注册的用户

若有,直接返回这个用户信息;

若没有,则先注册一个用户,插入对应关系。并返回用户。

接下来就是普通登录的流程:验证用户状态,验证密码,插入登录信息等操作。

编写代码

appsettings.json中添加微信小程序的配置,配置好AppId和AppSecret

...

“WeChat”: {
“MiniProgram”: {
“Token”: “”,
“OpenAppId”: “”,
“AppId”: “000000000000000000”,
“AppSecret”: “XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX”,
“EncodingAesKey”: “”
}
}

 

新建一个WeChatAuthProvider 并继承于ExternalAuthProviderApiBase,编写登录

    internal class WeChatAuthProvider : ExternalAuthProviderApiBase
    {
        private readonly LoginService loginService;

    <span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-title">WeChatAuthProvider(<span class="hljs-params">LoginService loginService)
    {
        <span class="hljs-keyword">this.loginService = loginService;
    }

    <span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-keyword">override <span class="hljs-keyword">async Task&lt;ExternalAuthUserInfo&gt; <span class="hljs-title">GetUserInfo(<span class="hljs-params"><span class="hljs-built_in">string accessCode)
    {

        <span class="hljs-keyword">var result = <span class="hljs-keyword">new ExternalAuthUserInfo();
        <span class="hljs-keyword">var weChatLoginResult = <span class="hljs-keyword">await loginService.Code2SessionAsync(accessCode);

        <span class="hljs-comment">//小程序调用获取token接口 https://api.weixin.qq.com/cgi-bin/token 返回的token值无法用于网页授权接口!
        <span class="hljs-comment">//tips:https://www.cnblogs.com/remon/p/6420418.html
        <span class="hljs-comment">//var userInfo = await loginService.GetUserInfoAsync(weChatLoginResult.OpenId);

        <span class="hljs-keyword">var seed = Guid.NewGuid().ToString(<span class="hljs-string">"N").Substring(<span class="hljs-number">0, <span class="hljs-number">7);

        result.Name = seed;
        result.UserName = seed;
        result.Surname = <span class="hljs-string">"微信用户";
        result.ProviderKey = weChatLoginResult.OpenId;
        result.Provider = <span class="hljs-keyword">nameof(WeChatAuthProvider);

        <span class="hljs-keyword">return result;
    }
}

 WebCore项目中,将WeChatAuthProvider 注册到Abp.Zero第三方登录的Providers中

微信的AppId和AppSecret分别对应ClientId,ClientSecret

   private void ConfigureExternalAuth()
        {

        IocManager.Register&lt;IExternalAuthConfiguration, ExternalAuthConfiguration&gt;();
        <span class="hljs-keyword">var externalAuthConfiguration = IocManager.Resolve&lt;IExternalAuthConfiguration&gt;();
        <span class="hljs-keyword">var appId = _appConfiguration[<span class="hljs-string">"WeChat:MiniProgram:AppId"];
        <span class="hljs-keyword">var appSecret = _appConfiguration[<span class="hljs-string">"WeChat:MiniProgram:AppSecret"];
        externalAuthConfiguration.Providers.Add(<span class="hljs-keyword">new ExternalLoginProviderInfo(
            <span class="hljs-keyword">nameof(WeChatAuthProvider), appId, appSecret, <span class="hljs-keyword">typeof(WeChatAuthProvider))
            );
       
         );
    }</span></span></span></span></span></span></span></span></span></span></span></span></code></pre>

改写TokenAuthController.cs 中的ExternalAuthenticate方法

        private async Task<ExternalAuthenticateResultModel> ExternalAuthenticate(ExternalAuthenticateModel model)
        {
            var externalUser = await GetExternalUserInfo(model);

        <span class="hljs-comment">//将openId传给ProviderKey
        model.ProviderKey = externalUser.ProviderKey;

        <span class="hljs-keyword">var loginResult = <span class="hljs-keyword">await _logInManager.LoginAsync(<span class="hljs-keyword">new UserLoginInfo(model.AuthProvider, model.ProviderKey, model.AuthProvider), GetTenancyNameOrNull());

        <span class="hljs-keyword">switch (loginResult.Result)
        {
            <span class="hljs-keyword">case AbpLoginResultType.Success:
                {
                    <span class="hljs-keyword">var accessToken = CreateAccessToken(CreateJwtClaims(loginResult.Identity));
                    <span class="hljs-keyword">return <span class="hljs-keyword">new ExternalAuthenticateResultModel
                    {
                        AccessToken = accessToken,
                        EncryptedAccessToken = GetEncryptedAccessToken(accessToken),
                        ExpireInSeconds = (<span class="hljs-built_in">int)_configuration.Expiration.TotalSeconds
                    };
                }
            <span class="hljs-keyword">case AbpLoginResultType.UnknownExternalLogin:
                {
                    <span class="hljs-keyword">var newUser = <span class="hljs-keyword">await RegisterExternalUserAsync(externalUser);
                    <span class="hljs-keyword">if (!newUser.IsActive)
                    {
                        <span class="hljs-keyword">return <span class="hljs-keyword">new ExternalAuthenticateResultModel
                        {
                            WaitingForActivation = <span class="hljs-literal">true
                        };
                    }

                    <span class="hljs-comment">// Try to login again with newly registered user!
                    loginResult = <span class="hljs-keyword">await _logInManager.LoginAsync(<span class="hljs-keyword">new UserLoginInfo(model.AuthProvider, model.ProviderKey, model.AuthProvider), GetTenancyNameOrNull());
                    <span class="hljs-keyword">if (loginResult.Result != AbpLoginResultType.Success)
                    {
                        <span class="hljs-keyword">throw _abpLoginResultTypeHelper.CreateExceptionForFailedLoginAttempt(
                            loginResult.Result,
                            model.ProviderKey,
                            GetTenancyNameOrNull()
                        );
                    }

                    <span class="hljs-keyword">return <span class="hljs-keyword">new ExternalAuthenticateResultModel
                    {
                        AccessToken = CreateAccessToken(CreateJwtClaims(loginResult.Identity)),
                        ExpireInSeconds = (<span class="hljs-built_in">int)_configuration.Expiration.TotalSeconds
                    };
                }
            <span class="hljs-literal">default:
                {
                    <span class="hljs-keyword">throw _abpLoginResultTypeHelper.CreateExceptionForFailedLoginAttempt(
                        loginResult.Result,
                        model.ProviderKey,
                        GetTenancyNameOrNull()
                    );
                }
        }
    }

 改写TokenAuthController.cs 中的GetExternalUserInfo方法

 private async Task<ExternalAuthUserInfo> GetExternalUserInfo(ExternalAuthenticateModel model)
        {
            var userInfo = await _externalAuthManager.GetUserInfo(model.AuthProvider, model.ProviderAccessCode);
            //if (userInfo.ProviderKey != model.ProviderKey)
            //{
            //    throw new UserFriendlyException(L("CouldNotValidateExternalUser"));
            //}

        <span class="hljs-keyword">return userInfo;
    }</span></span></span></span></span></span></span></span></span></span></span></span></code></pre>

鉴权状态验证模块

整个鉴权登录的过程我们需要维护鉴权状态(Status),在获取到登录凭证AccessCode 后及时写入值。

鉴权状态将有:

CREATED:  已建立,等待用户扫码

ACCESSED: 已扫码,等待用户确认授权

AUTHORIZED: 已授权完成

EXPIRED: 小程序码过期,已失效

我们需要建立一个缓存,来存储上述值

建立WechatMiniappLoginTokenCacheItem, 分别创建Status属性和ProviderAccessCode 

    public class WechatMiniappLoginTokenCacheItem
    {
        public string Status { get; set; }
        public string ProviderAccessCode { get; set; }
    }

建立缓存类型WechatMiniappLoginTokenCache。

    public class WechatMiniappLoginTokenCache : MemoryCacheBase<WechatMiniappLoginTokenCacheItem>, ISingletonDependency
    {
        public WechatMiniappLoginTokenCache() : base(nameof(WechatMiniappLoginTokenCache))
        {

    }
}</span></span></span></span></span></span></span></span></span></span></span></span></code></pre>

在Domain项目中新建MiniappManager类作为领域服务,并注入ACodeService微信小程序码生成服务 和WechatMiniappLoginTokenCache缓存对象

 public class MiniappManager : DomainService
    {

    <span class="hljs-keyword">private <span class="hljs-keyword">readonly ACodeService aCodeService;
    <span class="hljs-keyword">private <span class="hljs-keyword">readonly WechatMiniappLoginTokenCache wechatMiniappLoginTokenCache;

    <span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-title">MiniappManager(<span class="hljs-params">ACodeService aCodeService,
        WechatMiniappLoginTokenCache wechatMiniappLoginTokenCache)
    {
        <span class="hljs-keyword">this.aCodeService=aCodeService;
        <span class="hljs-keyword">this.wechatMiniappLoginTokenCache=wechatMiniappLoginTokenCache;
    }

    ...

}

 

分别建立SetTokenAsync,GetTokenAsync和CheckTokenAsync,分别用于设置Token对应值,获取Token对应值和Token对应值合法性校验

 public virtual async Task<WechatMiniappLoginTokenCacheItem> GetTokenAsync(string token)
        {
            var cacheItem = await wechatMiniappLoginTokenCache.GetAsync(token, null);
            return cacheItem;
        }


    <span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-keyword">virtual <span class="hljs-keyword">async Task <span class="hljs-title">SetTokenAsync(<span class="hljs-params"><span class="hljs-built_in">string token, <span class="hljs-built_in">string status, <span class="hljs-built_in">string providerAccessCode, <span class="hljs-built_in">bool isCheckToken = <span class="hljs-literal">true, DateTimeOffset? absoluteExpireTime = <span class="hljs-literal">null)
    {
        <span class="hljs-keyword">if (isCheckToken)
        {
            <span class="hljs-keyword">await <span class="hljs-keyword">this.CheckTokenAsync(token);

        }
        <span class="hljs-keyword">await wechatMiniappLoginTokenCache.SetAsync(token, <span class="hljs-keyword">new WechatMiniappLoginTokenCacheItem()
        {
            Status=status,
            ProviderAccessCode=providerAccessCode
        }, absoluteExpireTime: absoluteExpireTime);
    }



    <span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-keyword">virtual <span class="hljs-keyword">async Task <span class="hljs-title">CheckTokenAsync(<span class="hljs-params"><span class="hljs-built_in">string token)
    {
        <span class="hljs-keyword">var cacheItem = <span class="hljs-keyword">await wechatMiniappLoginTokenCache.GetAsync(token, <span class="hljs-literal">null);

        <span class="hljs-keyword">if (cacheItem == <span class="hljs-literal">null)
        {
            <span class="hljs-keyword">throw <span class="hljs-keyword">new UserFriendlyException(<span class="hljs-string">"WechatMiniappLoginInvalidToken",
        <span class="hljs-string">"微信小程序登录Token不合法");
        }
        <span class="hljs-keyword">else
        {
            <span class="hljs-keyword">if (cacheItem.Status==<span class="hljs-string">"AUTHORIZED")
            {
                <span class="hljs-keyword">throw <span class="hljs-keyword">new UserFriendlyException(<span class="hljs-string">"WechatMiniappLoginInvalidToken",
       <span class="hljs-string">"微信小程序登录Token已失效");
            }
        }
    }</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>

 

编写GetACodeAsync,生成微信小程序码

public async Task<byte[]> GetACodeAsync(string token, string page, DateTimeOffset? absoluteExpireTime = null)
        {

        <span class="hljs-keyword">await wechatMiniappLoginTokenCache.SetAsync(token, <span class="hljs-keyword">new WechatMiniappLoginTokenCacheItem()
        {
            Status=<span class="hljs-string">"CREATED",
        },
        absoluteExpireTime: absoluteExpireTime);


        <span class="hljs-keyword">var result = <span class="hljs-keyword">await aCodeService.GetUnlimitedACodeAsync(token, page);

        <span class="hljs-keyword">return result.BinaryData;
    }</span></span></span></span></span></span></span></span></span></span></span></span></code></pre>

生成后会将Token值写入缓存,此时状态为CREATED,对应的页面为“已扫码”

之后以byte[]方式返回小程序码图片

编写Api接口

在Application项目中新建MiniappAppService 类作为领域服务,并注入MiniappManager对象

    [AbpAllowAnonymous]
    //[AbpAuthorize(PermissionNames.Pages_Wechat)]
    public class MiniappAppService : AppServiceBase
    {
        public static TimeSpan TokenCacheDuration = TimeSpan.FromMinutes(5);
        public static TimeSpan AuthCacheDuration = TimeSpan.FromMinutes(5);
        private readonly MiniappManager miniappManager;

    <span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-title">MiniappAppService(<span class="hljs-params">MiniappManager miniappManager)
    {
        <span class="hljs-keyword">this.miniappManager=miniappManager;
    }

    ...
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>

编写各方法

        [HttpGet]
        [WrapResult(WrapOnSuccess = false, WrapOnError = false)]
        public async Task<IActionResult> GetACodeAsync(GetACodeAsyncInput input)
        {
            var mode = input.Mode;

        <span class="hljs-keyword">var result = <span class="hljs-keyword">await miniappManager.GetACodeAsync(input.Scene, input.Page, DateTimeOffset.Now.Add(TokenCacheDuration));

        <span class="hljs-keyword">return <span class="hljs-keyword">new FileContentResult(result, MimeTypeNames.ImagePng);


    }

    [<span class="hljs-meta">HttpGet]
    [<span class="hljs-meta">AbpAllowAnonymous]
    <span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-keyword">virtual <span class="hljs-keyword">async Task&lt;WechatMiniappLoginTokenCacheItem&gt; <span class="hljs-title">GetTokenAsync(<span class="hljs-params"><span class="hljs-built_in">string token)
    {
        <span class="hljs-keyword">var cacheItem = <span class="hljs-keyword">await miniappManager.GetTokenAsync(token);
        <span class="hljs-keyword">return cacheItem;
    }




    [<span class="hljs-meta">AbpAllowAnonymous]
    <span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-keyword">virtual <span class="hljs-keyword">async Task <span class="hljs-title">AccessAsync(<span class="hljs-params"><span class="hljs-built_in">string token)
    {
        <span class="hljs-keyword">await miniappManager.SetTokenAsync(token, <span class="hljs-string">"ACCESSED", <span class="hljs-literal">null, <span class="hljs-literal">true, DateTimeOffset.Now.Add(AuthCacheDuration));
    }

    [<span class="hljs-meta">AbpAllowAnonymous]
    <span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-keyword">virtual <span class="hljs-keyword">async Task <span class="hljs-title">AuthenticateAsync(<span class="hljs-params">ChangeStatusInput input)
    {
        <span class="hljs-keyword">await miniappManager.SetTokenAsync(input.Token, <span class="hljs-string">"AUTHORIZED", input.ProviderAccessCode, <span class="hljs-literal">true, DateTimeOffset.Now.Add(TimeSpan.FromMinutes(<span class="hljs-number">1)));
    }</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>

GetACodeAsync:获取小程序码Api,

GetTokenAsync:获取Token对应值Api,

AccessAsync:已扫码调用的Api,

AuthenticateAsync:已授权调用的Api,

 

至此,完成了所有服务端接口

使用 Abp.Zero 搭建第三方登录模块(二):服务端开发

https://blog.matoapp.net/posts/eab1cbaf/

作者

林晓lx

发布于

2022-06-24

更新于

2024-09-11

许可协议

评论