使用 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<ExternalAuthUserInfo> <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<IExternalAuthConfiguration, ExternalAuthConfiguration>();
<span class="hljs-keyword">var externalAuthConfiguration = IocManager.Resolve<IExternalAuthConfiguration>();
<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<WechatMiniappLoginTokenCacheItem> <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 搭建第三方登录模块(二):服务端开发