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

编辑
至此,完成了所有服务端接口
使用 Abp.Zero 搭建第三方登录模块(二):服务端开发的更多相关文章
- 使用 Abp.Zero 搭建第三方登录模块(一):原理篇
第三方登录是基于用户在第三方平台上(如微信,QQ, 百度)已有的账号来快速完成系统的登录.注册-登录等功能. 微信的鉴权 以微信的鉴权为例: 假如你的网站有一个扫码登录的功能,会弹出一个由微信提供的 ...
- 使用 Abp.Zero 搭建第三方登录模块(三):网页端开发
简短回顾一下网页端的流程,总的来说网页端的职责有三: 生成一个随机字符作为鉴权会话的临时Token, 生成一个小程序码, Token作为参数固化于小程序码当中 监控整个鉴权过程状态,一旦状态变为AU ...
- Day01_搭建环境&CMS服务端开发
学成在线 第1天 讲义-项目概述 CMS接口开发 1 项目的功能构架 1.1 项目背景 受互联网+概念的催化,当今中国在线教育市场的发展可谓是百花齐放.如火如荼. 按照市场领域细分为:学前教育.K12 ...
- Swift3.0服务端开发(一) 完整示例概述及Perfect环境搭建与配置(服务端+iOS端)
本篇博客算是一个开头,接下来会持续更新使用Swift3.0开发服务端相关的博客.当然,我们使用目前使用Swift开发服务端较为成熟的框架Perfect来实现.Perfect框架是加拿大一个创业团队开发 ...
- node.js中ws模块创建服务端和客户端,网页WebSocket客户端
首先下载websocket模块,命令行输入 npm install ws 1.node.js中ws模块创建服务端 // 加载node上websocket模块 ws; var ws = require( ...
- Photon Server 实现注册与登录(二) --- 服务端代码整理
一.有的代码前端和后端都会用到.比如一些请求的Code.使用需要新建项目存放公共代码. 新建项目Common存放公共代码: EventCode :存放服务端自动发送信息给客户端的code Operat ...
- go语言游戏服务端开发(二)——网络通信
一.网络层 网络游戏客户端除了全局登录使用http请求外,一般通过socket长连接与服务端保持连接.go语言的net包提供网络socket长连接相关操作. 对于服务端,一般经历 Listen.Acc ...
- 使用.NET开发搭建OpenAI模型的中间服务端
前言:前不久微信上大家玩ChatGPT聊天机器人玩的不亦乐乎:不过随着ChatGPT被封杀,所以用微信聊天机器人有可能导致封号的风险.那如果自己不想每次都去OpenAI官网上进行对话[PS:官网上面聊 ...
- Swift3.0服务端开发(二) 静态文件添加、路由配置以及表单提交
今天博客中就来聊一下Perfect框架的静态文件的添加与访问,路由的配置以及表单的提交.虽然官网上有聊静态文件的访问的部分,但是在使用Perfect框架来访问静态文件时还是有些点需要注意的,这些关键点 ...
- MVC系列学习(十二)-服务端的验证
在前一讲,提到过,客户端的东西永远可以造假,所以我们还要在服务端进行验证 注意:先加载表单,后添加js文件,才能有效:而先加载js,后添加表单,是没有效果的 1.视图与Model中的代码如下 2.一张 ...
随机推荐
- 深入探索OCR技术:前沿算法与工业级部署方案揭秘
深入探索OCR技术:前沿算法与工业级部署方案揭秘 注:以上图片来自网络 1. OCR技术背景 1.1 OCR技术的应用场景 OCR是什么 OCR(Optical Character Recogniti ...
- 记录一次py2编码带来的坑
"中文" == u"中文" # PY2 False"中文" == u"中文" # PY3 True
- yarn常用命令
1. 安装 npm install yarn -g 2. 设置淘宝镜像 yarn config set npmRegistryServer https://registry.npm.taobao.or ...
- 基于队列实现生产者消费者(Python)
# 进城之间数据隔离 # 进程之间通信(IPC) Inter Process communication # 基于文件 :同一台机器上的多个进程之间通信 # Queue 队列 # 基于socket的文 ...
- SpringBoot 整合多数据源的事务问题
代码 先贴代码: 核心就是:Spring给我们提供的一个类 AbstractRoutingDataSource,然后我们再写一个切面来切换数据源,肯定要有一个地方存储key还要保证上下文都可用,所以我 ...
- 使用Docker单机部署Ceph
安装Docker过程参考:https://www.cnblogs.com/hackyo/p/9280042.html 1. 创建Ceph专用网络 sudo docker network create ...
- Skywalking-Aop Docker单机环境搭建
1.OAP-SERVER和UI环境搭建 本次搭建是基于MySQL进行持久化,因此需要提前准备好一个MySQL容器 (MySQL容器部署略过).如有错误还请指正. 1.1 OAP服务搭建 拉取skywa ...
- UVALive7146 Defeat the Enemy
题目链接 题目 见链接. 题解 知识点:贪心,STL. 首先要保证我方军队能消灭对方军队才行,因此只要我们按攻击力从大到小排,对方按防御力从大到小排,从大到小遍历,用我方所有攻击力大于敌方目前防御力军 ...
- C 语言常用头文件解释
C系统提供了丰富的系统文件,称为库文件,整理一下以后好实用: <stdio.h> 定义了三个变量类型.一些宏和各种函数来执行输入和输出 https://www.runoob.com/cpr ...
- java 注解结合 spring aop 实现自动输出日志
auto-log auto-log 是一款为 java 设计的自动日志监控框架. 创作目的 经常会写一些工具,有时候手动加一些日志很麻烦,引入 spring 又过于大材小用. 所以希望从从简到繁实现一 ...