使用 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;

        public WeChatAuthProvider(LoginService loginService)
        {
            this.loginService = loginService;
        }

        public override async Task<ExternalAuthUserInfo> GetUserInfo(string accessCode)
        {

            var result = new ExternalAuthUserInfo();
            var weChatLoginResult = await loginService.Code2SessionAsync(accessCode);

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

            var seed = Guid.NewGuid().ToString("N").Substring(0, 7);

            result.Name = seed;
            result.UserName = seed;
            result.Surname = "微信用户";
            result.ProviderKey = weChatLoginResult.OpenId;
            result.Provider = nameof(WeChatAuthProvider);

            return result;
        }
    }

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

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

   private void ConfigureExternalAuth()
        {

            IocManager.Register<IExternalAuthConfiguration, ExternalAuthConfiguration>();
            var externalAuthConfiguration = IocManager.Resolve<IExternalAuthConfiguration>();
            var appId = _appConfiguration["WeChat:MiniProgram:AppId"];
            var appSecret = _appConfiguration["WeChat:MiniProgram:AppSecret"];
            externalAuthConfiguration.Providers.Add(new ExternalLoginProviderInfo(
                nameof(WeChatAuthProvider), appId, appSecret, typeof(WeChatAuthProvider))
                );
           
             );
        }

改写TokenAuthController.cs 中的ExternalAuthenticate方法

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

            //将openId传给ProviderKey
            model.ProviderKey = externalUser.ProviderKey;

            var loginResult = await _logInManager.LoginAsync(new UserLoginInfo(model.AuthProvider, model.ProviderKey, model.AuthProvider), GetTenancyNameOrNull());

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

                        // Try to login again with newly registered user!
                        loginResult = await _logInManager.LoginAsync(new UserLoginInfo(model.AuthProvider, model.ProviderKey, model.AuthProvider), GetTenancyNameOrNull());
                        if (loginResult.Result != AbpLoginResultType.Success)
                        {
                            throw _abpLoginResultTypeHelper.CreateExceptionForFailedLoginAttempt(
                                loginResult.Result,
                                model.ProviderKey,
                                GetTenancyNameOrNull()
                            );
                        }

                        return new ExternalAuthenticateResultModel
                        {
                            AccessToken = CreateAccessToken(CreateJwtClaims(loginResult.Identity)),
                            ExpireInSeconds = (int)_configuration.Expiration.TotalSeconds
                        };
                    }
                default:
                    {
                        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"));
            //}

            return userInfo;
        }

鉴权状态验证模块

整个鉴权登录的过程我们需要维护鉴权状态(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))
        {

        }
    }

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

 public class MiniappManager : DomainService
    {

        private readonly ACodeService aCodeService;
        private readonly WechatMiniappLoginTokenCache wechatMiniappLoginTokenCache;

        public MiniappManager(ACodeService aCodeService,
            WechatMiniappLoginTokenCache wechatMiniappLoginTokenCache)
        {
            this.aCodeService=aCodeService;
            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;
        }


        public virtual async Task SetTokenAsync(string token, string status, string providerAccessCode, bool isCheckToken = true, DateTimeOffset? absoluteExpireTime = null)
        {
            if (isCheckToken)
            {
                await this.CheckTokenAsync(token);

            }
            await wechatMiniappLoginTokenCache.SetAsync(token, new WechatMiniappLoginTokenCacheItem()
            {
                Status=status,
                ProviderAccessCode=providerAccessCode
            }, absoluteExpireTime: absoluteExpireTime);
        }



        public virtual async Task CheckTokenAsync(string token)
        {
            var cacheItem = await wechatMiniappLoginTokenCache.GetAsync(token, null);

            if (cacheItem == null)
            {
                throw new UserFriendlyException("WechatMiniappLoginInvalidToken",
            "微信小程序登录Token不合法");
            }
            else
            {
                if (cacheItem.Status=="AUTHORIZED")
                {
                    throw new UserFriendlyException("WechatMiniappLoginInvalidToken",
           "微信小程序登录Token已失效");
                }
            }
        }

 

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

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

            await wechatMiniappLoginTokenCache.SetAsync(token, new WechatMiniappLoginTokenCacheItem()
            {
                Status="CREATED",
            },
            absoluteExpireTime: absoluteExpireTime);


            var result = await aCodeService.GetUnlimitedACodeAsync(token, page);

            return result.BinaryData;
        }

生成后会将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;

        public MiniappAppService(MiniappManager miniappManager)
        {
            this.miniappManager=miniappManager;
        }

        ...
    }

编写各方法

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

            var result = await miniappManager.GetACodeAsync(input.Scene, input.Page, DateTimeOffset.Now.Add(TokenCacheDuration));

            return new FileContentResult(result, MimeTypeNames.ImagePng);


        }

        [HttpGet]
        [AbpAllowAnonymous]
        public virtual async Task<WechatMiniappLoginTokenCacheItem> GetTokenAsync(string token)
        {
            var cacheItem = await miniappManager.GetTokenAsync(token);
            return cacheItem;
        }




        [AbpAllowAnonymous]
        public virtual async Task AccessAsync(string token)
        {
            await miniappManager.SetTokenAsync(token, "ACCESSED", null, true, DateTimeOffset.Now.Add(AuthCacheDuration));
        }

        [AbpAllowAnonymous]
        public virtual async Task AuthenticateAsync(ChangeStatusInput input)
        {
            await miniappManager.SetTokenAsync(input.Token, "AUTHORIZED", input.ProviderAccessCode, true, DateTimeOffset.Now.Add(TimeSpan.FromMinutes(1)));
        }

GetACodeAsync:获取小程序码Api,

GetTokenAsync:获取Token对应值Api,

AccessAsync:已扫码调用的Api,

AuthenticateAsync:已授权调用的Api,

编辑

 

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

posted @ 2022-06-24 11:05  林晓lx  阅读(600)  评论(6编辑  收藏  举报